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

--database-url <URL>

string

-

yes (or DATABASE_URL)

PostgreSQL connection URL, for example postgres://pykrete:<password>@127.0.0.1/pykrete or postgres://pykrete@%2Frun%2Fpostgresql/pykrete for a unix socket.

--config <PATH>

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

domain

string

-

yes

Public host name, plus :port if it is not the default, for example auth.infra.delta.rocks or localhost:3000. The issuer is http[s]://<domain>/ and every URL in discovery, the WebAuthn relying party id (the host part) and origin, and the internal clients' redirect URIs are built from it.

tls_terminated

boolean

false

no

Generate https:// URLs. Set it to true whenever users reach the server over HTTPS; the backend itself never speaks TLS. Also makes freshfront mark its cookies Secure (it reads the scheme from the issuer).

listen

socket address

127.0.0.1:3000

no

Address and port of the HTTP listener, for example 127.0.0.1:3001. Keep it on a loopback or private address: the backend trusts the X-Real-IP header, so only the reverse proxy may reach it. freshfront's AUTH_BACKEND must point here.

session_secret

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 openssl rand -base64 48.

otp_secret

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 session_secret and back it up with the database. Use a long random value.

yubicloud

object

absent

no

Lets users add a YubiKey by touching it once, checked with Yubico's YubiCloud service, see YubiCloud. Doesn't need otp_secret.

frontend

object

absent

in practice, yes

Where freshfront runs and how hooks authenticate, see Frontend. Without it, every path the backend does not handle answers 502 Frontend unavailable, and hooks are off.

email

object

absent

for production

SMTP delivery, see Email. Without it, emails are only written to the log.

ldap

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

yubicloud.client_id

string

-

yes

Client ID from https://upgrade.yubico.com/getapikey/.

yubicloud.api_key

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.

yubicloud.url

URL

https://api.yubico.com/wsapi/2.0/verify

no

Validation endpoint, for a self-hosted validation server. Must be https; private addresses are allowed.

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

frontend.upstream

string

-

yes

http://<host>:<port> (plain HTTP only, no path or query; the port defaults to 80) or unix:<path> for a unix socket, for example unix:/run/pykrete/freshfront.sock. Pages and hook calls both go here.

frontend.hooks_secret

string

absent

no

Bearer token sent as Authorization: Bearer <hooks_secret> with every hook call. When absent the backend calls no hooks at all, and so can't render email (see Email). Must equal freshfront's HOOKS_SECRET.

Warning
A hook that fails (wrong secret, non-2xx answer, invalid JSON, more than 5 seconds) fails the operation that called it: sign-in for 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.

Email

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

email.from

string

-

yes

Sender, a bare address or Name <address>, for example pykrete <noreply@example.com>. Its domain is also used in Message-ID.

email.smtp

URL

-

yes

SMTP server and credentials. Always give the port. Percent-encode special characters in the user name and password (@ becomes %40).

email.smtp forms:

URL Connection

smtps://user:password@mail.example.com:465

TLS from the first byte

smtp://user:password@mail.example.com:587?tls=required

STARTTLS, fails if the server doesn't offer it

smtp://user:password@mail.example.com:587?tls=opportunistic

STARTTLS when offered, otherwise plain text

smtp://127.0.0.1:25

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 at info level ("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

ldap.listen

socket address

-

yes

Address and port to listen on, for example 0.0.0.0:636.

ldap.base_dn

string

-

yes

Directory root, a non-empty DN such as dc=example,dc=com.

ldap.certificate

path

-

yes

PEM certificate chain for the listener.

ldap.private_key

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

port

listen

ldap_port

ldap.listen

rs256_key

the oidc-token-signing-rs256 key in the PKCS#11 token

idp_key, idp_cert, saml_issuer

none; SAML is not supported

Environment variables

Backend

Variable Required What it does

DATABASE_URL

yes, unless --database-url

PostgreSQL URL.

PYKRETE_PKCS11_MODULE

yes

Path to the PKCS#11 module, for example libsofthsm2.so or opensc-pkcs11.so.

PYKRETE_PKCS11_SLOT

yes

Slot id, decimal or 0x-prefixed hex.

PYKRETE_PKCS11_PIN

for SoftHSM2 and most tokens

User PIN. If unset, the backend calls C_Login without a PIN, which only works with tokens that authenticate by other means.

PYKRETE_PKCS11_KEY_PREFIX

no

Prepended to every key id in the token (CKA_ID and CKA_LABEL), so several deployments can share a token.

RUST_LOG

no

Log filter (tracing EnvFilter syntax), for example info or pykrete=debug,info. Unset means errors only.

SOFTHSM2_CONF

for SoftHSM2

Read by SoftHSM2 itself, not by pykrete: path to its config file.

freshfront

Variable Default What it does

FRESHFRONT_SOCKET

unset: TCP port 8000 on all interfaces

Listen on a unix socket at this path instead. A stale socket file is removed at start.

AUTH_BACKEND

http://127.0.0.1:3000

Base URL of the backend for server-side calls: discovery, code exchange for the dashboard, GraphQL.

HOOKS_SECRET

unset: every /hooks/... request gets 404

Must equal the backend's frontend.hooks_secret.

NETBIRD_CLIENT_IDS, NETBIRD_AUTH_AUDIENCE, NETBIRD_DOMAIN_SUFFIX

unset

Used by the bundled claims hook for NetBird, see Organizations.

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, requested_expiry 60 to 3600 s; poll interval 5 s; at most 5 pending per client and user; binding_message up to 128 characters

Email verification link

24 h, resend allowed after 1 minute

Organization invitation (default)

7 days

/graphql request body

2 MiB plus 64 KiB

Hook call

5 s timeout, 1 MiB response limit

Email

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

oidc-token-signing-rs256

RSA 2048

RS256 signatures on ID tokens and access tokens

oidc-token-signing-es256

EC P-256

ES256 signatures, when a client (ID token) or audience (access token) selects ES256

oidc-refresh-encrypt-es256

EC P-256

Refresh tokens: JWE ECDH-ES + A256GCM. Encryption uses the public key in software; decryption derives the shared secret in the token (CKM_ECDH1_DERIVE).

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 session_secret at start, not stored

Hooks secret

config file and freshfront's environment

SMTP password

config file (email.smtp)

OTP secret

config file (otp_secret)

YubiCloud API key

config file (yubicloud.api_key)

Authenticator app and self-programmed YubiKey secrets

userdata_challenges, encrypted with otp_secret

YubiCloud keys

userdata_challenges, public ID only (the secret stays with Yubico)

Recovery codes

userdata_challenges (SHA-256)

Client secrets (DELTA-CLIENT-SECRET-...)

oauth_client_secrets

API keys (DELTA-API-KEY-...)

oauth_api_keys

App passwords (LDAP)

userdata_app_passwords

Passwords

userdata_challenges

Registration codes

userdata_challenges (password_pending)

Email verification and invitation codes

userdata_email_verifications, orgs_invitations

LDAP TLS key

the file in ldap.private_key

Treat database dumps and backups as secret.