README.md 15.3 KB
Newer Older
ale's avatar
ale committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42

auth-server
===========

A low-level authentication server with pluggable backends and some
advanced features:

* two-factor authentication support (TOTP, U2F)
* 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](https://git.autistici.org/id/usermetadb) for long-term
anonymized user activity data. For this reason, it is recommended to
install an auth-server on every host.

It listens for authorization requests over a UNIX socket. UNIX
permissions should be used to control access to the socket if
necessary. Clients speak a custom simple line-based attribute/value
protocol, and can send multiple requests over the same connection.

## 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, 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.

43 44 45 46 47 48 49 50 51 52 53 54
## 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.

ale's avatar
ale committed
55 56 57 58 59 60 61
# 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
62 63 64 65 66
* `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.
ale's avatar
ale committed
67 68 69 70 71 72
* `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.
ale's avatar
ale committed
73 74 75 76
* `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:
ale's avatar
ale committed
77
  * `url` is the URL of the service
ale's avatar
ale committed
78
  * `tls` configures TLS for the client:
ale's avatar
ale committed
79 80 81
    * `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
82 83
* `memcache_servers` contains a list of memcached server addresses (in
  host:port format)
ale's avatar
ale committed
84

ale's avatar
ale committed
85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106
## 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:

```yaml
---
backends:
  sql:
    driver: sqlite3
    db_uri: users.db
services:
  example_service:
    backends:
      - backend: sql
      - backend: file
        params:
          src: admins.yml
        static_groups: [admins]
```

ale's avatar
ale committed
107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129
## 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)

## Service definition

Each service definition is a dictionary with the following attributes:

ale's avatar
ale committed
130 131 132 133 134 135
* `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.
136 137
  * `static_groups` is a list of group names that users sourced from
    this backend will automatically be added to
ale's avatar
ale committed
138 139 140 141 142
* `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
shammash's avatar
shammash committed
143 144
* `enable_last_login_reporting` is a boolean flag that enables last login
  reporting to usermetadb
ale's avatar
ale committed
145 146 147 148 149 150 151 152 153
* `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
ale's avatar
ale committed
154 155 156 157 158 159
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:
ale's avatar
ale committed
160 161 162 163 164

* `name` is the username
* `email` is the email associated with the user (optional)
* `password` stores the encrypted password
* `totp_secret` stores the *unencrypted* TOTP secret seed
ale's avatar
ale committed
165 166 167
* `u2f_registrations` is a list of U2F registrations with `key_handle`
  and `public_key` attributes, in the format used by *pamu2fcfg* (for
  convenience)
ale's avatar
ale committed
168 169
* `groups` is a list of group names that the user belongs to

ale's avatar
ale committed
170
## LDAP backend
ale's avatar
ale committed
171 172

The *ldap* backend will look up user information in a LDAP database.
ale's avatar
ale committed
173 174
The backend connects to a single LDAP server and requires the
following top-level configuration:
ale's avatar
ale committed
175 176 177 178 179

* `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

ale's avatar
ale committed
180 181
Each service can then use different queries, as shown in the next
section.
ale's avatar
ale committed
182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233

### 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.
* `otp_secret` should contain the hex-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.

ale's avatar
ale committed
234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329
## 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](https://github.com/mattn/go-sqlite3),
  [mysql](https://github.com/go-sql-driver/mysql) and
  [postgres](https://godoc.org/github.com/lib/pq) 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). 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.

### 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:

```sql
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 u2f_registrations (
    email text NOT NULL,
    key_handle blob NOT NULL,
    public_key blob NOT NULL
);
CREATE INDEX u2f_registrations_idx ON u2f_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);
```

With this schema, one could use the following configuration for a
service:

```yaml
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 u2f_registrations WHERE email = ?"
            get_user_asp: "SELECT service, password FROM service_passwords WHERE email = ?"
```

ale's avatar
ale committed
330

ale's avatar
ale committed
331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410
# 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.

## 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
* `u2f_app_id` (optional): U2F AppID
* `u2f_response` (optional): U2F response object
  * `key_handle`
  * `signature_data`
  * `client_data`
* `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
* `u2f_req`: when *2fa_method* is *u2f*, this field will contain a U2F
  SignResponse object:
  * `version`
  * `challenge`
  * `registered_keys`: a list of registered keys
* `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.
ale's avatar
ale committed
411