Commit ad6e4c82 authored by ale's avatar ale
Browse files

Update dependencies

parent ff465de0
package clientutil
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
)
// DoJSONHTTPRequest makes an HTTP POST request to the specified uri,
// with a JSON-encoded request body. It will attempt to decode the
// response body as JSON.
func DoJSONHTTPRequest(ctx context.Context, client *http.Client, uri string, req, resp interface{}) error {
data, err := json.Marshal(req)
if err != nil {
return err
}
httpReq, err := http.NewRequest("POST", uri, bytes.NewReader(data))
if err != nil {
return err
}
httpReq.Header.Set("Content-Type", "application/json")
httpReq = httpReq.WithContext(ctx)
httpResp, err := RetryHTTPDo(client, httpReq, NewExponentialBackOff())
if err != nil {
return err
}
defer httpResp.Body.Close()
if httpResp.StatusCode != 200 {
return fmt.Errorf("HTTP status %d", httpResp.StatusCode)
}
if httpResp.Header.Get("Content-Type") != "application/json" {
return errors.New("not a JSON response")
}
if resp == nil {
return nil
}
return json.NewDecoder(httpResp.Body).Decode(resp)
}
......@@ -2,7 +2,6 @@ package clientutil
import (
"errors"
"net"
"net/http"
"time"
......@@ -18,14 +17,37 @@ func NewExponentialBackOff() *backoff.ExponentialBackOff {
return b
}
// Retry operation op until it succeeds according to the backoff policy b.
// A temporary (retriable) error is something that has a Temporary method.
type tempError interface {
Temporary() bool
}
type tempErrorWrapper struct {
error
}
func (t tempErrorWrapper) Temporary() bool { return true }
// TempError makes a temporary (retriable) error out of a normal error.
func TempError(err error) error {
return tempErrorWrapper{err}
}
// Retry operation op until it succeeds according to the backoff
// policy b.
//
// Note that this function reverses the error semantics of
// backoff.Operation: all errors are permanent unless explicitly
// marked as temporary (i.e. they have a Temporary() method that
// returns true). This is to better align with the errors returned by
// the net package.
func Retry(op backoff.Operation, b backoff.BackOff) error {
innerOp := func() error {
err := op()
if err == nil {
return err
}
if netErr, ok := err.(net.Error); ok && netErr.Temporary() {
if tmpErr, ok := err.(tempError); ok && tmpErr.Temporary() {
return err
}
return backoff.Permanent(err)
......@@ -33,12 +55,21 @@ func Retry(op backoff.Operation, b backoff.BackOff) error {
return backoff.Retry(innerOp, b)
}
var errHTTPBackOff = errors.New("http status 503")
var errHTTPBackOff = TempError(errors.New("temporary http error"))
func isStatusTemporary(code int) bool {
switch code {
case http.StatusTooManyRequests, http.StatusBadGateway, http.StatusServiceUnavailable, http.StatusGatewayTimeout:
return true
default:
return false
}
}
// RetryHTTPDo retries an HTTP request until it succeeds, according to
// the backoff policy b. It will retry on temporary network errors and
// upon receiving specific throttling HTTP errors (currently just
// status code 503).
// upon receiving specific temporary HTTP errors. It will use the
// context associated with the HTTP request object.
func RetryHTTPDo(client *http.Client, req *http.Request, b backoff.BackOff) (*http.Response, error) {
var resp *http.Response
op := func() error {
......@@ -49,13 +80,13 @@ func RetryHTTPDo(client *http.Client, req *http.Request, b backoff.BackOff) (*ht
var err error
resp, err = client.Do(req)
if err == nil && resp.StatusCode == 503 {
if err == nil && isStatusTemporary(resp.StatusCode) {
resp.Body.Close()
return errHTTPBackOff
}
return err
}
err := Retry(op, b)
err := Retry(op, backoff.WithContext(b, req.Context()))
return resp, err
}
......@@ -125,6 +125,9 @@ func (b *balancer) dial(ctx context.Context, network, addr string) (net.Conn, er
if err == nil {
return conn, nil
} else if err == context.Canceled {
// A timeout might be bad, set the error bit
// on the connection.
b.notify(addr, false)
return nil, err
}
b.notify(addr, false)
......
A/I Auth Server
===============
auth-server
===========
An authentication server with an LDAP backend and some advanced
features:
A low-level authentication server with pluggable backends and some
advanced features:
* OTP support
* two-factor authentication support (TOTP, U2F)
* application-specific passwords
* rate limiting and brute force protection
* user partitioning across multiple backends
* new device detection
The authentication server can be queried using an HTTP API. The
software includes a PAM module (pam_authclient) that can be used
to integrate system services.
Its purpose is to be the single point of authentication for all
authentication flows in a service.
# Deployment
# Configuration
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.
The server's behavior can be configured with a Python file defining
some module-level variables:
* `LDAP_URI` is the LDAP connection string.
* `LDAP_BIND_DN` and `LDAP_BIND_PW` specify the bind credentials: they
should allow the server to read authentication-related attributes on
the user objects, and to search the user subtree.
* `LDAP_SERVICE_MAP` is a `{service: query}` dictionary that defines
how to query the LDAP database for user accounts, for each service.
* `LDAP_SCHEMA` is a dictionary that describes how to retrieve
authentication information from the LDAP object attributes.
# Configuration
## Query definition
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
* `rate_limits` defines the global rate limiters and blacklists. See
the *Rate limiting* section below.
* `enabled_backends` is the list of user backends that should be
enabled (the available backends are *file* and *ldap*)
* `ldap_config` specifies the configuration for the LDAP backend --
see the *LDAP Backend* section below
* `user_meta_server` holds the configuration for the user-meta-server
backend:
* `backend_url` is the URL of the service
* `tls_config` 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
## 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:
* `backends` is a list of user backend specifications, each one should
include a backend-specific configuration under an attribute named
after the backend itself:
* `file` is simply a path to a user list file, see the *File
backend* section below
* `ldap` configues the LDAP backend for this service
* `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
* `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. 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
* `totp_secret` stores the *unencrypted* TOTP secret seed
* `groups` is a list of group names that the user belongs to
The file backend only supports TOTP as a two-factor authentication
method, U2F support is currently missing.
## LDAP Backend
The *ldap* backend will look up user information in a LDAP database.
### Query definition
LDAP queries are meant to return a single user account object from the
database using a *search* operation. They are represented as
dictionaries, with the following fields:
* `dn`: specifies the full DN of the object
* `base`: specifies a base DN for the search
* `filter`: specifies a filter to apply to the search
The `dn` and `base` attributes are mutually exclusive. In fact,
queries can be of two types:
* If you can know the specific DN of a user account object given its
username, specify the `dn` field. This will run a SCOPE_BASE query
for that specific DN. In this case, `filter` is optional.
* Specify `base` and `filter` together to identify a single object.
This will result in a SCOPE_SUBTREE search starting at `base`. In
this case the `filter` attribute is required.
On every incoming request, the query fields are subject to string
substitution, using the standard Python syntax of the `%` operator,
with the following client-provided variables available:
* `user`: username
* `service`: authentication service
* `shard`: user shard
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.
So, for example, a mail service could use the following query:
The LDAP query for a service is defined by the following standard LDAP
parameters:
{
'base': 'ou=Accounts,dc=example,dc=com',
'filter': 'mail=%(user)s'
}
* `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.
or alternatively, if the database structure is simple enough:
The `search_filter` should contain somewhere the literal string `%s`,
which will be replaced with the username in the final LDAP query.
{
'dn': 'mail=%(user)s,ou=Accounts,dc=example,dc=com'
}
## Schema definition
### 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`: attribute containing the encrypted password. Usually
(and by default) this is `userPassword`, a somewhat standard LDAP
attribute. Since this attribute is often also used for
authentication of the LDAP protocol itself, an eventual `{crypt}`
prefix is ignored. Passwords should be encrypted with the system
`crypt` method.
* `otp_secret`: this attribute should contain the hex-encoded TOTP
secret.
* `app_specific_password`: attribute (possibly defined more than once)
containing an encoded app-specific password.
* `shard`: if set, LDAP attribute containing a shard ID for user
partitioning.
The LDAP backend module makes some assumptions on the structure of the
user database: the most important is that app-specific passwords are
attributes of the user object. App-specific passwords should be
encoded as colon-separated strings:
* `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
service:encrypted_password:comment
The default attribute mapping looks like this:
Again, the password should be encrypted with the system `crypt`
method. The comment is a free-form string set by the user to tell the
various credentials apart.
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.
## OTP implementation
......@@ -120,90 +184,3 @@ token revocation is a read-write, locked operation which is more
difficult to perform on a LDAP backend.
# Usage
## Client authentication
Requests to the authentication server can themselves be authenticated,
and it may be a good idea to do so in a production environment.
Authserv supports TLS-based authentication using a private CA: in
order to perform authentication queries, a client must present an X509
certificate signed by a specific Certification Authority.
This feature is enabled by passing the `--ca`, `--ssl-cert` and
`--ssl-key` options to the server.
The PAM client has options to specify a client certificate.
## HTTP API
The authentication server accepts HTTP POST requests at the
`/api/1/auth` URL. Requests should contain the following parameters
(with a Content-Type of `application/x-www-form-urlencoded`):
* `service`: the service to authenticate for
* `username`: username
* `password`: password
* `otp_token`: a 6-digit TOTP token (optional)
* `source_ip`: the remote address of the client connection (optional)
* `shard`: server shard (optional)
The response is a simple string, one of:
* `OK`: the authentication request was successful
* `ERR_AUTHENTICATION_FAILURE`: the authentication request failed
* `ERR_OTP_REQUIRED`: an OTP token is required in order to
authenticate, but it was not provided in the request.
If the client application receives an `ERR_OTP_REQUIRED` result, it
should prompt the user for an OTP token and retry the authentication
request (including the same username and password) with the
`otp_token` field set. The PAM module, for example, can ask the user
for the OTP token interactively, while web applications could display
an interstitial form.
## PAM
A PAM module, `pam_authclient`, is provided to integrate system
services with the authentication server. It provides only the
*account* (a no-op) and *auth* operations.
Example usage:
auth required pam_authclient.so auth_server=127.0.0.1:1616
The module knows about the following options:
* `auth_server=`*HOST:PORT* specifies the authentication server to talk
to. It's possible to specify more than one server, separated by
commas, in which case they will be tried in sequence in case of
failure.
* `ssl_crt=`*FILE* load the X509 client certificate from FILE (in PEM format).
* `ssl_key=`*FILE* load the X509 certificate key from FILE.
* `ca=`*FILE* should point at the X509 CA certificate, used to verify
the authenticity of the server connection: the server certificate
must be signed by this CA.
* `shard=`*ID* sets the *shard* parameter, which may be used by the
authentication server to limit authentication to a subset of users.
The `service` parameter sent to the auth server will be the PAM service,
it is not possible to override this in the PAM configuration.
## NGINX mail_http_auth support
The authentication server can optionally offer an HTTP API compatible
with NGINX's `mail_http_auth` module. This is particularly useful in
combination with user partitioning, as the server can use the `shard`
attribute of the user to direct NGINX to the right backend.
The module understands the following configuration variables:
* `NGINX_AUTH_SERVICE` (default: `mail`) is the service that will be
used for authentication by this module.
* `NGINX_AUTH_PORT_MAP` is a dictionary that maps protocol names to
port numbers for the backend connection. The default is
`{"pop3": 110, "imap": 143}`.
---
enabled_backends:
- file
services:
test:
backends:
- { file: test-users.yml }
interactive:
challenge_response: true
backends:
- { file: test-users.yml }
---
- name: testuser
email: testuser@example.com
password: "16384$8$1$c479e8eb722f1b071efea7826ccf9c20$96d63ebed0c64afb746026f56f71b2a1f8796c73141d2d6b1958d4ea26c60a0b"
groups:
- group1
- group2
- name: 2fauser
email: 2fauser@example.com
password: "16384$8$1$c479e8eb722f1b071efea7826ccf9c20$96d63ebed0c64afb746026f56f71b2a1f8796c73141d2d6b1958d4ea26c60a0b"
totp_secret: "O32OBVS5BL5EAPB5"
// Copyright 2016 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build ignore
// mkpost processes the output of cgo -godefs to
// modify the generated types. It is used to clean up
// the sys API in an architecture specific manner.
//
// mkpost is run after cgo -godefs; see README.md.
package main
import (
"bytes"
"fmt"
"go/format"
"io/ioutil"
"log"
"os"
"regexp"
)
func main() {
// Get the OS and architecture (using GOARCH_TARGET if it exists)
goos := os.Getenv("GOOS")
goarch := os.Getenv("GOARCH_TARGET")
if goarch == "" {
goarch = os.Getenv("GOARCH")
}
// Check that we are using the new build system if we should be.
if goos == "linux" && goarch != "sparc64" {
if os.Getenv("GOLANG_SYS_BUILD") != "docker" {
os.Stderr.WriteString("In the new build system, mkpost should not be called directly.\n")
os.Stderr.WriteString("See README.md\n")
os.Exit(1)
}
}
b, err := ioutil.ReadAll(os.Stdin)
if err != nil {
log.Fatal(err)
}
// If we have empty Ptrace structs, we should delete them. Only s390x emits
// nonempty Ptrace structs.
ptraceRexexp := regexp.MustCompile(`type Ptrace((Psw|Fpregs|Per) struct {\s*})`)
b = ptraceRexexp.ReplaceAll(b, nil)
// Replace the control_regs union with a blank identifier for now.
controlRegsRegex := regexp.MustCompile(`(Control_regs)\s+\[0\]uint64`)
b = controlRegsRegex.ReplaceAll(b, []byte("_ [0]uint64"))
// Remove fields that are added by glibc
// Note that this is unstable as the identifers are private.
removeFieldsRegex := regexp.MustCompile(`X__glibc\S*`)
b = removeFieldsRegex.ReplaceAll(b, []byte("_"))
// We refuse to export private fields on s390x
if goarch == "s390x" && goos == "linux" {
// Remove cgo padding fields
removeFieldsRegex := regexp.MustCompile(`Pad_cgo_\d+`)
b = removeFieldsRegex.ReplaceAll(b, []byte("_"))
// Remove padding, hidden, or unused fields
removeFieldsRegex = regexp.MustCompile(`X_\S+`)
b = removeFieldsRegex.ReplaceAll(b, []byte("_"))
}
// Remove the first line of warning from cgo
b = b[bytes.IndexByte(b, '\n')+1:]
// Modify the command in the header to include:
// mkpost, our own warning, and a build tag.
replacement := fmt.Sprintf(`$1 | go run mkpost.go
// Code generated by the command above; see README.md. DO NOT EDIT.
// +build %s,%s`, goarch, goos)
cgoCommandRegex := regexp.MustCompile(`(cgo -godefs .*)`)
b = cgoCommandRegex.ReplaceAll(b, []byte(replacement))
// gofmt
b, err = format.Source(b)
if err != nil {
log.Fatal(err)
}
os.Stdout.Write(b)
}
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build ignore
/*
Input to cgo -godefs. See README.md
*/
// +godefs map struct_in_addr [4]byte /* in_addr */