[LDAP](https://en.wikipedia.org/wiki/Lightweight_Directory_Access_Protocol) is a directory service we use to inventory the users,
groups, passwords, (some) email forwards and machines. It distributes
some configuration and password files to all machines and can reload
services.

Note that this documentation needs work, particularly regarding user
management procedures, see [issue 40129](https://gitlab.torproject.org/tpo/tpa/team/-/issues/40129).

[[_TOC_]]

# Tutorial

The main LDAP documentation is on [web interface][db.torproject.org]. See
specifically the instructions on how to:

[db.torproject.org]: https://db.torproject.org/

 * [reset a lost password](https://db.torproject.org/password.html)
 * [change your SSH key](https://db.torproject.org/doc-mail.html)

The rest of this document is targeted at sysadmins troubleshooting
LDAP issues, setting up new services, or trying to understand the
setup.

## Getting to know LDAP

You should have received an email like this when your LDAP account was
created:

    Subject: New ud-ldap account for <your name here>

That includes information about how to configure email forwarding and
SSH keys. You should follow those steps to configure your SSH key to
get SSH access to servers (see [ssh-jump-host](/doc/ssh-jump-host/)).

## How to change my email forward?

Send an (inline!) signed OpenPGP email to `changes@db.torproject.org`
to change your email forward. A command like this, in a UNIX shell,
would do it:

    echo "emailForward: user@example.com" | gpg --armor --sign

Then copy-paste that in your email client, making sure to avoid
double-signing the email and sending in clear text (instead of HTML).

The email forward can also be changed in the [web interface][db.torproject.org].

# How-to

## Set a sudo password

See the [sudo password user configuration](doc/accounts#sudo).

## Know when will my change take effect?

Once a change is saved to LDAP, the actual change will take at least 5
minutes and at most 15 minutes to propagate to the relevant host. See
the [configuration file distribution
section](#configuration-file-distribution) for more details on why it
is so.

## Locking an account

See [the user retirement procedures](howto/retire-a-user).

## Connecting to LDAP

LDAP is not accessible to the outside world, so you need to get behind
the firewall. Most operations are done directly on the LDAP server, by
logging in as a regular user on `db.torproject.org` (currently
`alberti`).

Once that's resolved, you can use [ldapvi(1)](https://manpages.debian.org/ldapvi.1.en.html) or [ldapsearch(1)](https://manpages.debian.org/ldapsearch.1.en.html)
to inspect the database. User documentation on that process is in
[doc/accounts](doc/accounts) and <https://db.torproject.org>. See also the rest
of this documentation.

## Restoring from backups

There's no special backup procedures for the LDAP server: it's backed
up like everything else in the [howto/backup](howto/backup) system.

To restore the OpenLDAP database, you need to head over the Bacula
director, and enter the console:

    ssh -tt bacula-director-01 bconsole

Then call the `restore` command and select `6: Select backup for a
client before a specified time.` Then pick the server (currently
`alberti.torproject.org`) and a date. Then you need to "mark" the
right files:

    cd /var/lib/ldap
    mark *
    done

Then confirm the restore. The files will end up in
`/var/tmp/bacula-restores` on the LDAP server.

The next step depends on whether this is a partial or total
restore. 

### Partial restore

If you only need to access a specific field or user or part of the
database, you can use `slapcat` to dump the database from the restored
files even if the server is not running. You first need to "configure"
a "fake" server in the restore directory. You will need to create two
files under `/var/tmp/bacula-restores`:

 * `/var/tmp/bacula-restores/etc/ldap/slapd.conf`
 * `/var/tmp/bacula-restores/etc/ldap/userdir-ldap-slapd.conf`

They can be copied from `/etc`, with the following modifications:

    diff -ru /etc/ldap/slapd.conf etc/ldap/slapd.conf
    --- /etc/ldap/slapd.conf	2011-10-30 15:43:43.000000000 +0000
    +++ etc/ldap/slapd.conf	2019-11-25 19:48:57.106055596 +0000
    @@ -17,10 +17,10 @@
     
     # Where the pid file is put. The init.d script
     # will not stop the server if you change this.
    -pidfile         /var/run/slapd/slapd.pid
    +pidfile         /var/tmp/bacula-restores/var/run/slapd/slapd.pid
     
     # List of arguments that were passed to the server
    -argsfile        /var/run/slapd/slapd.args
    +argsfile        /var/tmp/bacula-restores/var/run/slapd/slapd.args
     
     # Read slapd.conf(5) for possible values
     loglevel        none
    @@ -57,4 +57,4 @@
     #backend		<other>
     
     # userdir-ldap
    -include /etc/ldap/userdir-ldap-slapd.conf
    +include /var/tmp/bacula-restores/etc/ldap/userdir-ldap-slapd.conf
    diff -ru /etc/ldap/userdir-ldap-slapd.conf etc/ldap/userdir-ldap-slapd.conf
    --- /etc/ldap/userdir-ldap-slapd.conf	2019-11-13 20:55:58.789411014 +0000
    +++ etc/ldap/userdir-ldap-slapd.conf	2019-11-25 19:49:45.154197081 +0000
    @@ -5,7 +5,7 @@
     suffix          "dc=torproject,dc=org"
     
     # Where the database file are physically stored
    -directory       "/var/lib/ldap"
    +directory       "/var/tmp/bacula-restores/var/lib/ldap"
     
     moduleload      accesslog
     overlay accesslog
    @@ -123,7 +123,7 @@
     
     
     database hdb
    -directory       "/var/lib/ldap-log"
    +directory       "/var/tmp/bacula-restores/var/lib/ldap-log"
     suffix cn=log
     #
     sizelimit 10000

Then `slapcat` is able to read those files directly:

    slapcat -f /var/tmp/bacula-restores/etc/ldap/slapd.conf -F /var/tmp/bacula-restores/etc/ldap

Copy-paste the stuff you need into `ldapvi`.

### Full rollback

Untested procedure.

If you need to roll back the *entire* server to this version, you
first need to stop the LDAP server:

    service slapd stop

Then move the files into place (in `/var/lib/ldap`):

    mv /var/lib/ldap{,.orig}
    cp -R /var/tmp/bacula-restores/var/lib/ldap /var/lib/ldap
    chown -R openldap:openldap /var/lib/ldap

And start the server again:

    service slapd start

## Listing members of a group

To tell which users are part of a given group (LDAP or otherwise), you
can use the [getent(1)](https://manpages.debian.org/getent.1) command. For example, to see which users
are part of the `tordnsel` group, you would call this command:

    $ getent group tordnsel
    tordnsel:x:1532:arlo,arma

In the above, `arlo` and `arma` are members of the `tordnsel` group.
The fields in the output are in the format of the [group(5)](https://manpages.debian.org/buster/manpages/group.5.en.html) file.

Note that the group membership will vary according to the machine on
which the command is run, as not all users are present everywhere.

## Adding/removing users in a group

Using this magical `ldapvi` command on the LDAP server
(`db.torproject.org`):

    ldapvi -ZZ --encoding=ASCII --ldap-conf -h db.torproject.org -D "uid=$USER,ou=users,dc=torproject,dc=org"

... you get thrown in a text editor showing you the entire dump of the
LDAP database. Be careful.

To add or remove a user to/from a group, first locate that user with
your editor search function (e.g. in `vi`, you'd type
<kbd>/uid=ahf</kbd> to look for the `ahf` user). You should see a
block that looks like this:

    351 uid=ahf,ou=users,dc=torproject,dc=org
    uid: ahf
    objectClass: top
    objectClass: inetOrgPerson
    objectClass: debianAccount
    objectClass: shadowAccount
    objectClass: debianDeveloper
    uidNumber: 2103
    gidNumber: 2103
    [...]
    supplementaryGid: torproject

To add or remove a group, simply add or remove a `supplementaryGid`
line. For example, in the above, we just added this line:

    supplementaryGid: tordnsel

to add `ahf` to the `tordnsel` group.

Save the file and exit the editor. `ldapvi` will prompt you to confirm
the changes, you can review with the <kbd>v</kbd> key or save with
<kbd>y</kbd>.

### Adding/removing an admin

The LDAP administrator group is a special group that is not defined
through the `supplementaryGid` field, but by adding users into the
group itself. With `ldapvi` (see above), you need to add a `member:`
line, for example:

```
2 cn=LDAP Administrator,ou=users,dc=torproject,dc=org
objectClass: top
objectClass: groupOfNames
cn: LDAP administrator
member: uid=anarcat,ou=users,dc=torproject,dc=org
```

To remove the user from the admin group, remove the line.

The group grants the user access to administer LDAP directly, for
example making any change through `ldapvi`.

## Searching LDAP

This will load a text editor with a dump of all the users (useful to
modify an existing user or add a new one):

    ldapvi -ZZ --encoding=ASCII --ldap-conf -h db.torproject.org -D "uid=$USER,ou=users,dc=torproject,dc=org"

This dump all known hosts in LDAP:

    ldapsearch -ZZ -Lx -h db.torproject.org -b "ou=hosts,dc=torproject,dc=org"

Note that this will only work on the LDAP host itself or on
whitelisted hosts which are few right now. Also note that this uses an
"anonymous" connection, which means that some (secret) fields might
not show up. For hosts, that's fine, but if you search for users, you
will need to use authentication. This, for example, will dump all
users with an SSH key:

    ldapsearch -ZZ -LxW -h db.torproject.org -D "uid=$USER,ou=users,dc=torproject,dc=org" -b "ou=users,dc=torproject,dc=org" '(sshRSAAuthKey=*)'

Note how we added a [search filter](https://ldap.com/ldap-filters/) (`(sshRSAAuthKey=*)`) here. We could
also have parsed the output in a script or bash, but this can actually
be much simpler. Also note that the previous searches dump the entire
objects. Sometimes it might be useful to only *list* the object
handles or certain fields. For example, this will list all hosts
`rebootPolicy` attribute:

    ldapsearch -h db.torproject.org -x -ZZ -b ou=hosts,dc=torproject,dc=org -LLL '(objectClass=*)' 'rebootPolicy'

This will list all servers with a manual reboot policy:

    ldapsearch -h db.torproject.org -x -ZZ -b ou=hosts,dc=torproject,dc=org -LLL '(rebootPolicy=manual)' ''

Note here the empty (`''`) attribute list.

To list hosts that do *not* have a reboot policy, you need a boolean modifier:

    ldapsearch -h db.torproject.org -x -ZZ -b ou=hosts,dc=torproject,dc=org -LLL '(!(rebootPolicy=manual))' ''

Such filters can be stacked to do complex searches. For example, this
filter lists all active accounts:

    ldapsearch -ZZ -vLxW -h db.torproject.org -D "uid=$USER,ou=users,dc=torproject,dc=org" -b "ou=users,dc=torproject,dc=org" '(&(!(|(objectclass=debianRoleAccount)(objectClass=debianGroup)(objectClass=simpleSecurityObject)(shadowExpire=1)))(objectClass=debianAccount))'

This lists users with access to Gitolite:

    ((allowedGroups=git-tor)|(exportOptions=GITOLITE))

... inactive users:

    (&(shadowExpire=1)(objectClass=debianAccount))

## Modifying the schema

If you need to add, change or remove a field in the *schema* of the
LDAP database, it is a different, and complex operation. You will only
need to do this if you launch a new service that (say) requires a new
password specifically for that service.

The schema is maintained in the [userdir-ldap.git](https://gitweb.torproject.org/admin/userdir-ldap.git/) repository. It
is stored in the `userdir-ldap.schema` file. Assuming the modified
object is a `user`, you would need to edit the file in three places:

 1. as a comment, in the beginning, to allocate a new field, for
    example:

        @@ -113,6 +113,7 @@
         #   .45 - rebootPolicy
         #   .46 - totpSeed
         #   .47 - sshfpHostname
        +#   .48 - mailPassword
         #
         # .3 - experimental LDAP objectClasses
         #   .1 - debianDeveloper

   This is purely informative, but it is important as it serves as a
   central allocation point for that numbering system. Also note that
   the entire schema lives under a branch of the [Debian.org IANA OID
   allocation](https://dsa.debian.org/iana/). If you reuse the OID
   space of Debian, it's important to submit the change to Debian
   sysadmins (`dsa@debian.org`) so they merge your change and avoid
   clashes.

 2. create the actual attribute, somewhere next to a similar attribute
    or after the previous OID, in this case we created an attributed
    called `mailPassword` right after `rtcPassword`, since other
    passwords were also grouped there:

        attributetype ( 1.3.6.1.4.1.9586.100.4.2.48
               NAME 'mailPassword'
               DESC 'mail password for SMTP'
               EQUALITY octetStringMatch
               SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 )

 3. finally, the new attribute needs to be added to the
    `objectclass`. in our example, the field was added alongside the
    other password fields in the `debianAccount` `objectclass`, which
    looked like this after the change:

        objectclass ( 1.3.6.1.4.1.9586.100.4.1.1
        	NAME 'debianAccount'
        	DESC 'Abstraction of an account with POSIX attributes and UTF8 support'
        	SUP top AUXILIARY
        	MUST ( cn $ uid $ uidNumber $ gidNumber )
        	MAY ( userPassword $ loginShell $ gecos $ homeDirectory $ description $ mailDisableMessage $ sudoPassword $ webPassword $ rtcPassword $ mailPassword $ totpSeed ) )

Once that schema file is propagated to the LDAP server, this should
automatically be loaded by `slapd` when it is restarted (see
below). But the ACL for that field should also be modified. In our
case, we had to add the `mailPassword` field to two ACLs:

    --- a/userdir-ldap-slapd.conf.in
    +++ b/userdir-ldap-slapd.conf.in
    @@ -54,7 +54,7 @@ access to attrs=privateSub
            by * break
     
     # allow users write access to an explicit subset of their fields
    -access to attrs=c,l,loginShell,ircNick,labeledURI,icqUIN,jabberJID,onVacation,birthDate,mailDisableMessage,gender,emailforward,mailCallout,mailGreylisting,mailRBL,mailRHSBL,mailWhitelist,mailContentInspectionAction,mailDefaultOptions,facsimileTelephoneNumber,telephoneNumber,postalAddress,postalCode,loginShell,onVacation,latitude,longitude,VoIP,userPassword,sudoPassword,webPassword,rtcPassword,bATVToken
    +access to attrs=c,l,loginShell,ircNick,labeledURI,icqUIN,jabberJID,onVacation,birthDate,mailDisableMessage,gender,emailforward,mailCallout,mailGreylisting,mailRBL,mailRHSBL,mailWhitelist,mailContentInspectionAction,mailDefaultOptions,facsimileTelephoneNumber,telephoneNumber,postalAddress,postalCode,loginShell,onVacation,latitude,longitude,VoIP,userPassword,sudoPassword,webPassword,rtcPassword,mailPassword,bATVToken
            by self write
            by * break
     
    @@ -64,7 +64,7 @@ access to attrs=c,l,loginShell,ircNick,labeledURI,icqUIN,jabberJID,onVacation,bi
     ##
     
     # allow authn/z by anyone
    -access to attrs=userPassword,sudoPassword,webPassword,rtcPassword,bATVToken
    +access to attrs=userPassword,sudoPassword,webPassword,rtcPassword,mailPassword,bATVToken
            by * compare
     
     # readable only by self

If those are the only required changes, it is acceptable to directly
make those changes directly on the LDAP server, as long as the *exact*
same changes are performed in the git repository.

It is preferable, however, to [build and
upload](howto/build_and_upload_debs) `userdir-ldap` as a Debian package instead.

## Deploying new userdir-ldap releases

Our userdir-ldap codebase is deployed through Debian packages built by
hand on TPA's members computers, from our [userdir-ldap
repository][]. Typically, when we make changes to that repository, we
should make sure we send the patches upstream, to the [DSA
userdir-ldap repository][]. The right way to do *that* is to send the
patch by email, to <mailto:dsa@debian.org>, since they do not have
merge requests enabled on that repository.

If you are lucky, we will have the latest version of the upstream code
and your patch will apply cleanly upstream. If unlucky, you'll
actually need to merge with upstream first. This process is generally
done through those steps:

 1. `git merge` the upstream changes, and resolve the conflicts
 2. update the changelog (make sure you have the upstream version with
    `~tpo1` as a suffix so that upgrades work when if we ever catch up
    with upstream)
 3. build the Debian package: `git buildpackage`
 4. deploy the Debian package

Note that unless the change is trivial, the Debian package should be
deployed *very* carefully. Because userdir-ldap is such a critical
piece of infrastructure, it can easily break stuff like PAM and
logins, so it is important to deploy it one machine at a time, and run
`ud-replicate` on the deployed machine (and `ud-generate` if the
machine is the LDAP server).

So "deploy the Debian package" should actually be done by copying, by
hand, the package to specific servers over SSH, and only after testing
there, [uploading it to the Debian archive](howto/build_and_upload_debs).

Note that it's probably a good idea to update the [userdir-ldap-cgi
repository][] alongside userdir-ldap. The above process should
similarly apply.

## Pager playbook

An LDAP server failure can trigger lots of emails as `ud-ldap` fails
to synchronize things. But the infrastructure should survive the
downtime, because users and passwords are *copied* over to all
hosts. In other words, authentication doesn't rely on the LDAP server
being up.

In general, OpenLDAP is very stable and doesn't generally crash, so we
haven't had many emergencies scenarios with it yet. If anything
happens, make sure the `slapd` service is running.

The `ud-ldap` software, on the other hand, is a little more
complicated and can be hard to diagnose. It has a large number of
moving parts (Python, Perl, Bash, Shell scripts) and talks over a
large number of protocols (email, DNS, HTTPS, SSH, finger). The
failure modes documented here are far from exhaustive and you should
expect exotic failures and error messages.

### LDAP server failure

That said, if the LDAP server goes down, password changes will not
work, and the server inventory (at <https://db.torproject.org/>) will
be gone. A mitigation is to use Puppet manifests and/or PuppetDB to
get a host list and server inventory, see the [Puppet
documentation](puppet) for details.

### Git server failure

The LDAP server will fail to regenerate (and therefore update) zone
files and zone records if the Git server is unavailable. This is
described in [issue 33766](https://gitlab.torproject.org/tpo/tpa/team/-/issues/33766). The fix is to recover the git server. A
workaround is to run this command on the primary DNS server (currently
`nevii`):

    sudo -u dnsadm /srv/dns.torproject.org/bin/update --force

### Deadlocks in ud-replicate

The `ud-replicate` process keeps a "reader" lock on the LDAP
server. If for some reason the network transport fails, that lock
might be held on forever. This happened in the past on hosts with
flaky network or ipsec problems that null-routed packets between ipsec
nodes.

There is a Nagios check that will detect stale
synchronisations. Example:

    Subject: ** PROBLEM Service Alert: palmeri/setup - ud-ldap freshness is WARNING **

Note that this can generate a *lot* of warnings because one per server
will be sent!

The fix is to find the offending locked process and kill it. In
desperation:

    pkill -u sshdist rsync

... but really, you should carefully review the rsync processes before
killing them all like that. And obviously, fixing the underlying
network issue would be important to avoid such problems in the future.

Also note that the lock file is in
`/var/cache/userdir-ldap/hosts/ud-generate.lock`, and `ud-generate`
tries to get a *write* lock on the file. This implies that a deadlock
will also affect file generation and keep `ud-generate` from
generating fresh config files.

Finally, `ud-replicate` also holds a lock on `/var/lib/misc` on the
*client* side, but that rarely causes problems.

### Troubleshooting changes@ failures

A common user question is that they are unable to change their SSH
key. This can happen if their email client somehow has trouble sending
a PGP signature correctly. Most often than not, this is because their
email client does a line wrap or somehow corrupts the OpenPGP
signature in the email.

A good place to start looking for such problems is the log files on
the LDAP server (currently `alberti`). For example, this has a trace
of all the emails received by the `changes@` alias:

    /srv/db.torproject.org/mail-logs/received.changes

A common problem is people using `--clearsign` instead of `--sign`
when sending an SSH key. When that happens, many email clients
(including Gmail) will word-wrap the SSH key after the comment,
breaking the signature. For example, this might happen:

    -----BEGIN PGP SIGNED MESSAGE-----
    Hash: SHA512

    ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDKxqYYEeus8dRXBHhLsp0SjH7ut2X8UM9hdXN=
    wJIl89otcJ5qKoXj90K9hq8eBjG2KuAZtp0taGQHqzBOFK+sFm9/gIqvzzQ07Pn0xtkmg10Hunq=
    vPKMj4gDFLIqTF0WSPA2E6L/TWaeVJ+IiGuE49j+0Ohd7UFDEquM1H/zno22vIEm/dxWLPWD9gG=
    MmwBghvfK/dRyzSEDGlAVeWLzoIvVOG12/ANgic3TlftbhiLKTs52hy8Qhq/aQBqd0McaE4JGxe=
    9k71OCg+0WHVS4q7HVdTUqT3VFFfz0kjDzYTYQQcHMqPHvYzZghxMVCmteNdJNwJmGSNPVaUeJG=
    MumJ9
    anarcat@curie

    -----BEGIN PGP SIGNATURE-----
    [...]
    -----END PGP SIGNATURE-----

Using `--sign --armor` will work around this problem, as the original
message will all be ASCII-armored.

### Dependency loop on new installs

Installing a new server requires granting the new server access
various machines, including [puppet](puppet) and the LDAP server
itself. This is granted ... by Puppet through LDAP!

So a server cannot register itself on the LDAP server and needs an
operator to first create a `host` snippet on the LDAP server, and then
run Puppet on the Puppet server. This is documented in the
[installation notes](new-machine).

## Disaster recovery

The LDAP server is mostly built by hand and should therefore be
restored from backups in case of a catastrophic failure. Care should
be taken to keep the SSH keys of the server intact.

The IP address (and name?) of the LDAP server should not be hard-coded
anywhere. When the server was last renumbered ([issue 33908](https://gitlab.torproject.org/tpo/tpa/team/-/issues/33908)), the
only changes necessary were on the server itself, in `/etc`. So in
theory, a fresh new server could be deployed (from backups) in a new
location (and new address) without having to do much.

# Reference

## Installation

All `ud-ldap` components are deployed through Debian packages,
compiled from the git repositories. It is assumed that some manual
configuration was performed on the main LDAP server to get it
bootstrapped, but that procedure was lost in the mists of time. 

Only backups keep us from total catastrophe in case of
lost. Therefore, this system probably cannot be reinstalled from
scratch.

## SLA

The LDAP server is designed to be fault-tolerant in the sense that
it's database is copied over other hosts. It should otherwise be
highly available as it's a key component in managing users
authentication and authorization, and machines.

## Design

The LDAP setup at Tor is based on the one from Debian.org. It has a
long, old and complex history, lost in the mists of time.

Configuration and database files like SSH keys, OpenPGP keyrings,
password, group databases, or email forward files are synchronised to
various hosts from the LDAP database. Most operations can be performed
on the [db.torproject.org](https://db.torproject.org/) site or by [email](https://db.torproject.org/doc-mail.html).

### Architecture overview

This is all implemented by a tool called `ud-ldap`, inherited from the
Debian project. The project is made of a collection of bash, Python
and Perl scripts which take care of synchronizing various
configuration files to hosts based on the LDAP configuration. Most of
this section aims at documenting how this program works.

`ud-ldap` is made of two Debian packages: `userdir-ldap`, which ships
the various server- and client-side scripts (and is therefore
installed everywhere), and `userdir-ldap-cgi` which ships the web
interface (and is therefore installed only on the LDAP server).

Configuration files are generated on the server by the `ud-generate`
command, which goes over the LDAP directory and crafts a tree of
configuration files, one directory per host defined in LDAP. Then each
host pulls those configuration files with `ud-replicate`. A common set
of files is exported everywhere, while the `exportOptions` field can
override that by disabling some exports or enabling special ones.

An email gateway processes OpenPGP-signed emails which can change a
user's fields, passwords or SSH keys, for example.

In general, `ud-ldap`:

 * creates UNIX users and groups on (some or all) machines
 * distributes password files for those users or other services
 * distributes user SSH public keys
 * distributes all SSH host public keys to all hosts
 * configures and reload arbitrary services, but particularly handles
   email, DNS, and git servers
 * provides host metadata to Puppet

This diagram covers those inter-dependencies at the time of writing.

![LDAP architecture diagram](ldap/graph.svg)

### Configuration file distribution

An important part of `ud-ldap` is the `ud-generate` command, which
generates configuration files for each host. Then the `ud-replicate`
command runs on each node to `rsync` those files. Both commands are
ran from cron on regular intervals. `ud-replicate` is configured by
the `userdir-ldap` package, at every 5 mintues. `ud-generate` is also
configured to run every 5 minutes, starting on the third minute of
every hour, in `/etc/cron.d/local-ud-generate` (so at minute 3, 8, 13,
..., 53, 58).

More specifically, this is what happens:

 1. on the LDAP server (currently `alberti`), `ud-generate` writes
    various files (detailed below) in one directory per host

 2. on all hosts, `ud-replicate` `rsync`'s that host's directory from
    the LDAP server (as the `sshdist` user)

`ud-generate` will write files only if the LDAP database or keyring
changed since last time, or at most every 24 hours, based on the
timestamp (`last_update.trace`). The `--force` option can be used to
bypass those checks.

### Files managed by ud-generate

This is a (hopefully) exhaustive list of files generated by
`ud-generate` as part of userdir-ldap 0.3.97 ("UNRELEASED"). This
might have changed since this was documented, on 2020-10-07.

All files are written in the `/var/cache/userdir-ldap/hosts/`, with
one subdirectory per host.

| Path                               | Function                                                                          | Fields used                                                                                                                         |
|------------------------------------|-----------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------|
| `all-accounts.json`                | JSON list of users                                                                | `uid`, `uidNumber`, `userPassword`, `shadowExpire`                                                                                  |
| `authorized_keys`                  | `authorized_keys` file for `ssh_dist`, if `AUTHKEYS` in `exportOptions`           | `ipHostNumber`, `sshRSAHostKey`, `purpose`, `sshdistAuthKeysHost`                                                                   |
| `bsmtp`                            | ?                                                                                 | ?                                                                                                                                   |
| `debian-private`                   | debian-private mailing list subscription                                          | `privateSub`, `userPassword` (skips inactive) , `supplementaryGid` (skips guests)                                                   |
| `debianhosts`                      | list of all IP addresses, unused                                                  | `hostname`, `ipHostNumber`                                                                                                          |
| `disabled-accounts`                | list of disabled accounts                                                         | `uid`, `userPassword` (*includes* inactive)                                                                                         |
| `dns-sshfp`                        | per-host DNS entries (e.g. debian.org), if `DNS` in `exportOptions`               | see below                                                                                                                           |
| `dns-zone`                         | user-managed DNS entries (e.g. debian.net), if `DNS` in `exportOptions`           | `dnsZoneEntry`                                                                                                                      |
| `forward.alias`                    | .forward compatibility, unused?                                                   | `uid`, `emailForward`                                                                                                               |
| `group.tdb`                        | `group` file template, with only the group that have access to that host          | `uid`, `gidNumber`, `supplementaryGid`                                                                                              |
| `last_update.trace`                | timestamps of last change to LDAP, keyring and last `ud-generate` run             | N/A                                                                                                                                 |
| `mail-callout`                     | ?                                                                                 | mailCallout                                                                                                                         |
| `mail-contentinspectionaction.cdb` | how to process this user's email (blackhole, markup, reject)                      | `mailContentInspectionAction`                                                                                                       |
| `mail-contentinspectionaction.db`  |                                                                                   |                                                                                                                                     |
| `mail-disable`                     | disabled email messages                                                           | `uid`, `mailDisableMessage`                                                                                                         |
| `mail-forward.cdb`                 | .forward "CDB" database, see [cdbmake(1)][]                                       | `uid`, `emailForward`                                                                                                               |
| `mail-forward.db`                  | .forward Oracle Berkeley DB "DBM" database                                        | `uid`, `emailForward`                                                                                                               |
| `mail-greylist`                    | greylist the account or not                                                       | mailGreylisting                                                                                                                     |
| `mail-rbl`                         | ?                                                                                 | mailRBL                                                                                                                             |
| `mail-rhsbl`                       | ?                                                                                 | mailRHSBL                                                                                                                           |
| `mail-whitelist`                   | ?                                                                                 | mailWhitelist                                                                                                                       |
| `markers`                          | xearth geolocation markers, unless `NOMARKERS` in `extraOptions`                  | `latitude`, `longitude`                                                                                                             |
| `passwd.tbd`                       | `passwd` file template, if `loginShell` is set and user has access                | `uid`, `uidNumber`, `gidNumber`, `gecos`, `loginShell`                                                                              |
| `rtc-passwords`                    | secondary password for RTC calls                                                  | `uid`, `rtcPassword`, `userPassword` (skips inactive), `supplementaryGid` (skips guests)                                            |
| `shadow.tdb`                       | `shadow` file template, same as `passwd.tdb`, if `NOPASSWD` not in `extraOptions` | `uid`, `uidNumber`, `userPassword`, `shadowExpire`, `shadowLastChange`, `shadowMin`, `shadowMax`, `shadowWarning`, `shadowInactive` |
| `ssh-gitolite`                     | `authorized_keys` file for `gitolite`, if `GITOLITE` in `exportOptions`           | `uid`, `sshRSAAuthKey`                                                                                                              |
| `ssh-keys-$HOST.tar.gz`            | SSH *user* keys, as a tar archive                                                 | `uid`, `allowed_hosts`                                                                                                              |
| `ssh_known_host`                   | SSH host keys                                                                     | `hostname`, `sshRSAHostKey`, `ipHostNumber`                                                                                         |
| `sudo-passwd`                      | `shadow` file for `sudo`                                                          | `uid`, `sudoPassword`                                                                                                               |
| `users.oath`                       | TOTP authentication                                                               | `uid`, `totpSeed`, `userPassword` (skips inactive) , `supplementaryGid` (skips guests)                                              |
| `web-passwords`                    | secondary password database for web apps (user:pass)                              | `uid`, `webPassword`                                                                                                                |

### How files get distributed by ud-replicate

The `ud-replicate` program runs on all hosts every 5 minutes and logs
in as the `sshdist` user on the LDAP server. It rsyncs the files from
the `/var/cache/userdir-ldap/hosts/$HOST/` directory on the LDAP server to
the `/var/lib/misc/$HOST` directory.

For example, for a host named `example.torproject.org`, `ud-generate`
will write the files in
`/var/cache/userdir-ldap/hosts/example.torproject.org/` and
`ud-replicate` will synchronize that directory, on
`example.torproject.org`, in the
`/var/lib/misc/example.torproject.org/` directory. The
`/var/lib/misc/thishost` symlink will also point to that directory.

Then ud-replicate those special things with some of those
files. Otherwise consumers of those files are expected to use them
directly in `/var/lib/misc/thishost/`, as is.

#### `makedb` template files

Files labeled with `template` are inputs for the [makedb(1)][]
command. They are like their regular "non-template" counterparts,
except they have a prefix that corresponds to:

 1. an incremental index, prefixed by zero (e.g. 01, 02, 03,
    ... 010...)
 2. the `uid` field (the username), prefixed by a dot (e.g. `.anarcat`)
 3. the `uidNumber` field (the UNIX UID), prefixed by an equal sign
    (e.g. `=1092`)

Those are the fields for the `passwd` file. The `shadow` file has only
prefixes 1 and 2. This file format is used to create the databases in
`/var/lib/misc/` which are fed into the NSS database with the
[libnss-db](https://tracker.debian.org/pkg/libnss-db) package. The database files get generated by
[makedb(1)][] from the templates above. It is what allows the `passwd`
file in `/etc/passwd` to remain untouched while still allowing ud-ldap
to manage extra users.

[makedb(1)]: https://manpages.debian.org/makedb.1

#### self-configuration: sshdist `authorized_keys`

The `authorized_keys` file gets shipped if `AUTHKEYS` is set in
`extraOptions`. This is typically set on the LDAP server (currently
`alberti`), so that all servers can login to the server (as the
`sshdist` user) and synchronise their configuration with
`ud-replicate`. 

This file gets dropped in `/var/lib/misc/authorized_keys` by
`ud-replicate`. A symlink in `/etc/ssh/userkeys/sshdist` ensures those
keys are active for the `sshdist` user.

#### other special files

More files are handled specially by `ud-replicate`:

 * `forward-alias` gets modified (`@emailappend` appended to each
   line) and replaces `/etc/postfix/debian`, which gets rehashed by
   `postmap`. this is done only if `/etc/postfix` and `forward-alias`
   exist
 * the `bsmtp` config file is deployed in `/etc/exim4`, if both exist
 * if `dns-sshfp` or `dns-zone` are changed, the DNS server zone files
   get regenerated and server reloaded (`sudo -u dnsadm
   /srv/dns.torproject.org/bin/update`, see "DNS zone file management"
   below)
 * `ssh_known_hosts` gets symlinked to `/etc/ssh`
 * the `ssh-keys.tar.gz` tar archive gets decompressed in
   `/var/lib/misc/userkeys`
 * the `web-passwords` file is given to `root:www-data` and made
   readable only by the group
 * the `rtc-passwords` file is installed in `/var/local/` as:
   * `rtc-passwords.freerad` if `/etc/freeradius` exists
   * `rtc-passwords.return` if `/etc/reTurn` exists
   * `rtc-passwords.prosody` if `/etc/prosody` exists
   .. and the appropriate service (`freeradius`,
   `resiprocate-turn-server`, `prosody`, respectively) get reloaded

### Authentication mechanisms

ud-ldap deals uses multiple mechanisms to authenticate users and
machines.

 1. the web interface binds to the LDAP directory anonymously, or as
    the logged in user, if any. an encrypted copy of the
    username/password pair is stored on disk, encrypted, and passed
    around in a URL token
 2. the email gateway runs as the `sshdist` user and binds to the LDAP
    directory using the `sshdist`-specific password. the `sshdist`
    user has full admin rights to the LDAP database through the slapd
    configuration. commands are authenticated using OpenPGP
    signatures, checked against the keyring, maintained outside of
    LDAP, manually, in the `account-keyring.git` repository, which
    needs to be pushed to the LDAP server by hand.
 3. `ud-generate` runs as the `sshdist` user and binds as that user
    to LDAP as well
 4. `ud-replicate` runs as root on all servers. it authenticates with
    the central LDAP server over SSH *using the SSH server **host**
    private key as a user key*, and logs in to the SSH server as the
    `sshdist` user. the `authorized_keys` file for that user on the
    LDAP server (`/etc/ssh/userkeys/sshdist`) determines which files
    the client has access to using a predefined `rsync` command which
    restricts to only `/var/cache/userdir-ldap/hosts/$HOST/`
 5. Puppet binds to the LDAP server over LDAPS using the custom CA,
    anonymously
 6. LDAP admins also have access to the LDAP server directly, provided
    they can get a shell (or a port forward) to access it

This is not related to ud-ldap authentication itself, but ud-ldap
obviously distributes authentication systems all over the place:

 * PAM and NSS usernames and passwords
 * SSH user authentication keys
 * SSH server public keys
 * `webPassword`, `rtcPassword` and so on
 * email forwards and email block list checks
 * DNS zone files (which may include things like SSH server public
   keys, for example)

### SSH access controls

A user gets granted access if it is part of a group that has been
granted access on the host with the `allowedGroups` field. An
additional group has access to *all* host, defined as
`allowedgroupspreload` (currently `adm`) in
`/etc/userdir-ldap/userdir-ldap.conf` on the LDAP server (currently
`alberti`).

Also note the `NOPASSWD` value for `exportOptions`: if set, it marks
the host as not allowing passwords so the `shadow` database is not
shipped which makes it impossible to login to the host with a
password. In practice this has no effect since password-based
authentication is disabled at the SSH server level, however.

### LDAP user fields

Those are the fields in the `user` LDAP object as of userdir-ldap
0.3.97 ("UNRELEASED"). This might have changed since this was
documented, on 2020-10-07. Some of those fields, but not all, can be
modified or deleted by the user through the email interface
(`ud-mailgate`).

| User field                    | Meaning                                                                                             |
| ----------                    | -------                                                                                             |
| `cn`                          | "common name" AKA "last name"                                                                       |
| `emailForward`                | address to forward email to                                                                         |
| `gecos`                       | GECOS metadata field                                                                                |
| `gidNumber`                   | Primary numeric group identifier, the UNIX GID                                                      |
| `homeDirectory`               | UNIX `$HOME` location, unused                                                                       |
| `ircNick`                     | IRC nickname, informative                                                                           |
| `keyFingerprint`              | OpenPGP fingerprint, grants access to email gateway                                                 |
| `labeledURI`                  | home page?                                                                                          |
| `loginShell`                  | UNIX login shell, grants user shell access, depending on gidNumber                                  |
| `mailCallout`                 | enables Sender Address Verification                                                                 |
| `mailContentInspectionAction` | how to process user's email detected as spam (reject, blackhole, markup)                            |
| `mailDefaultOptions`          | enables the "normal" set of SMTP checks, e.g. greylisting and RBLs                                  |
| `mailGreylisting`             | enables greylisting                                                                                 |
| `mailRBL`                     | set of RBLs to use                                                                                  |
| `mailRHSBL`                   | set of RHSBLs to use                                                                                |
| `mailWhitelist`               | sender envelopes to whitelist                                                                       |
| `mailDisableMessage`          | message to bounce messages with to disable an email account                                         |
| `rtcPassword`                 | previously used in XMPP authentication, unused                                                      |
| `samba*`                      | many samba fields, unused                                                                           |
| `shadowExpire`                | `1` if the account is expired                                                                       |
| `shadowInactive`              | ?                                                                                                   |
| `shadowLastChange`            | Last change date, in days since epoch                                                               |
| `shadowMax`                   | ?                                                                                                   |
| `shadowMin`                   | ?                                                                                                   |
| `shadowWarning`               | ?                                                                                                   |
| `sn`                          | "surname" AKA "first name"                                                                          |
| `sshRSAAuthKey`               | SSH public keys                                                                                     |
| `sudoPassword`                | `sudo` passwords on different hosts                                                                 |
| `supplementaryGid`            | Extra groups GIDs the user is a member of                                                           |
| `uidNumber`                   | Numeric user identifier, the UNIX UID, not to be confused with the above                            |
| `uid`                         | User identifier, the user's *name*                                                                  |
| `userPassword`                | LDAP password field, stripped of the `{CRYPT}` prefix to be turned into a UNIX password if relevant |

[cdbmake(1)]: https://manpages.debian.org/cdbmake.1

#### sudoPassword field format

The `sudoPassword` field is special. It has 4 fields separated by
spaces:

 1. a UUID
 2. the status, which is either the string `unconfirmed` or the string
    `confirmed:` followed by a SHA1 (!) HMAC of the string
    `password-is-confirmed`, `sudo`, the UID, the UUID, the host list,
    and the hashed password, joined by colons (`:`), primed with a
    secret key stored in `/etc/userdir-ldap/key-hmac-$UID` where UID
    is the numeric identifier of the calling user, generally `33`
    (probably the web server?) or `sshdist`? The secret key can also
    overridden by the `UD_HMAC_KEY` environment variable
 3. the host list, either `*` (meaning all hosts) or a comma (`,`)
    separated list of hosts this password applies to
 4. the hashed password, which is restricted to 50 characters: if
    longer, it is invalid (`*`)

That password field gets validated by email through `ud-mailgate`.

The field can, of course, have multiple values.

#### sshRSAAuthKey field format

The `sshRSAAuthKey` field can have multiple values. Each one should be
a valid [authorized_keys(5)][] file.

Its presence influences whether a user is allowed to login to a host
or not. That is, if it is missing, the user will not be added to the
`shadow` database.

The `GITOLITE` hosts treat the field specially: it looks for
`allowed_hosts` fields and will match only on the right host. If will
skip keys that have other options.

[authorized_keys(5)]: https://manpages.debian.org/authorized_keys.5

### LDAP host fields

Those are the fields in the `user` LDAP object as of userdir-ldap
0.3.97 ("UNRELEASED"). This might have changed since this was
documented, on 2020-10-07. Those fields are usually edited by hand by
an LDAP admin using `ldapvi`.

| Group field     | Meaning                                                      |
|-----------------|--------------------------------------------------------------|
| `description`   | free-form text field  description                            |
| `memory`        | main memory size, with `M` suffix (unused?)                  |
| `disk`          | main disk size, with `G` suffixed (unused?)                  |
| `purpose`       | like description but purpose of the host                     |
| `architecture`  | CPU architecture (e.g. `amd64`)                              |
| `access`        | always "restricted"?                                         |
| `physicalHost`  | parent metal or hoster                                       |
| `admin`         | always "torproject-admin@torproject.org"                     |
| `distribution`  | always "Debian"                                              |
| `l`             | location ("City, State, Country"), unused                    |
| `ipHostNumber`  | IPv4 or IPv6 address, multiple values                        |
| `sshRSAHostKey` | SSH server public key, multiple values                       |
| `rebootPolicy`  | how to reboot this server: `manual`, `justdoit`, `rotation`) |

#### `rebootPolicy` field values

The `rebootPolicy` is documented in the [upgrade
procedures](upgrades).

#### `purpose` field values

The `purpose` field is special in that it supports a crude markup
language which can be used to create links in the web interface, but
is also used to generate SSH `known_hosts` files. To quote the
ud-generate source code:

> In the purpose field, `[[host|some other text]]` (where some other
> text is optional) makes a hyperlink on the web \[interface\]. We now
> also add these hosts to the ssh `known_hosts` file.  But so that we
> don't have to add everything we link, we can add an asterisk and say
> `[[*...` to ignore it.  In order to be able to add stuff to ssh
> without http linking it we also support `[[-hostname]]` entries.

Otherwise the `description` and `purpose` fields are fairly similar
and often contain the same value.

Note that there can be multiple `purpose` values, in case we need
multiple names like that. For example, the prometheus/grafana server
has:

```
purpose: [[-prometheus1.torproject.org]]
purpose: [[prometheus.torproject.org]]
purpose: [[grafana.torproject.org]]
```

because:

 * `prometheus1.torproject.org`: is an SSH alias but not a web one
 * `prometheus.torproject.org`: because the host also runs Prometheus
   as a web interface
 * `grafana.torproject.org`: and that is the Grafana web interface

#### `exportOptions` field values

The `exportOptions` field warrants a more detailed explanation. Its
value determines which files are created by `ud-generate` for a given
host. It can either enable or inhibit the creation of certain files.

 * `AUTHKEYS`: ship the `authorized_keys` file for `sshdist`,
   typically on the LDAP server for `ud-replicate` to connect to it
 * `BSMTP`: ship the `bsmtp` file
 * `DNS`: ships DNS zone files (`dns-sshfp` and `dns-zone`)
 * `GITOLITE`: ship the gitolite-specific SSH `authorized_keys`
   file. can also be suffixed, e.g. `GITOLITE=OPTIONS` where `OPTIONS`
   does magic stuff like skip some hosts (?) or change the SSH command
   restriction
 * `KEYRING`: ship the `sync_keyrings` GnuPG keyring file (`.gpg`)
   defined in `userdir-ldap.conf`, generated from the
   `admin/account-keyring.git` repository (technically: the
   `ssh://alberti.torproject.org/srv/db.torproject.org/keyrings/keyring.git`
   repository...) 
 * `NOMARKERS`: inhibits the creation of the `markers` file
 * `NOPASSWD`: if present, the `passwd` database has `*` in the
   password field, `x` otherwise. also inhibits the creation of the
   `shadow` file. also marks a host as `UNTRUSTED` (below)
 * `PRIVATE`: ship the `debian-private` mailing list registration file
 * `RTC-PASSWORDS`: ship the `rtc-passwords` file
 * `TOTP`: ship the `users.oath` file
 * `UNTRUSTED`: skip sudo passwords for this host unless explicitly
   set
 * `WEB-PASSWORDS`: ship the `web-passwords` file

Of those parameters, only `AUTHKEYS`, `DNS` and `GITOLITE` are used at
TPO, for, respectively, the LDAP server, DNS servers, and the git
server.

### Email gateway

The email gateway runs on the LDAP server. There are four aliases,
defined in `/etc/aliases`, which forward to the `sshdist` user with an
extension:

    change:           sshdist+changes
    changes:          sshdist+changes
    chpasswd:         sshdist+chpass
    ping:             sshdist+ping

Then three `.forward` files in the `~sshdist` home directory redirect
this to the `ud-mailgate` Python program while also appending a copy
of the email into `/srv/db.torproject.org/mail-logs/`, for example:

    # cat ~sshdist/.forward+changes
    "| /usr/bin/ud-mailgate change"
    /srv/db.torproject.org/mail-logs/received.changes

This is how `ud-mailgate` processes incoming messages:

 1. it parses the email from stdin using Python's `email.parser`
    library

 2. it tries to find an OpenPGP-signed message and passes it to the
    `GPGCheckSig` function to verify the signature against the trusted
    keyring

 3. it does a check against replay attacks by checking:
 
    * if the OpenPGP signature timestamp is reasonable (less than 3
      days in the future, or 4 days in the past)

    * if the signature has already been received in the last 7 days

    The `ReplayCache` is a [dbm](https://docs.python.org/3/library/dbm.html) database stored in
    `/var/cache/userdir-ldap/mail/replay`.

 4. it then behaves differently whether it was called with `ping`,
    `chpass` or `change` as its argument

 5. in any case it tries to send a reply to the user by email,
    encrypted in the case of `chpass`

The `ping` routine just responds to the user with their LDAP entry,
rendered according to the `ping-reply` template (in
`/etc/userdir-ldap/templates`).

The `chpass` routine behaves differently depending on a magic string
in the signed message, which can either be:

 1. "Please change my Debian password"
 2. "Please change my Tor password"
 3. "Please change my Kerberos password"
 4. "Please change my TOTP seed"

The first two do the same thing. The latter two are not in use at
TPO. The main `chpass` routine basically does this:

 1. generate a 15-character random string
 2. "hash" it with Python's [crypt](https://docs.python.org/3/library/crypt.html) with a MD5 (!) salt
 4. set the hashed password in the user's LDAP object, `userPassword`
    field
 5. bump the `shadowLastChange` field in the user's LDAP object
 6. render the `passwd-changed` email template which will include an
    OpenPGP encrypted copy of the cleartext email

The `change` routine does one or many of the following, depending on
the lines in the signed message:

 * on `show`: send a `key: value` list of parameters of the user's
   LDAP object, OpenPGP-encrypted
 * change the user's "position marker" (latitude/longitude) with a
   format like `Lat: -10.0 Long: +10.0`
 * add or replace a `dnsZoneEntry` if the line looks like `host IN
   {A,AAAA,CNAME,MX,TXT}`
 * replace LDAP user object fields if the line looks like `field:
   value`. only some fields are supported
 * add or replace `sshRSAAuthKey` lines when the line looks like an
   SSH key (note that this routine sends its error email
   separately). this gets massaged so that it matches the format
   expected by `ud-generate` in LDAP and is validated by piping in
   `ssh-keygen -l -f`. the `allowed_hosts` block is checked against
   the existing list of servers and it enforces a minimum RSA key size
   (2048 bits)
 * delete an LDAP user field, when provided with a line that looks
   like `del FIELD`
 * add or replace `mailrbl`, `mailrhsbl` and `mailwhiltelist` fields,
   except allow a space separator instead of the normal colon
   separator for arbitrary fields (??)
 * if the sudo password is changed, it checks if the HMAC provided
   matches the expected one from the database and switched from
   `unconfirmed` to `confirmed`

Note that the `change` routine only operates if the account is not
locked (if the `userPassword` does not contain the string `*LK*` or
starts with the `!` string).

### Web interface

The web interface is shipped as part of the userdir-ldap-cgi Debian
package, built from the [userdir-ldap-cgi repository][]. The web
interface is written in Perl, using the builtin CGI module and [WML][]
templates. It handles password and settings changes for users,
although some settings (like sudo passwords) require an extra
confirmation by OpenPGP-signed message through the email gateway. It
also lists machines known by LDAP.

[userdir-ldap-cgi repository]: https://gitweb.torproject.org/admin/userdir-ldap-cgi.git/

The web interface also ships documentation in the form of HTML pages
rendered through [WML][] templates.

The web interface binds to the LDAP database as the logged in user (or
anonymously, for some listings and searches) and therefore doesn't
enjoy any special privilege in itself.

Each "dynamic" page is a standalone CGI script, although it uses some
common code from `Util.pm` to load settings, format some strings, deal
with authentication tokens and passwords.

The main page is the `search.cgi` interface, which allows users to
perform a search in the user database, based on a subset of LDAP
fields. This script uses the `searchform.wml` template.

The login form (`login.cgi`) binds with the LDAP database using the
provided user/password. A "hack" is present to "upgrade" the user's
passwords to MD5, presumably it was in cleartext
before. Authentication persistence is done through an authentication
token (`authtoken` in the URL), which consists of a MD5 "*encoded
username and a key to decrypt the password stored on disk, the
authtoken is protected from modification by an HMAC*". In practice, it
seems the user's password is stored on disk, encrypted with a Blowfish
cipher in CBC mode (from `Crypt::CBC`), with a 10 bytes (80 bits) key,
while the HMAC is based on SHA1 (from `Digest::HMAC_SHA1`). The tokens
are stored in `/var/cache/userdir-ldap/web-cookies/` with one file per
user, named after a salted MD5 hash of the username. Tokens expire
after 10 minutes by the web interface, but it doesn't seem like old
tokens get removed unless the user is active on the site.

Although the user/password pair is not stored directly in the user's
browser cookies or history, the authentication token effectively acts
as a valid user/password to make changes to the LDAP user database. It
could be abused to authenticate as an LDAP user and change their
password, for example.

The login form uses the `login.wml` template.

The `logout.cgi` interface, fortunately, allows users to clear this
on-disk data, invalidating possibly leaked tokens.

The `update.cgi` interface is what processes actual changes requested
by users. It will extract the actual LDAP user and password from the
on-disk encrypted token and bind with that username and password. It
does some processing of the form to massage it into a proper LDAP
update, running some password quality checks using a wrapper around
[cracklib](https://github.com/cracklib/cracklib) called `password-qualify-check` which, essentially,
looks at a word list, the GECOS fields and the old password. Partial
updates are possible: if (say) the `rtcPassword` fields don't match
but the `userPassword` fields do, the latter will be performed because
it is done first. It is here that unconfirmed `sudo` passwords are set
as well. It's the user's responsibility to send the challenge response
by signed OpenPGP email afterwards. This script uses the `update.wml`
template.

The `machines.cgi` script will list servers registered in the LDAP in
a table. It binds to the LDAP server anonymously and searches for all
hosts. It uses the `hostinfo.wml` template.

Finally the `fetchkey.cgi` script will load a public key from the
`keyrings` configuration setting based on the provided fingerprint and
dump it in plain text.

### Interactions with Puppet

The [Puppet server](puppet) is closely coupled with LDAP, from which
it gathers information about servers.

It specifically uses those fields:

| LDAP field     | Puppet use                                                                                               |
|----------------|----------------------------------------------------------------------------------------------------------|
| `hostname`     | matches with the Puppet node host name, used to load records                                             |
| `ipHostNumber` | Ferm firewall, Bind, Bacula, Jenkins, PostgreSQL backups, static sync access control, backends discovery |
| `purpose`      | motd                                                                                                     |
| `physicalHost` | motd: shows parent in VM, VM children in host                                                            |

The `ipHostnumber` field is also used to lookup the host in the
`hoster.yaml` database in order to figure out which hosting provider
hosts the parent metal. This is, in turn, used in Hiera to change
certain parameters, like Debian mirrors.

Note that the above fields are explicitly imported in the
`allnodeinfo` data structure, along with `sshRSAHostKey` and
`mXRecord`, but those are not used. Furthermore, the `nodeinfo`
data structure imports all of the host's data, so there might be other
fields in use that I haven't found.

Puppet connects to the LDAP server directly over LDAPS (port 636) and
therefore requires the custom LDAP host CA, although it binds to the
server anonymously.

### DNS zone file management

One of the configuration files `ud-generate` generates are,
critically, the `dns-sshfp` and `dns-zone` files.

The `dns-sshfp` file holds the following records mapped to LDAP
`host` fields:

| DNS record  | LDAP host field              | Notes                                                 |
| ----------  | ---------------              | -----                                                 |
| `SSHFP`     | `sshRSAHostKey`              | extra entries possible with the `sshfphostname` field |
| `A`, `AAAA` | `ipHostNumber`               | TTL overridable with the `dnsTTL` field               |
| `HINFO`     | `architecture` and `machine` |                                                       |
| `MX`        | `mXRecord`                   |                                                       |

The `dns-zone` file contains *user*-specific DNS entries. If a `user`
object has a `dnsZoneEntry` field, that entry is written to the file
directly. A `TXT` record with the user's email address and their PGP
key fingerprint is also added for identification. That file is not in
use in TPO at the moment, but is (probably?) the mechanism behind the
user-editable `debian.net` zone.

Those files only get *distributed* to DNS servers (e.g. `nevii` and
`falax`), which are marked with the `DNS` flag in the `exportOptions`
field in LDAP.

Here is how zones are propagated from LDAP to the DNS server:

 1. `ud-replicate` will pull the files with `rsync`, as explained in
    the previous section

 2. if the `dns-zone` or `dns-sshfp` files change, `ud-replicate` will
    call `/srv/dns.torproject.org/bin/update` (from `dns_helpers.git`)
    as the `dnsadm` user, which creates the final zonefile in
    `/srv/dns.torproject.org/var/generated/torproject.org`

The `bin/update` script does the following:

 1. pulls the `auto-dns.git` and `domains.git` git repositories

 2. updates the DNSSEC keys (with `bin/update-keys`)

 3. update the GeoIP distribution mechanism (with `bin/update-geo`)

 4. builds the service includes from the `auto-dns` directory (with
    `auto-dns/build-services`), which writes the
    `/srv/dns.torproject.org/var/services-auto/all` file

 5. for each domain in `domains.git`, calls `write_zonefile` (from
    `dns_helpers.git`), which in turn:

    1. increments the serial number in the `.serial` state file
    2. generate a zone header with the new serial number
    3. include the zone from `domains.git`
    4. compile it with [named-compilezone(8)][], which is the part
       that expands the various `$INCLUDE` directives

 6. then calls `dns-update` (from `dns_helpers.git`) which rewrites
    the `named.conf` snippet and reloads bind, if needed

[named-compilezone(8)]: https://manpages.debian.org/named-compilezone.8

The various `$INCLUDE` directives in the `torproject.org` zonefile are
currently:

 * `/var/lib/misc/thishost/dns-sshfp` - generated on the LDAP server
   by `ud-generate`, contains SSHFP records for each host
 * `/srv/dns.torproject.org/puppet-extra/include-torproject.org`:
   generated by Puppet modules which call the `dnsextras` module. This
   is used, among other things, for TLSA records for HTTPS and SMTP
   services
 * `/srv/dns.torproject.org/var/services-auto/all`: generated by the
   `build-services` script in the `auto-dns.git` directory
 * `/srv/letsencrypt.torproject.org/var/hook/snippet`: generated by
   the `bin/le-hook` in the `letsencrypt-domains.git` repository, to
   authenticate against Let's Encrypt and generate [TLS](tls)
   certificates.

Note that this procedure fails when the git server is unavailable, see
[issue 33766](https://gitlab.torproject.org/tpo/tpa/team/-/issues/33766) for details.

### Source file analysis

Those are the various scripts shipped by userdir-ldap. This table
describes which programming language it's written in and a short
description of its purpose. The `ud?` column documents whether the
command was considered for implementation in the [ud](https://github.com/Debian/ud) rewrite, and
gives us a hint on whether it is important or not.

| tool                      | lang   | ud? | description                                                     |
| ------------------------- | ------ | --- | --------------------------------------------------------------- |
| `ud-arbimport`            | Python |     | import arbitrary entries into LDAP                              |
| `ud-config`               | Python |     | prints config from `userdir-ldap.conf`, used by `ud-replicate`  |
| `ud-echelon`              | Python | x   | "Watches for email activity from Debian Developers"             |
| `ud-fingerserv`           | Perl   | x   | [finger(1)][] server to expose some (public) user information   |
| `ud-fingerserv2.c`        | C      |     | same in C?                                                      |
| `ud-forwardlist`          | Python |     | convert `.forward` files into LDAP configuration                |
| `ud-generate`             | Python | x   | critical code path, generates all configuration files           |
| `ud-gpgimport`            | Python |     | seems unused? "Key Ring Synchronization utility"                |
| `ud-gpgsigfetch`          | Python |     | refresh signatures from a keyring? unused?                      |
| `ud-groupadd`             | Python | x   | tries to create a group, possibly broken, not implemented by ud |
| `ud-guest-extend`         | Python |     | "Query/Extend a guest account"                                  |
| `ud-guest-upgrade`        | Python |     | "Upgrade a guest account"                                       |
| `ud-homecheck`            | Python |     | audits home directory permissions?                              |
| `ud-host`                 | Python |     | interactively edits host entries                                |
| `ud-info`                 | Python |     | same with user entries                                          |
| `ud-krb-reset`            | Perl   |     | kerberos password reset, unused?                                |
| `ud-ldapshow`             | Python |     | stats and audit on the LDAP database                            |
| `ud-lock`                 | Python | x   | locks many accounts                                             |
| `ud-mailgate`             | Python | x   | email operations                                                |
| `ud-passchk`              | Python |     | audit a password file                                           |
| `ud-replicate`            | Bash   | x   | rsync file distribution from LDAP host                          |
| `ud-replicated`           | Python |     | rabbitmq-based trigger for ud-replicate, unused?                |
| `ud-roleadd`              | Python | x   | like ud-groupadd, but for roles, possibly broken too            |
| `ud-sshlist`              | Python |     | like ud-forwardlist, but for ssh keys                           |
| `ud-sync-accounts-to-afs` | Python |     | sync to AFS, unused                                             |
| `ud-useradd`              | Python | x   | create a user in LDAP, possibly broken?                         |
| `ud-userimport`           | Python |     | imports passwd and group files                                  |
| `ud-xearth`               | Python |     | generates xearth DB from LDAP entries                           |
| `ud-zoneupdate`           | Shell  | x   | increments serial on a zonefile and reload bind                 |

Note how the `ud-guest-upgrade` command works. It generates an LDAP
snippet like:

    delete: allowedHost
    -
    delete: shadowExpire
    -
    replace: supplementaryGid
    supplementaryGid: $GIDs
    -
    replace: privateSub
    privateSub: $UID@debian.org

where the `guest` gid is replaced by the "default" `defaultgroup`
set in the `userdir-ldap.conf` file.

[finger(1)]: https://manpages.debian.org/finger.1

Those are other files in the source distribution which are not
directly visible to users but are used as libraries by other files.

| libraries               | lang   | description                                  |
| ----------------------- | ------ | -------------------------------------------- |
| `UDLdap.py`             | Python | mainly an Account representation             |
| `userdir_exceptions.py` | Python | exceptions                                   |
| `userdir_gpg.py`        | Python | yet another GnuPG Python wrapper             |
| `userdir_ldap.py`       | Python | various functions to talk with LDAP and more |

Those are the configuration files shipped with the package:

| configuration files          | lang   | description                                                             |
| ---------------------------- | ------ | ----------------------------------------------------------------------- |
| `userdir-ldap.conf`          | Python | LDAP host, admin user, email, logging, keyrings, web, DNS, MX, and more |
| `userdir_ldap.pth`           | ???    | no idea!                                                                |
| `userdir-ldap.schema`        | LDAP   | TPO/Debian-specific LDAP schema additions                               |
| `userdir-ldap-slapd.conf.in` | slapd  | slapd configuration, includes LDAP access control                       |

## Issues

There is no issue tracker specifically for this project, [file][] or
[search][] for issues in the [team issue tracker][search], with the `LDAP`
label.

 [file]: https://gitlab.torproject.org/tpo/tpa/team/-/issues/new
 [search]: https://gitlab.torproject.org/tpo/tpa/team/-/issues?label_name%5B%5D=LDAP

## Monitoring and testing

Nagios checks the `/var/lib/misc/thishost/last_update.trace` timestamp
and warns if a host is more than an hour out of date.

The LDAP server is monitored in the sense that Nagios checks that the
process is running.

The web and mail servers are checked as per normal policy.

## Logs and metrics

The LDAP directory holds a list of usernames, email addresses, real
names, and possibly even physical locations. This information gets
destroyed when a user is completely removed but can be kept
indefinitely for locked out users.

`ud-ldap` keeps a full copy of all emails sent to
`changes@db.torproject.org`, `ping@torproject.org` and
`chpass@torproject.org` in `/srv/db.torproject.org/mail-logs/`. This
includes personally identifiable information (PII) like `Received-by`
headers (which may include user's IP addresses), user's email
addresses, SSH public keys, hashed sudo passwords, and junk mail. The
mail server should otherwise follow normal mail server logging
policies.

The web interface keeps authentication tokens in
`/var/cache/userdir-ldap/web-cookies`, which store encrypted username
and password information. Those get removed when a user logs out or
after 10 minutes of inactivity, when the user returns. It's unclear
what happens when a user forgets to logout and fails to return to the
site. Web server logs should otherwise follow the normal TPO policy,
see [the static mirror network](static-component#logs-and-metrics) for more information on that.

The OpenLDAP server itself (`slapd`) keeps no logs.

There are no performance metrics recorded for this service.

## Backups

There's no special backup procedures for the LDAP server, it is
assumed that the on-disk `slapd` database can be backed up reliably by
Bacula.

## Other documentation

 * our (TPA) [userdir-ldap repository][]
 * our (TPA) [userdir-ldap-cgi repository][]
 * the [DSA wiki](https://dsa.debian.org/) has some ud-ldap documentation, see in
   particular:
   * [how to get sudo passwords](https://dsa.debian.org/user/sudo/)
   * [how to add a guest account](https://dsa.debian.org/howto/add-guest/)
   * [how to add an account](https://dsa.debian.org/howto/add-account/)
   * [how to lock an account](https://dsa.debian.org/howto/lock-account/)
 * upstream (DSA) [userdir-ldap source code](https://salsa.debian.org/dsa-team/mirror/userdir-ldap)
 * upstream (DSA) [userdir-ldap-cgi source code](https://salsa.debian.org/dsa-team/mirror/userdir-ldap-cgi)
 * [ud](https://github.com/Debian/ud) - a partial ud-ldap rewrite in Django from 2013-2014, no
   change since 2017, the [announcement for the rewrite](https://wiki.debian.org/Teams/DSA/UserdirLdapRewrite)
 * [userdir-ldap-pylons](https://salsa.debian.org/dsa-team/mirror/userdir-ldap-pylons) - a partial ud-ldap rewrite in Pylons from
   2011, abandoned
 * [LDAP.com](https://ldap.com/) has [extensive documentation](https://ldap.com/learn-about-ldap/), for example on
   [LDAP filters](https://ldap.com/ldap-filters/)

# Discussion

## Overview

`ud-ldap` is decades old (the `ud-generate` manpage mentions 1999, but
it could be older) and is hard to debug and extend. This section aims
at documenting issues with the software and possible alternatives.

Our [userdir-ldap repository][] is a fork of the [DSA userdir-ldap
repository][]. The codebase is therefore shared with the Debian
project, which uses it more heavily than TPO. According to [GitLab's
analysis](https://salsa.debian.org/dsa-team/mirror/userdir-ldap/-/graphs/master), weasel has contributed the most to the repository (since
2007), followed closely by Joey Schulze, which wrote most of the code
before that, between 1999 and 2007.

[DSA userdir-ldap repository]: https://salsa.debian.org/dsa-team/mirror/userdir-ldap
[userdir-ldap repository]: https://gitweb.torproject.org/admin/userdir-ldap.git/

The service is mostly in maintenance mode, both at DSA and in TPO,
with small, incremental changes being made to the codebase over all
those years. Attempts have been made to rewrite it with a Django
frontend ([ud](https://github.com/Debian/ud), 2013-2014 no change since 2017) or Pylons
([userdir-ldap-pylons](https://salsa.debian.org/dsa-team/mirror/userdir-ldap-pylons), 2011, abandoned), all have been abandoned.

### Major issues with userdir-ldap

ud-ldap is old, hard to maintain, and possibly has serious security
issues. it is a liability, in the long term, in particular for those
reasons:

 * **old cryptographic primitives**: SHA-1 is used to hash `sudo`
   passwords, MD5 is used to hash user passwords, those hashes are
   communicated over OpenPGP_encrypted email but stored in LDAP in
   clear-text. There is a "hack" present in the web interface to
   enforce MD5 passwords on logins, and the mail interface also has
   MD5 hard-coded for password resets. Blowfish and HMAC-SHA-1 are also
   used to store and authenticate (respectively) LDAP passwords in the
   web interface. MD5 is used to hash usernames.

 * **rolls its own crypto**: `ud-ldap` ships its own wrapper around GnuPG,
   implementing the (somewhat arcane) command-line dialect. it has not
   been determined if that implementation is either accurate or safe.

 * **email interface hard to use**: it has trouble with standard
   OpenPGP/MIME messages and is hard to use for users

 * **old web interface**: it's made of old Perl CGI scripts that uses
   a custom template format built on top of [WML][] with custom
   pattern replacement, without any other framework than Perl's
   builtin `CGI` module. it uses in-URL tokens which could be
   vulnerable to XSS attacks.

[WML]: https://en.wikipedia.org/wiki/Website_Meta_Language

 * **large technical debt**
 
   * ud-ldap is written in (old) Python 2, Perl and shell. it will at
     least need to be ported to Python 3 in the short term.
   * code reuse is minimal across the project.
   * ud-ldap    has no test suite, linting or CI of any form. 
   * opening some files (e.g. `ud-generate`) yield so many style
     warnings that my editor (Emacs with Elpy) disables checks.
   * it is believed to be impossible or at least impractical to setup
     a new ud-ldap setup from scratch.

 * **authentication is overly complex**: as detailed in the
   [authentication section](#authentication-mechanisms), with 6 different authentication
   methods with the LDAP server.

 * **replicates configuration management**: ud-ldap does configuration
   management and file distribution, as root
   (`ud-generate`/`ud-replicate`), something which should be reserved
   to Puppet. this might have been justified when ud-ldap was written,
   in 1999, since configuration management wasn't very popular back
   then ([Puppet](https://en.wikipedia.org/wiki/Puppet_(software)) was created in 2005, only [cfengine](https://en.wikipedia.org/wiki/CFEngine) existed
   back then, which was created in 1993)

 * **difficult to customize**: Tor-specific customizations are made as
   patches to the git repository and require a package rebuild. they
   are therefore difficult to merge back upstream and require us to
   run our own fork.

Our version of ud-ldap has therefore diverged from upstream. The
changes are not extensive, but they are still present and require a
merge every time we want to upgrade the package. At the time of
writing, it is:

    anarcat@curie:userdir-ldap(master)$ git diff --stat f1e89a3
     debian/changelog           | 18 ++++++++++++++++++
     debian/rules               |  2 +-
     debian/ud-replicate.cron.d |  2 +-
     templates/welcome-message  | 41 ++++++++++++++++++++++++++++-------------
     ud-generate                |  3 ---
     ud-mailgate                |  2 ++
     ud-replicate               |  2 +-
     userdir-ldap-slapd.conf.in |  4 ++--
     userdir-ldap.conf          |  2 +-
     userdir-ldap.schema        |  9 ++++++++-
     10 files changed, 62 insertions(+), 23 deletions(-)

It seems that upstream doesn't necessarily run released code, and we
certainly don't: the above merge point had 47 commits on top of the
previous release (0.3.96). The current release, as of October 2020, is
0.3.97, and upstream already has 14 commits on top of it.

The web interface is in a similar conundrum, except worse:

    22 files changed, 192 insertions(+), 648 deletions(-)

At least the changes there are only on the HTML templates. The merge
task is tracked in [issue 40062](https://gitlab.torproject.org/tpo/tpa/team/-/issues/40062).

## Goals

The goal of the current discussion would be to find a way to fix the
problems outlined above, either by rewriting or improving ud-ldap,
replacing parts of it, or replacing ud-ldap completely with something
else, possibly removing LDAP as a database altogether.

### Must have

 * framework in use must be supported for the foreseeable future
   (e.g. not Python 2)
 * unit tests or at least upstream support must be active
 * system must be simpler to understand and diagnose
 * single source of truth: overlap with Puppet must be
   resolved. either Puppet uses LDAP as a source of truth (e.g. for
   hosts and users) or LDAP goes away. compromises are possible:
   Puppet could be the source of truth for hosts, and LDAP for users.

### Nice to have

 * use one language across the board (e.g. Python 3 everywhere)
 * reuse existing project's code, for example an existing LDAP
   dashboard or authentication system
 * ditch LDAP. it's hard to understand and uncommon enough to cause
   significant confusion for users.

### Non-Goals

 * we should avoid writing our own control panel, if possible

## Approvals required

The proposed solution should be adopted unanimously by TPA. A survey
might be necessary to confirm our users would be happy with the change
as well.

## Proposed Solution

TL;DR: three phase migration away from LDAP

 1. stopgap: merge with upstream, port to Python 3 if necessary
 2. move hosts to Puppet, replace ud-ldap with another user dashboard
 3. move users to Puppet (sysadmins) or Kubernetes / GitLab CI /
    GitLab Pages (developers), remove LDAP and replace with SSO dashboard

The long version...

### Short term: merge with upstream, port to Python 3 if necessary

In the **short term**, the situation with Python 2 needs to be
resolved. Either the Python code needs to be ported to Python 3, or it
needs to be replaced by something else. That is "urgent" in the sense
that Python 2 is already end of life and will likely not be supported
by the next Debian release, around summer 2024. Some work in that
direction has been done upstream, but it's currently unclear whether
ud-ldap is or will be ported to Python 3 in the short term.

The **diff with upstream** also makes it hard to collaborate. We
should make it possible to use directly the upstream package with a
local configuration, without having to ship and maintain our own fork.

### Mid term: move hosts to Puppet, possibly replace ud-ldap with simpler dashboard

In the **mid-term**, we should remove the duplication of duty
between Puppet and LDAP, at least in terms of actual file
distribution, which should be delegated to Puppet. In practical terms,
this implies replacing `ud-generate` and `ud-replicate` with the
Puppet server and agents. It could still talk with LDAP for the host
directory, but at that point it might be better to simply **move all
host metadata into Hiera**.

For users, the situation is less clear: we need some sort of dashboard
for users to **manage their email forward** and, if that project ever sees
the light of day, their email (submission, IMAP?) password. It is also
needed to **manage shell access** and SSH keys. So in the mid-term, the
**LDAP user directory would remain**.

At this point, however, it might not be necessary to use ud-ldap at
all: another dashboard could be use to manage the LDAP database. The
`ud-mailgate` interface could be retired and the web interface
replaced with something simpler, like [ldap-user-manager][ldap-user-manager].

So hopefully, in the mid term, it should be possible to completely
replace ud-ldap with Puppet for hosts and sysadmins, and an already
existing LDAP dashboard for user interaction.

### Long term: replace LDAP completely, with Puppet, GitLab and Kubernetes, possibly SSO dashboard

In the **long term**, the situation is muddier: at this stage, our
dependence on ud-ldap is either small (just users) or non-existent (we
use a different dashboard). But we still have LDAP, and that might be
a database we could get rid of completely.

We could simply stop offering shell access to non-admin users. User
access on servers would be managed completely by Puppet: only `sudo`
passwords need to be set for sysadmin anyways and those could live
inside Hiera.

Users currently requiring shell access would be encouraged to migrate
their service to a container image and workflow. This would be backed
by GitLab (for source code), GitLab CI/CD (for deployment) and
Kubernetes (for the container backend). Shell access would be limited
to sysadmins, which would take on orphan services which would be
harder to migrate inside containers.

Because the current shell access provided is very limited, it is
believe migration to containers would actually be not only feasible
but also beneficial for users, as they would possibly get more
privileges than they currently do.

Storage could be provided by Ceph and PostgreSQL clusters.

Those are the current services requiring shell access (as per
`allowedGroups` in the LDAP host directory), and their possible
replacements:

| Service                                     | Replacement                                  |
|---------------------------------------------|----------------------------------------------|
| Applications (e.g. bridgedb, onionoo, etc)  | GitLab CI, Kubernetes or Containers          |
| fpcentral                                   | [retirement?][]                              |
| Debian package archive                      | GitLab CI, GitLab pages                      |
| Email                                       | email-specific dashboard                     |
| Git(olite) maintenance                      | GitLab                                       |
| Git(web) maintenance                        | GitLab                                       |
| Jenkins                                     | GitLab CI                                    |
| Mailing lists                               | Debian packages + TPA                        |
| RT                                          | Debian packages + TPA                        |
| Schleuder maintenance                       | Debian packages + TPA                        |
| Shell server (e.g. IRC)                     | ZNC bouncer in a container                   |
| Static sites (e.g. mirror network, ~people) | GitLab Pages, GitLab CI, Nginx cache network |
| Trac                                        | GitLab                                       |

Note that this implies the TPA team takes over certain services
(e.g. Mailman, RT and Schleuder, in the above list). It might mean
expanding the sysadmin team to grant access to service admins.

It also implies switching the email service to another, hopefully
simpler, dashboard. Alternatively, this could be migrated back into
Puppet as well: we already manage a lot of email forwards by hand in
there and we already get support requests for people to change their
email forward because they do not understand the ud-ldap interface
well enough to do it themselves (e.g. [this ticket](https://gitlab.torproject.org/tpo/tpa/team/-/issues/40059)). We could also
completely delegate email hosting to a third-party provider, as was
discussed in [the submission project](submission).

Those are the applications that would need to be containerized for
this approach to be completed:

 * BridgeDB
 * Check/tordnsel
 * Collector
 * Concensus health
 * CiviCRM
 * Doctor
 * Exonerator
 * Gettor
 * Metrics
 * OnionOO
 * Survey
 * Translation
 * ZNC

[retirement?]: https://gitlab.torproject.org/tpo/tpa/team/-/issues/40009

This is obviously a quite large undertaking and would need to be
performed progressively. Thankfully, it can be done in parallel
without having to convert everything in one go.

Alternatively, a single-sign-on dashboard like [FreeIPA][] or
[Keycloak][] could be considered, to unify service authentication and
remove the plethora of user/password pairs we use everywhere. This is
definitely not being served by the current authentication system
(LDAP) which basically offers us a single password for all services
(unless we change the schema to add a password for each new service,
which is hardly practical).

## Cost

This would be part of the running TPA budget.

## Alternatives considered

The LDAP landscape in the free world is somewhat of a wasteland,
thanks to the "embrace and extend" attitude Microsoft has taken to the
standard (replacing LDAP and Kerberos with their proprietary Active
Directory standard).

### Replacement web interfaces

 * [eGroupWare][]: has an LDAP backend, probably not relevant
 * [LDAP account manager][]: self-service interface non-free
 * [ldap-user-manager][]: "PHP web-based interface for LDAP user
   account management and self-service password change", seems
   interesting
 * [GOsa][]: "administration frontend for user administration"
 * [phpLDAPadmin][]: like [phpMyAdmin][] but for LDAP, for "power users",
   long history of critical security issues
 * [web2ldap][]: web interface, python, still maintained, not exactly intuitive

[phpMyAdmin]: https://www.phpmyadmin.net/
[ldap-user-manager]: https://github.com/wheelybird/ldap-user-manager
It might be simpler to rewrite `userdir-ldap-cgi` with [Django][], say
using the [django-auth-ldap][] authentication plugin.

[web2ldap]: https://web2ldap.de/
[eGroupWare]: https://www.egroupware.org/en/
[phpLDAPadmin]: http://phpldapadmin.sourceforge.net/
[GOsa]: https://github.com/gosa-project/gosa-core
[LDAP account manager]: https://www.ldap-account-manager.org/lamcms/

### commandline tools

 * [cpu][]: "Change Password Utility", with an LDAP backend, no
   release since 2004
 * [ldapvi][]: currently in use by sysadmins
 * [shelldap][]: similar to ldapvi, but a shell!
 * [splatd][]: syncs `.forward`, SSH keys, home directories, abandoned
   for 10+ years?

[cpu]: http://cpu.sourceforge.net/
[splatd]: https://github.com/threerings/splatd
[shelldap]: https://hg.sr.ht/~mahlon/shelldap
[ldapvi]: http://www.lichteblau.com/ldapvi/

### others

 * [LDAP synchronization connector][]: "Open source connector to
   synchronize identities between an LDAP directory and any data
   source, including any database with a JDBC connector, another LDAP
   server, flat files, REST API..."
 * [Keycloak][]: single-sign-on interface which talks with LDAP
 * [FreeIPA][]: similar, except built on top of 389 DS, the Fedora
   LDAP thing
 * [LDAPjs][]: pure Javascript LDAP client
 * [GQLDAP][]: GTK client, abandoned
 * [LDAP admin][]: Desktop interface, written in Lazarus/Pascal (!)

[LDAP admin]: https://github.com/ibv/LDAP-Admin
[Django]: https://www.djangoproject.com/
[django-auth-ldap]: https://pypi.org/project/django-auth-ldap/
[GQLDAP]: https://sourceforge.net/projects/gqclient/
[LDAPjs]: http://ldapjs.org/
[FreeIPA]: https://www.freeipa.org/
[Keycloak]: https://www.keycloak.org/
[LDAP synchronization connector]: https://lsc-project.org/doku.php