Kiln uses four credential types with separate responsibilities. Public endpoints require no credentials. Login sessions are for the admin console, administrator API tokens are for scripts and automation, and path-based playback keys are for players and playlist distribution. Their privileges do not overlap, and one credential type cannot become another.
Credentials at a glance
| Credential | Shape | Used for | Lifetime |
|---|---|---|---|
| Public endpoint | No credential | Health checks, metrics, EPG, logos | Not applicable |
| Session JWT | Authorization: Bearer <jwt>, Ed25519 signed |
Admin console, interactive debugging | token_ttl_hours (24 by default); invalidated when credentials change |
| Administrator API token | kiln_v1_ prefix plus 48 random characters |
Scripts, CI, external tooling | Expiration set at creation; rotatable and revocable |
| Playback key | v1 prefix plus 126 random characters, carried in the URL path |
Players and playlist distribution | Expiration set at creation; revocable |
Users and roles
Users live in the [[auth.users]] array of the config file. At least one is required or startup validation fails. Every user needs username, password_hash, and role, and usernames must be unique.
[[auth.users]]
username = "admin"
password_hash = "$2a$10$..."
role = "admin"
[[auth.users]]
username = "operator"
password_hash = "$2a$10$..."
role = "viewer"
channel_ids = ["demo-hls", "demo-dash"]role = "admin" grants everything and is also the gate to the admin console: after login the console re-checks /v1/me and sends any non-admin role back to the login page. Other roles are constrained by channel_ids and only see and play the channels listed there, across the channel list, the playlist, and the status endpoint. An empty channel_ids means no channel restriction.
Passwords are stored as bcrypt hashes, never in plaintext. To generate one:
make hash PASSWORD='your-password'go run scripts/hash-password.go 'your-password'Session JWTs
POST /v1/auth/login exchanges a username and password for an Ed25519-signed JWT. The response also includes the expiration time, username, and role.
curl -s http://127.0.0.1:8080/v1/auth/login \
-H 'content-type: application/json' \
-d '{"username":"admin","password":"your-password"}'Signing keys
The algorithm is fixed to EdDSA. The private key is resolved in order, first match wins:
Inline PEM in config
auth.token_private_key, or the KILN_TOKEN_PRIVATE_KEY environment variable.
Key file path
auth.token_private_key_file, or the KILN_TOKEN_PRIVATE_KEY_FILE environment variable.
Auto-managed in the data directory
With neither of the above, Kiln reads {data_dir}/auth/ed25519.pem. If the file is missing it generates a fresh pair, writes the private key with mode 0600, and stores the public key at {data_dir}/auth/ed25519.pub.pem.
The public key is optional and is derived from the private key when omitted. If you do supply token_public_key or token_public_key_file, Kiln verifies that it matches the private key and refuses to start otherwise. At least one of the private key, the private key file, or server.data_dir must be present, or config validation fails.
To generate a pair by hand:
go run scripts/gen-jwt-keys.go ./secrets # writes ed25519.pem / ed25519.pub.pemValidation and rate limiting
- Issued tokens carry
iss,aud,exp,iat,nbf,sub, and a unique randomjti. All time-based assertions are enforced on verification, with 30 seconds of clock skew allowed. token_issuerandtoken_audienceboth default tokilnand must match at verification time. Changing either invalidates every outstanding token.token_ttl_hoursdefaults to 24 and falls back to 24 hours for any value at or below zero.- Login attempts are rate limited per client IP.
login_rate_per_mindefaults to 20 and excess attempts return 429. The credential-change endpoint has a separate limiter keyed by username and IP. - Preview tokens issued by the console are a restricted session: bound to a single channel, expiring in five minutes by default, and explicitly rejected by every management endpoint.
Administrator API tokens
Session JWTs belong in a browser; scripts should not borrow one. Issue a dedicated credential instead, under Settings → Admin API Tokens in the console.
Issuing and storing
The plaintext is kiln_v1_ followed by 48 random characters and is shown exactly once, at creation or rotation. The server keeps only its SHA-256 digest plus a short recognizable prefix, so no endpoint can reveal the plaintext again. If you lose it, rotate.
curl -s http://127.0.0.1:8080/v1/admin/channels \
-H "authorization: Bearer kiln_v1_..." | jqThe four scopes
Scopes are independent, and none implies another. Grant only what the job needs:
read
Read channels, settings, egress configuration, and logs.
write
Create and modify configuration, including M3U import and export, but never delete.
delete
Delete channels and egress entries, revoke playback keys, clear access logs.
refresh
Probe sources, refresh the guide, warm up and preview channels, stop sessions, test egress routes.
Expiration, rotation, and revocation
- Expiration is set at creation and can be adjusted later, up to ten years. Leave it unset for a token that never expires.
- Rotation keeps the name, scopes, and expiration and replaces only the secret. The old value stops working immediately, and the new plaintext value is shown once. Use rotation when replacing a leaked credential.
- Revocation is permanent. Every subsequent request with that token returns 401.
- Deleting the record invalidates the token and removes it from the list, but the prefix snapshots already written to the audit log are preserved.
- Edit, rotate, revoke, and delete all require an
If-Matchheader carrying the currentrevision. A missing or stale value returns 409, so two admin sessions cannot silently overwrite each other.
Audit log
Every request made with an administrator API token is recorded, allowed and denied alike: token prefix, method and path, required scope, decision and denial reason, HTTP status, client address, user agent, and request ID. The console shows the most recent calls directly below the token list.
Denial reasons are a fixed set, which makes them easy to act on:
| Reason | Status | Meaning |
|---|---|---|
revoked |
401 | The token was revoked or disabled |
expired |
401 | The token has passed its expiration date |
session_required |
403 | The route accepts login sessions only |
route_not_available |
403 | The route is not registered for API tokens |
missing_scope |
403 | The token lacks the scope this route requires |
Boundaries
Changing login credentials
PUT /v1/me/credentials changes the username or the password and accepts an administrator login session only; API tokens are rejected. The request must include the current password. New passwords are 8 to 72 bytes, and usernames are at most 64 characters with no control characters.
A successful save does three things: it stores the new credentials as an override, increments the account’s credential revision, and returns a fresh session token. The revision bump invalidates every previously issued session, signing out other devices. In the console the same action lives under System Settings, in the account card or the account menu in the top-right corner.
What [security] controls
[security]
play_require_auth = true
allowed_hosts = []
# cors_origins = ["http://127.0.0.1:5173"]
# public_hosts = ["kiln.lan", "origin.example.com", "localhost"]
max_playlist_bytes = 8388608
max_body_bytes = 1048576| Key | Effect |
|---|---|
play_require_auth |
Whether playback endpoints require a credential. On by default; turning it off makes everything under /v1/play/ world-readable and is for local debugging only. KILN_PLAY_OPEN=1 is the same switch |
allowed_hosts |
Explicit outbound exemptions for origin hostnames and IPs that resolve to loopback or private addresses. Upstream and channel declarations do not grant this exemption |
public_hosts |
Inbound allowlist matched against the request Host header. When non-empty, anything outside the list gets 403, except loopback health checks. Empty means unrestricted |
cors_origins |
Origins allowed to call the API cross-origin. Empty means same-origin only, and no CORS headers are emitted at all |
max_playlist_bytes |
Read ceiling for a single upstream playlist, 8 MiB by default, so an oversized manifest cannot exhaust memory |
max_body_bytes |
Read ceiling for admin request bodies, 1 MiB by default; import endpoints are allowed a multiple of it |
Administrative responses carry X-Content-Type-Options, Referrer-Policy, X-Frame-Options: DENY, a strict Content-Security-Policy, and a no-store cache policy. EPG, logo, and immutable media responses use endpoint-specific caching. The frame policy prevents the console from being embedded in a third-party page.
Playback keys
A playback key is a separate credential for players and playlist distribution. It travels in the URL path (/p/{token}/...), can be limited to a subset of channels with an optional expiration date, and can be revoked at any time. Every use is written to the playback access log, with only the key prefix retained in the recorded path. Playback keys are isolated from login credentials, so sharing a playback URL does not grant administrative access.
Creation, revocation, and the access log all live under Access Control in the console. See Playback and distribution.
Production hardening checklist
Replace the sample password
The admin / admin pair in the example config exists only for the first launch. Generate a new bcrypt hash with make hash and replace password_hash, or change it from the console after logging in.
Pin the signing key
In production, set token_private_key_file or KILN_TOKEN_PRIVATE_KEY_FILE explicitly and fold the key into your backup and rotation process. A key that Kiln auto-generated disappears with its data directory, and every session dies with it.
Keep play_require_auth on
Make sure no stray KILN_PLAY_OPEN=1 survives in the environment. Distribute playback keys instead of disabling playback authentication.
Tighten the host allowlists
Use public_hosts to pin the domains served. Add a hostname or IP to allowed_hosts only when Kiln must reach a loopback or private origin; channel configuration does not replace this step.
Give automation its own token
Scripts should always use an administrator API token with the minimum scopes for the job, an expiration date, periodic rotation, and audit logging.
Full value ranges live in the configuration reference, and the overrides in environment variables.