ceo is a distributed HTTP application running on three hosts. As of this writing, those are phosphoric-acid, mail and caffeine (coffee in the dev environment).
/api/mailmanendpoints. This is because the REST API for Mailman3 is currently configured to run on localhost.
caffeinehost provides the
/api/dbendpoints. This is because the root account of MySQL and PostgreSQL on caffeine can only be accessed locally.
cloudhost provides the
/api/cloudendpoints. This is because the NGINX vhost files need to be created on the host where the cloud NGINX server is running.
- All other endpoints are provided by
phosphoric-acid. phosphoric-acid is the only host with the
ceod/adminKerberos key which means it is the only host which can create new principals and reset passwords.
Some endpoints can be accessed from multiple hosts. This is explained more in Security.
Interestingly, ceod instances can actually make API calls to each other. For example, when the instance on phosphoric-acid creates a new user, it will make a call to the instance on mail to subscribe the user to the csc-general mailing list.
In the old ceo, most LDAP modifications were performed on the client side, using the client's Kerberos credentials to authenticate to LDAP via GSSAPI. Using the client's credentials is desirable since we currently have custom authz rules in our slapd.conf on auth1 and auth2. If we were to use the server's credentials instead, this would result in two different sets of authz rules - one at the API layer and one at the OpenLDAP layer - and syscom members would very likely forget to update both at the same time.
So, we want a way for the server to use the client's credentials when interacting with LDAP. The most secure way to do this is via a Kerberos extension called "constrained delegation", or S4U. While the MIT KDC, which we are currently using, does provide support for S4U, this requires using LDAP as a database backend, which we are not using. While it is theoretically possible to migrate our KDC databases to LDAP, this would be a very risky operation, and probably not worth it if ceo is the only app which will use it.
Therefore, we will use unconstrained delegation. The client essentially forwards their TGT to ceod, which uses it to access other services over GSSAPI on the client's behalf. We accomplish this using GSSAPI delegation (i.e. set the GSS_C_DELEG_FLAG when creating a security context).
Since the client's credentials are used when interacting with LDAP, this means that most LDAP-related endpoints can actually be accessed from any host. Only the Kerberos-specific endpoints (e.g. resetting a password) truly need to be on phosphoric-acid.
As of this writing, there are two endpoints where the ceod/admin credentials are used instead: creating new members, and renewing existing members. This is because office staff need to be able to use these endpoints, and allowing them to directly create new LDAP records would be a privilege escalation; allowing them to directly modify the shadowExpire field is undesirable as well because this could prevent syscom members from logging in.
The REST API uses SPNEGO for authetication via the HTTP Negotiate Authentication scheme (https://www.ietf.org/rfc/rfc4559.txt). The API does not verify that the user actually knows the key for the service ticket; therefore, TLS is necessary to prevent MITM attacks. (TLS is also necessary to protect the KRB-CRED message, which is unencrypted.)
SPNEGO is pretty awkward, to be honest, as it completely breaks the stateless nature of HTTP. If we decide that SPNEGO is too much trouble, we should switch to plain HTTP cookies instead, and cache them somewhere in the client's home directory.
For future contributors: if you wish to make ceo accessible from the browser, you will need to add some kind of "Kerberos gateway" logic to the API such that the user's password can be used to obtain Kerberos tickets. One possible implementation would be to prompt the user for a password, obtain a TGT, then encrypt the TGT and store it as a JWT in the user's browser. The API can decrypt the JWT later and use it as long as the ticket has not expired; otherwise, the user will be re-prompted for their password.