auth

package module
v0.0.0-...-0fd5418 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 17, 2023 License: MIT Imports: 9 Imported by: 4

README

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.

Documentation

Index

Constants

View Source
const (
	MechanismPassword = "password"
	MechanismASP      = "asp"
	MechanismOTP      = "otp"
	MechanismWebAuthN = "webauthn"
)

Possible methods that were used to achieve successful authentication.

View Source
const (
	TFAMethodNone = ""
	TFAMethodOTP  = "otp"
	TFAMethodU2F  = "u2f"
)

Known second-factor auth methods.

View Source
const (
	StatusOK = iota
	StatusInsufficientCredentials
	StatusError
)

Possible response statuses.

Variables

View Source
var DefaultCodec = &kvCodec{}

DefaultCodec is the codec used by the on-wire protocol.

Functions

This section is empty.

Types

type Codec

type Codec interface {
	Encode(interface{}) []byte
	Decode([]byte, interface{}) error
}

Codec serializes simple key/value maps on the wire. Requirements for a codec: it must transfer maps, it must be trivial to implement in both C, Go and Python. It must not have messy external dependencies if possible.

type Mechanism

type Mechanism string

Mechanism is an enum for the possible authentication mechanisms.

func (Mechanism) String

func (m Mechanism) String() string

type Request

type Request struct {
	Service          string
	Username         string
	Password         []byte
	OTP              string
	WebAuthnSession  *webauthn.SessionData
	WebAuthnResponse *protocol.ParsedCredentialAssertionData
	DeviceInfo       *usermetadb.DeviceInfo
}

Request to authenticate a user. It supports multiple methods for authentication including challenge-response 2FA.

func (*Request) DecodeFromMap

func (r *Request) DecodeFromMap(m map[string]string, prefix string)

func (*Request) EncodeToMap

func (r *Request) EncodeToMap(m map[string]string, prefix string)

type Response

type Response struct {
	Status          Status
	Mechanism       Mechanism
	AuthenticatorID string
	TFAMethods      []TFAMethod
	WebAuthnSession *webauthn.SessionData
	WebAuthnData    *protocol.CredentialAssertion
	UserInfo        *UserInfo
}

Response to an authentication request.

func (*Response) DecodeFromMap

func (r *Response) DecodeFromMap(m map[string]string, prefix string)

func (*Response) EncodeToMap

func (r *Response) EncodeToMap(m map[string]string, prefix string)

func (*Response) Has2FAMethod

func (r *Response) Has2FAMethod(needle TFAMethod) bool

Has2FAMethod checks for the presence of a two-factor authentication method in the Response.

type Status

type Status int

Status of an authentication request.

func (Status) Error

func (s Status) Error() string

func (Status) String

func (s Status) String() string

type TFAMethod

type TFAMethod string

TFAMethod is a hint provided to the caller with the type of 2FA method that is available for authentication.

type UserInfo

type UserInfo struct {
	Email  string
	Shard  string
	Groups []string
}

UserInfo contains optional user information that may be useful to authentication endpoints.

func (*UserInfo) EncodeToMap

func (u *UserInfo) EncodeToMap(m map[string]string, prefix string)

Directories

Path Synopsis
sql
cmd

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL