Older versions of hashbangctl(1)
used to be called through sudo(8)
, with all
users being allowed to call that command without password.
This is not optimal, both for security reasons and because it would make it very
complex for hashbangctl(1)
to outlast the user's session (for instance, in the
case of user deletion).
The main alternatives considered were ØMQ and DBus.
- DBus is overly complex, and while making a set of Python methods available would not be a challenge, secure (including race-free) authentication was.
- ØMQ would only provide framing (its existing authentication protocol was not
suitable for our needs), and makes it impossible to use
SO_PEERCRED
as the API does not expose the underlying socket.
Users are authenticated using the SO_PEERCRED
socket option, which returns a
structure containing the user's id (and groups ids).
This was chosen because:
- it doesn't require cooperation from the client, making it very easy to implement primitive clients (in a shell script, for instance);
- it is simple for the server to implement, leaving very little room for error;
- the credentials are provided by the kernel itself, making them impossible to
spoof, except by
root
.
NOTE: Administrators can use sudo -u someuser hashbangctl
to impersonate a
user. This was hard to cleanly do in the previous design.
SCM_CREDENTIALS
allows sending kernel-checked credentials through a Unix domain socket; it requires explicit cooperation from the client, for no specific benefit (in our usecase).- The
python-dbus
library does not provide way to obtain the sending user without suffering from a race-condition that could enable attackers to impersonate other users.
Messages are encoded in I-JSON, a fragment of JSON that removes many ambiguities from the original specification.
As such, any standard-conformant JSON implementation can reliably parse and interpret data encoded using I-JSON.
In particular, I-JSON messages MUST be encoded using UTF-8.
The socket type SOCK_SEQPACKET
is used to provide reliable, in-order delivery
of datagrams: this means the operating system already handles framing for us.
The protocol works on a simple request/response model:
- Upon socket connection, the daemon sends a record describing the user (name, pubkeys, ...).
- The frontend can send back a request, identified by its
type
field:change
denotes a request to update the information in LDAP; fields (name
,pubkey
, ...) that are not filled in are taken from LDAP;delete
is a request to delete the requesting user; no additional information is required.
- The daemon respond with a type that is either
success
(without associated information) orerror
(with an optionalmsg
string field). - From there on, the client can submit a new request (back to 2).
NOTE: The client can disconnect at any point; SOCK_SEQPACKET
guarantees
reliable, atomic message delivery.
For example, sending {"type": "delete"}
and exiting without waiting for the response is valid.
Removed
type = delete
, that's a terrible idea (much better to disable an account by setting it to nologin).