auth-server
A low-level authentication server with pluggable backends and some
advanced features:
- two-factor authentication support (TOTP, U2F/WebAuthN)
- application-specific passwords
- rate limiting and brute force protection
- new device detection
Its purpose is to be the single point of authentication for all
authentication flows in a service.
Deployment
The auth-server is fully stateless: it delegates state to other
backends such as Memcached for short-term storage, and
usermetadb for long-term
anonymized user activity data. For this reason, it is recommended to
install an auth-server on every host.
The authentication protocol is a simple line-based text protocol. The
auth-server can listen on a UNIX or TCP socket: in the first case,
filesystem permissions should be used to control access to the socket,
while in the second case there is support for SSL, with optional
checks on the provided client certificates.
Services
A service in auth-server is a specific scope for an authentication
workflow, normally associated with a specific user-facing
service. Multiple services can be defined, each with its own
functionality and user backends.
User backends
The authentication server data model is based on the concept of a
user account. The server knows how to retrieve user accounts stored
in LDAP or SQL databases, but it has to be told the specific details
of how to find them and how to map the information there to what it
needs.
Other Dependencies
The auth-server can optionally use memcached to store short-term
data with a relatively high probability of retrieval. This is used to
store U2F challenges, and used OTP tokens for replay protection. If no
memcache servers are configured, such functionality will be disabled
but the auth-server will still run (useful for tests, or simpler
deployments).
It is possible to specify multiple memcached servers for HA purposes,
with a write-all / read-any model.
Configuration
The behavior of auth-server can be configured with a YAML file.
The YAML file should contain a dictionary with the following attributes:
services
is a dictionary describing all known services and their
authentication parameters. See the Service definition section below
services_dir
(optional) points at a directory containing service
configuration. Besides describing services in the main configuration
file (using the services
attribute), it is possible to define
additional services in YAML-encoded files (having a .yml
extension), which is more automation-friendly.
backends
is a dictionary describing all known backends and their
configuration parameters. The file backend is predefined and always
exists (it requires no configuration).
backends_dir
(optional) points at a directory containing backend
configuration as YAML-encoded files: all files with a .yml
extension will be loaded.
rate_limits
defines the global rate limiters and blacklists. See
the Rate limiting section below.
user_meta_server
holds the configuration for the user-meta-server
backend:
url
is the URL of the service
tls
configures TLS for the client:
cert
is the path to the client certificate
key
is the path to the client private key
ca
is the path to the CA store to verify the server certificate
memcache_servers
contains a list of memcached server addresses (in
host:port format)
Example configuration
An example configuration using both the sql backend (with default
schema and queries) for normal users, and the file backend for admin
users could look like this:
---
backends:
sql:
driver: sqlite3
db_uri: users.db
services:
example_service:
backends:
- backend: sql
- backend: file
params:
src: admins.yml
static_groups: [admins]
Rate limiting
Rate limits and blacklists are global (available to all services), to
allow brute force protection to work across multiple services. The
top-level configuration attribute rate_limits
is a dictionary of
named rate limiting configurations, that can be later referenced in
the service-specific rate_limits
list. Each rate limiter definition
should specify the following attributes:
limit
counts the number of events to allow over a period of time
period
defines the period of time
blacklist_for
adds the client to a blacklist if their request rate
goes above the specified threshold
on_failure
is a boolean value, when true the rate limiter will
only be applied to failed authentication requests
keys
is a list of strings specifying the request identifiers that
will make up the rate limiter key. The list can include one or both
of ip (referring to the remote client's IP) and user (username).
bypass
is a list of criteria that will cause the request to skip
the enforcement of this ratelimit/blacklist. Criteria are objects
with key
(one of ip or user) and value attributes, which
specify an exact equality match.
The following is an example of an IP-based ratelimit with blacklist
period of 1 hour, that will allow an arbitrary amount of requests from
localhost:
rate_limits:
blacklist_10qps_1h:
limit: 100
period: 10
blacklist_for: 3600
keys: [ip]
bypass:
- key: ip
value: "127.0.0.1"
- key: ip
value: "::1"
Service definition
Each service definition is a dictionary with the following attributes:
backends
is a list of user backend specifications, each one a
dictionary/map with the following attributes:
backend
must be the name of a backend that appears in the
top-level configuration map backends.
params
is a map of backend-specific attributes that configure
the backend for this service.
static_groups
is a list of group names that users sourced from
this backend will automatically be added to
challenge_response
is a boolean parameter that, when true, enables
two-factor authentication for this service (it should be enabled
only for interactive services)
enforce_2fa
is a boolean flag that, when true, will disable
non-2FA logins for this service
ignore_2fa
is a boolean flag that, when set, will ignore the
presence of application-specific passwords for the user, and will
always authenticate against the primary password
enable_last_login_reporting
is a boolean flag that enables last login
reporting to usermetadb
enable_device_tracking
is a boolean flag that enables device
tracking for this service (assuming the client provides device
information)
rate_limits
is a list of names of global rate limiters to be
applied to this service.
File backend
The file backend reads users and their credentials from a
YAML-encoded file. The service-specific configuration parameters are:
src
should point at the users file.
This file should contain a list of dictionaries, each representing a
user, with the following attributes:
name
is the username
email
is the email associated with the user (optional)
password
stores the encrypted password, see Password Encoding
below for details on the supported algorithms etc.
totp_secret
stores the unencrypted TOTP secret seed
(base32-encoded)
webauthn_registrations
is a list of WebAuthN registrations with
key_handle
and public_key
attributes, in the format used by the
webauthn-cred
tool (which generates keys in the proper WebAuthN formats)
u2f_registrations
is a list of legacy U2F registrations with
key_handle
and public_key
attributes, in the format used by
old versions of pamu2fcfg
groups
is a list of group names that the user belongs to
LDAP backend
The ldap backend will look up user information in a LDAP database.
The backend connects to a single LDAP server and requires the
following top-level configuration:
uri
of the LDAP server (like ldapi:///var/run/ldap/ldapi)
bind_dn
is the DN to bind with
bind_pw_file
points at a file containing the bind password
Each service can then use different queries, as shown in the next
section.
Query definition
LDAP queries are meant to return a single user account object from the
database using a search operation. There's two parts to it: first
the right object needs to be located, then we need to map the object's
attributes to someting that the auth-server understands.
The LDAP query for a service is defined by the following standard LDAP
parameters:
search_base
specifies a base DN for the search
search_filter
specifies a filter to apply to the search
scope
specifies the scope of the LDAP search, must be one of
base, one or sub
attrs
is a dictionary mapping LDAP attributes to their auth-server
metadata counterparts, see Schema definition below.
The search_filter
should contain somewhere the literal string %s
,
which will be replaced with the username in the final LDAP query.
Schema definition
In order to retrieve authentication information from the LDAP object,
the authentication server needs to know which attributes to use. To do
so, we use a so-called schema definition (a map of symbolic names to
LDAP attributes). The following attribute names are defined:
password
contains the encrypted password. Since this attribute is
often also used for authentication of the LDAP protocol itself, an
eventual {crypt}
prefix is ignored. Passwords should be encrypted,
see Password Encoding below for details on the supported
algorithms etc.
otp_secret
should contain the base32-encoded TOTP secret
app_specific_password
(possibly repeated) contains an encrypted
app-specific password
The default attribute mapping looks like this:
password: userPassword
totp_secret: totpSecret
app_specific_password: appSpecificPassword
Except for userPassword, the others are custom LDAP attributes and
are not part of any standard schema definition. You should create your
own.
App-specific passwords should be encoded as colon-separated strings:
service:encrypted_password:comment
The password should be encrypted. The comment is a free-form string
set by the user to tell the various credentials apart.
SQL backend
The SQL backend allows you to use a SQL database to store user
information. It can adapt to any schema, provided that you can write
the queries it expects.
The parameters for the SQL backend configuration are:
driver
is the name of the database/sql driver (currently it must
be one of sqlite3
, mysql
or postgres
, the built-in drivers)
db_uri
is the database URI (a.k.a. DSN), whose exact syntax will
depend on the chosen driver. Check out the documentation for the
database/sql sqlite,
mysql and
postgres drivers.
Query definition
Each service can specify a set of different SQL queries. It can be
configured with the following attributes:
queries
holds the map of SQL queries that tell the auth-server
how to query your database.
The known queries are identified by name. It does not matter what
operations you do as long as the queries take the expected input
substitution parameters, and return rows with the expected number of
fields (column names do not matter). Note that the order of returned
columns is critical, and it should match what is documented here. You
should use the parameter substitution symbol ?
as placeholder for
query parameters.
get_user
takes a single parameter (the user name) and must return
a single row with email, password, TOTP secret and shard
fields for the matching user.
get_user_groups
takes a single parameter (the user name) and must
return rows with a single group_name field corresponding to the
user's group memberships.
get_user_u2f
takes a single parameter (user name) and must return
the user's U2F registrations as rows with public_key and
key_handle fields, in their native binary format.
get_user_asp
takes a single parameter (user name) and must return
the user's application-specific passwords as rows with service and
password fields.
The only mandatory query is get_user, if the other ones are not
specified the associated fields will be empty.
Note that the relational queries (get_user_groups, get_user_u2f
and get_user_asp) should NOT return rows containing NULL values.
Example database schema
The following could be a (very simple) example database schema for a
case where usernames are also email addresses, with support for all
authentication features:
CREATE TABLE users (
email text NOT NULL,
password text NOT NULL,
totp_secret text,
shard text
);
CREATE UNIQUE INDEX users_email_idx ON users(email);
CREATE TABLE group_memberships (
email text NOT NULL,
group_name text NOT NULL
);
CREATE INDEX group_memberships_idx ON group_memberships(email);
CREATE TABLE webauthn_registrations (
email text NOT NULL,
key_handle blob NOT NULL,
public_key blob NOT NULL
);
CREATE INDEX webauthn_registrations_idx ON webauthn_registrations(email);
CREATE TABLE service_passwords (
email text NOT NULL,
service text NOT NULL,
password text NOT NULL
);
CREATE INDEX service_passwords_idx ON service_passwords(email);
(Note: this isn't a great schema example due to the lack of
referential integrity, it's just useful as an example)
With this schema, one could use the following configuration for a
service:
services:
example:
challenge_response: true
backends:
- backend: sql
params:
queries:
get_user: "SELECT email, password, totp_secret, shard FROM users WHERE email = ?"
get_user_groups: "SELECT group_name FROM group_memberships WHERE email = ?"
get_user_u2f: "SELECT public_key, key_handle FROM webauthn_registrations WHERE email = ?"
get_user_asp: "SELECT service, password FROM service_passwords WHERE email = ?"
Usage
The auth-server runs on a local UNIX socket. You can use UNIX
permissions to control who has access to this socket. The Debian
package makes it group-readable to the auth-server group, so you can
add specific users to it easily.
The daemon can run either standalone or be socket-activated by
systemd, which is what the Debian package does.
Check out the output of auth-server --help for documentation on how
to configure the listening sockets.
Wire protocol
The rationale behind the wire protocol ("why not http?") is twofold:
first, we wanted strict access control, and that's more easily done
with UNIX permissions, so UNIX sockets were chosen. Then, the protocol
should be able to transfer data maps, and it must be trivial to
implement (and verify) in C, Go and Python. Furthermore, it should
minimize external dependencies.
The protocol is line-based: multiple authentication requests can be
sent over the same connection, but every request must wait for a
response (i.e. no pipelining). Commands are single words, and can be
followed by a space and an attribute/value map. The responses are
simply attribute/value maps.
Attribute maps should have the following characteristics:
- maps can't be nested, they are simple key/value sets where both keys
and values are strings
- keys can't contain '=' characters
They are encoded using the following algorithm:
- if this is not the first attribute/value pair, add a space character
- add the key string
- add the '=' character
- if the value contains a non-printable character or a double quote:
- add the base64-encoded value
- if it does not:
- add a '"' character, then the value, then another '"' character.
API
There is only one command: auth
, which must be followed by the
authentication request. Parameters for an authentication request are:
service
: the service requesting the authentication
username
: name of the user to authenticate
password
: password (cleartext) provided by the user
otp
(optional): TOTP-based 2FA token
webauthn_session
(optional): opaque WebAuthN session data,
obtained from a previous response
webauthn_response
(optional): opaque WebAuthN response
data, obtained from the client device (usually the browser)
device
(optional): information about the client device
id
: a unique ID, specific to this device
remote_addr
: remote IP address (will be minimized)
remote_zone
: remote zone (country-level IP aggregation)
browser
: browser name
os
: client OS
user_agent
: client User-Agent string
mobile
: boolean variable indicating a mobile device
Responses will contain the following attributes:
status
: status of the request, one of ok,
insufficient_credentials or error
2fa_method
: if status is insufficient_credentials, one of
otp or u2f indicating which 2FA method should be used for the
next request
webauthn_session
: opaque WebAuthN session data, to be sent back
to the auth-server in the next authentication request
webauthn_data
: opaque WebAuthN credential assertion parameters
that should be used by the client device to sign the next request
user
: when status is ok (the authentication has been
successful), this dictionary will contain user information:
email
: email of this user
groups
: groups the user is a member of.
Note that the WebAuthN-related parameters are treated as opaque
strings but are actually JSON-encoded blobs.
Password encoding
Multiple password hashing algorithms are supported. The format is the
well-known dollar-separated field string, extended with optional
algorithm-specific parameters:
$id[$params...]$salt$encrypted
where the optional params field is itself a dollar-separated list of
integers.
All id values understood by the libc crypt(3) function are
supported, as well as a few more custom algorithms:
-
Scrypt (id $s$
), in which case the parameters are N, R and
P.
-
Argon2 (id $a2$
), with parameters time, memory and
threads.
Check the documentation for these algorithms for an explanation of the
meaning of the parameters. Each algorithm has different requirements
for the salt.