Configuration
The backend takes a few command-line flags, a JSON config file and environment variables; its keys live in a PKCS#11 token. freshfront is configured only through environment variables. For how the pieces fit together see Running pykrete.
Command line
pykrete --database-url <URL> --config <PATH>
| Flag | Type | Default | Required | What it does |
|---|---|---|---|---|
|
string |
- |
yes (or |
PostgreSQL connection URL, for example |
|
path |
- |
yes |
JSON config file, see Config file. |
--help prints the same list.
Config file
A JSON object. Unknown keys are ignored silently, so check spelling.
| Key | Type | Default | Required | What it does |
|---|---|---|---|---|
|
string |
- |
yes |
Public host name, plus |
|
boolean |
|
no |
Generate |
|
socket address |
|
no |
Address and port of the HTTP listener, for example |
|
string |
- |
yes |
Secret the browser session key is derived from (PBKDF2-HMAC-SHA256, 600000 iterations, fixed salt). The key
encrypts the session cookie with AES-256-GCM-SIV. Changing it signs every browser out. Use a long random value,
for example |
|
string |
absent |
for two-factor authentication |
Secret that authenticator app and YubiKey secrets are encrypted with (AES-256-GCM-SIV, key derived with
HMAC-SHA256). Without it, users can't set up a second factor. Changing or removing it makes existing authenticator
apps and YubiKeys unusable; affected users can still sign in with a recovery code or a passkey. Keep it separate
from |
|
object |
absent |
no |
Lets users add a YubiKey by touching it once, checked with Yubico's YubiCloud service, see
YubiCloud. Doesn't need |
|
object |
absent |
in practice, yes |
Where freshfront runs and how hooks authenticate, see Frontend. Without it, every path the backend does
not handle answers |
|
object |
absent |
for production |
SMTP delivery, see Email. Without it, emails are only written to the log. |
|
object |
absent |
no |
Enables the LDAPS listener, see LDAP. |
YubiCloud
Most YubiKeys ship with a Yubico OTP credential in slot 1 whose secret only Yubico knows. With yubicloud
configured, users register such a key by touching it once, and every code is checked with Yubico's validation
service (protocol 2.0). Without it, users can still program their own slot and have the server check codes itself,
which needs otp_secret.
| Key | Type | Default | Required | What it does |
|---|---|---|---|---|
|
string |
- |
yes |
Client ID from https://upgrade.yubico.com/getapikey/. |
|
string |
- |
yes |
API key from the same page (base64). Requests and responses are signed with it (HMAC-SHA1); a response with a wrong signature, nonce or code is treated as the service being unavailable. |
|
URL |
|
no |
Validation endpoint, for a self-hosted validation server. Must be |
Things to know before turning it on:
-
Yubico sees the key's public ID and the time of every sign-in and registration that uses it, and the backend's IP address.
-
When the service can't be reached, users with only a YubiCloud key can't pass two-factor authentication; they can use a recovery code or a passkey. The sign-in page tells them the service is unavailable, and the cause is logged.
-
The backend needs outbound HTTPS to the validation endpoint. Requests time out after 10 seconds.
-
A code accepted by YubiCloud is spent for every site using YubiCloud, so a code typed into another site can't be replayed here.
Frontend
| Key | Type | Default | Required | What it does |
|---|---|---|---|---|
|
string |
- |
yes |
|
|
string |
absent |
no |
Bearer token sent as |
identity, token issuance for claims, sending for the email hooks. If you set
hooks_secret, make sure freshfront has the same HOOKS_SECRET. See Hooks.pykrete sends two emails: email address verification links, and organization invitations addressed to an email address. The backend sends them over SMTP; freshfront renders the content through the email hooks.
| Key | Type | Default | Required | What it does |
|---|---|---|---|---|
|
string |
- |
yes |
Sender, a bare address or |
|
URL |
- |
yes |
SMTP server and credentials. Always give the port. Percent-encode special characters in the user name and
password ( |
email.smtp forms:
| URL | Connection |
|---|---|
|
TLS from the first byte |
|
STARTTLS, fails if the server doesn't offer it |
|
STARTTLS when offered, otherwise plain text |
|
Plain text, for a relay on the same host only |
The plain-text forms send the password unencrypted. Server certificates are checked against a bundled copy of the Mozilla root store, not the system store.
Behavior:
-
Without
email, nothing is sent. The backend logs each email atinfolevel ("email delivery is not configured"), including its link: the rendered subject and text when hooks are enabled, the raw data otherwise. Use this only for development. -
With
email, hooks must be enabled, because the content comes from freshfront. Otherwise every email fails with "sending email requires frontend hooks to render it". -
Email is sent while the request waits. If rendering or SMTP fails, the verification link or invitation is discarded and the user sees an error. There is no queue and no retry.
LDAP
| Key | Type | Default | Required | What it does |
|---|---|---|---|---|
|
socket address |
- |
yes |
Address and port to listen on, for example |
|
string |
- |
yes |
Directory root, a non-empty DN such as |
|
path |
- |
yes |
PEM certificate chain for the listener. |
|
path |
- |
yes |
PEM private key for that certificate. |
The listener speaks LDAPS only: TLS starts with the first byte, there is no plain LDAP and no StartTLS. Add, modify, delete, rename and compare requests are refused. Directory layout and binds are in LDAP.
Keys that are ignored
Older config files contain keys that the current backend does not read:
| Key | Current equivalent |
|---|---|
|
|
|
|
|
the |
|
none; SAML is not supported |
Environment variables
Backend
| Variable | Required | What it does |
|---|---|---|
|
yes, unless |
PostgreSQL URL. |
|
yes |
Path to the PKCS#11 module, for example |
|
yes |
Slot id, decimal or |
|
for SoftHSM2 and most tokens |
User PIN. If unset, the backend calls |
|
no |
Prepended to every key id in the token ( |
|
no |
Log filter (tracing |
|
for SoftHSM2 |
Read by SoftHSM2 itself, not by pykrete: path to its config file. |
freshfront
| Variable | Default | What it does |
|---|---|---|
|
unset: TCP port 8000 on all interfaces |
Listen on a unix socket at this path instead. A stale socket file is removed at start. |
|
|
Base URL of the backend for server-side calls: discovery, code exchange for the dashboard, GraphQL. |
|
unset: every |
Must equal the backend's |
|
unset |
Used by the bundled |
Fixed values
These are not configurable:
| What | Value |
|---|---|
Database pool |
at least 1 idle connection, 400 ms connection timeout |
Access token and ID token lifetime |
3600 s (the database keeps access tokens 120 s longer for clock skew) |
Authorization code lifetime |
600 s |
Refresh token chain lifetime |
30 days |
Device code |
1800 s; poll interval 5 s |
Backchannel (CIBA) request |
600 s by default, |
Email verification link |
24 h, resend allowed after 1 minute |
Organization invitation (default) |
7 days |
|
2 MiB plus 64 KiB |
Hook call |
5 s timeout, 1 MiB response limit |
15 s SMTP timeout, 20 s per message |
|
Second factor |
5 wrong codes per sign-in, 1 s delay after each; TOTP accepts the previous and next 30 s step; authenticator app setup valid 15 minutes; 10 recovery codes; YubiCloud requests time out after 10 s |
LDAP |
1000 entries per search, 128 KiB per message, 10 s TLS handshake, 300 s idle |
Example
/etc/pykrete/config.json:
{
"domain": "auth.infra.delta.rocks",
"tls_terminated": true,
"session_secret": "<session-secret>",
"otp_secret": "<otp-secret>",
"yubicloud": {
"client_id": "<client-id>",
"api_key": "<api-key>"
},
"frontend": {
"upstream": "unix:/run/pykrete/freshfront.sock",
"hooks_secret": "<hooks-secret>"
},
"email": {
"from": "pykrete <noreply@example.com>",
"smtp": "smtps://noreply%40example.com:<smtp-password>@mail.example.com:465"
},
"ldap": {
"listen": "0.0.0.0:636",
"base_dn": "dc=example,dc=com",
"certificate": "/etc/pykrete/ldap/fullchain.pem",
"private_key": "/etc/pykrete/ldap/privkey.pem"
}
}
Backend environment and command:
DATABASE_URL=postgres://pykrete@%2Frun%2Fpostgresql/pykrete
SOFTHSM2_CONF=/var/lib/pykrete/softhsm2.conf
PYKRETE_PKCS11_MODULE=/usr/lib/softhsm/libsofthsm2.so
PYKRETE_PKCS11_SLOT=<slot>
PYKRETE_PKCS11_PIN=<user-pin>
RUST_LOG=info
pykrete --config /etc/pykrete/config.json
freshfront environment:
FRESHFRONT_SOCKET=/run/pykrete/freshfront.sock
AUTH_BACKEND=http://127.0.0.1:3000
HOOKS_SECRET=<hooks-secret>
The backend user must be able to connect to the socket that freshfront creates.
Keys and secrets
Keys in the PKCS#11 token
All asymmetric keys are generated and used inside the PKCS#11 token; the backend only reads their public parts.
They are created on startup when missing, as persistent token objects whose CKA_ID and CKA_LABEL are
<PYKRETE_PKCS11_KEY_PREFIX><key id>:
| Key id | Type | Used for |
|---|---|---|
|
RSA 2048 |
|
|
EC P-256 |
|
|
EC P-256 |
Refresh tokens: JWE |
The token has to support CKM_RSA_PKCS_KEY_PAIR_GEN, CKM_EC_KEY_PAIR_GEN (P-256), CKM_SHA256_RSA_PKCS,
CKM_ECDSA and CKM_ECDH1_DERIVE.
On every start the public keys are copied into the jwk_keys table (rows without an authority are deleted
first) and published at /jwks and /.well-known/jwks.json. The JWKS therefore lists all three keys; the
refresh-token key has "use": "enc".
SoftHSM2
export SOFTHSM2_CONF=/var/lib/pykrete/softhsm2.conf
mkdir -p /var/lib/pykrete/tokens
echo "directories.tokendir = /var/lib/pykrete/tokens" > "$SOFTHSM2_CONF"
softhsm2-util --init-token --free --label pykrete --pin <user-pin> --so-pin <so-pin>
softhsm2-util --show-slots
--init-token prints the slot the token was reassigned to; that number is PYKRETE_PKCS11_SLOT. The backend
needs read and write access to the token directory (it generates keys there on first start).
Hardware tokens
Any PKCS#11 module works if it supports the mechanisms above. For example, an OpenSC-supported smartcard: module
opensc-pkcs11.so from OpenSC, slot 0, a key prefix of pykrete-, and the card's PIN.
Rotation and backups
Key rotation is not supported. Don't remove the keys from the token: the backend generates new ones at the next start, tokens signed with the old keys stop validating at relying parties, and refresh tokens can no longer be decrypted.
Back up the token storage (for SoftHSM2, the tokendir) together with the database.
Other secrets
| Secret | Where |
|---|---|
Session cookie key |
derived from |
Hooks secret |
config file and freshfront's environment |
SMTP password |
config file ( |
OTP secret |
config file ( |
YubiCloud API key |
config file ( |
Authenticator app and self-programmed YubiKey secrets |
|
YubiCloud keys |
|
Recovery codes |
|
Client secrets ( |
|
API keys ( |
|
App passwords (LDAP) |
|
Passwords |
|
Registration codes |
|
Email verification and invitation codes |
|
LDAP TLS key |
the file in |
Treat database dumps and backups as secret.