diff --git a/README.md b/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..b9c7e67a912455abc3ea73912d4bf14b59ca75b7
--- /dev/null
+++ b/README.md
@@ -0,0 +1,106 @@
+sso
+===
+
+Login server (or *identity provider*, IDP) using the
+[ai/sso](https://git.autistici.org/ai/sso) protocol (version 5) for
+single-sign on and [auth-server](https://git.autistici.org/id/auth) to
+authenticate users.
+
+This repository includes a few separate binaries:
+
+* *sso-server* is the login server / IDP
+* *saml-server* is a SSO-to-SAML bridge (for third-party software)
+* *sso-proxy* is a reverse HTTP proxy that adds single-sign-on access
+  controls to backends
+
+# Configuration
+
+The *sso-server* program requires a YAML configuration file. It
+understands the following attributes:
+
+* `secret_key_file`: path to the Ed25519 secret key (should be exactly
+64 bytes)
+* `public_key_file`: path to the Ed25519 public key (should be exactly
+32 bytes)
+* `domain`: SSO domain
+* `allowed_services`: a list of regular expressions. A request will be
+  allowed only if the target SSO services matches one of these
+  expressions.
+* `allowed_exchanges`: a list of regular expression source /
+  destination pairs (dictionaries with `src_regexp` and `dst_regexp`
+  attributes). Exchange requests will only be allowed if source and
+  destination SSO services both match one of these pairs.
+* `service_ttls`: a list of dictionaries used to set time-to-live for
+  SSO tickets for specific services. Each dictionary should have the
+  following attributes:
+  * `regexp`: regular expression that should match the SSO service
+  * `ttl`: TTL in seconds
+* `auth_session_lifetime`: time-to-live (in seconds) for the
+  sso-server user authentication session. When it expires, the user
+  will have to login again.
+* `session_secrets`: a list of two (or more, as long as the number is
+  even) secret keys to use for HTTP cookie-based sessions, in
+  *authentication-key*, *encryption-key* pairs. Authentication keys
+  can be 32 bytes (SHA128) or 64 bytes (SHA512), encryption keys
+  should be 16 (AES-128), 24 (AES-192) or 32 (AES-256) bytes long. For
+  key rotation, multiple pairs (old, new) can be specified so that
+  sessions are not immediately invalidated.
+* `csrf_secret`: a secret key used for CSRF protection
+* `auth_service`: the service name to use for the authentication
+  request sent to *auth-server* (generally "sso")
+* `device_manager`: configuration for the device tracking module:
+  * `auth_key`: a long-term key to authenticate HTTP-based cookies
+  * `geo_ip_data_files`: GeoIP databases to use (in mmdb format), if
+    unset the module will use the default GeoLite2-Country db
+  * `trusted_forwarders`: list of trusted IP addresses (reverse
+    proxies). If a request comes from here, we will use the
+    *remote_addr_header* instead
+  * `remote_addr_header`: HTTP header to use to obtain the remote
+    client address, when the request comes from a trusted forwarder
+* `http_server` specifies standard parameters for the HTTP server:
+  * `tls` contains the server-side TLS configuration:
+    * `cert` is the path to the server certificate
+    * `key` is the path to the server's private key
+    * `ca` is the path to the CA used to validate clients
+    * `acl` specifies TLS-based access controls, a list of entries
+      with the following attributes:
+      * `path` is a regular expression to match the request URL path
+      * `cn` is a regular expression that must match the CommonName
+        part of the subject of the client certificate
+
+## Device tracking
+
+The idea is to track a small amount of non-personally-identifying data
+for each device, and use it to notify users of unexpected
+accesses. This information is tracked by the
+[user-meta-server](https://git.autistici.org/id/usermetadb).
+
+It is implemented very simply, with a long-term cookie stored in the
+browser.
+
+
+# API
+
+The *sso-server* binary serves different types of HTTP traffic:
+
+* the login/logout interface (user-facing)
+* the SSO login endpoint (user-facing)
+* the SSO ticket exchange endpoint (service-facing)
+
+The ticket exchange API allows a service (the *source*) to exchange a
+valid SSO ticket for itself with a SSO ticket, for the same user,
+meant for a third-party service (*destination*). Its endpoint is
+located at the URL `/exchange` and it accepts the following query
+parameters:
+
+* `cur_tkt`: valid source SSO ticket
+* `cur_svc`: source SSO service
+* `cur_nonce`: nonce for *cur_tkt*
+* `new_svc`: destination SSO service
+* `new_nonce`: nonce for the new SSO ticket
+* `new_groups` (optional): a comma-separated list of groups that the
+  destination service might check membership for
+
+Note that annoyingly *cur_svc* and *cur_nonce* are redundant, as they
+are already contained within *cur_tkt*, but the SSO ticket API won't
+allow us to decode the ticket without verifying it at the same time.