edge reads one HOCON file at startup. This page lists its keys, says which secret each one holds and where that secret comes from, and describes what makes edge refuse to start. For loading rules, environment variables and the blocks shared with auth and central, see Configuration reference.
versola configure and the Helm chart generate this file for the release you deploy, so you rarely write it by hand. On the vps target a hand edit is lost on the next configure.
Loading and environment variables
The file path comes from CONFIG_PATH (image default /app/config/env.conf). ENV_CONFIG can hold the whole file inline instead. Keys are kebab-case, durations are HOCON durations ("60 seconds", 5 minutes), and ${VAR} substitution works.
| Variable | Default | Meaning |
|---|---|---|
PORT | 8080 | Application port |
DPORT | 8081 | Diagnostics port: /metrics, /liveness, /readiness |
BIND_HOST | 0.0.0.0 | Bind address |
CONFIG_PATH | /app/config/env.conf | Path of the HOCON file |
RUN_MIGRATIONS | false | true applies migrations at startup. Otherwise edge only validates the schema and stops on an out-of-date one |
WARMUP_ENABLED, WARMUP_BUDGET_SECONDS | true, 20 | Warm-up before readiness |
POSTGRES_POOL_METRICS_INTERVAL | none | Overrides postgres.pool-metrics-interval |
edge does not read APORT or MPORT.
Keys
“Required” means the key has no default and edge does not start without it.
Top level
| Key | Required | Default | Meaning |
|---|---|---|---|
env | yes | Environment name. prod enables production checks and makes /service/configuration/sync answer 404 | |
id | yes | This edge’s ID. It must match the edge registered in central (Edges in the console). Generated: edge-default | |
key-id | yes | kid of the key below, sent in the header of the token edge signs for central | |
private-key | yes | RSA private key, PKCS#8, base64. edge signs its sync calls to central with it (RS256). central holds the public half | |
configuration-cache-refresh-interval | yes | How often edge reloads resources, endpoints, permissions, roles, clients, presets, JWKS and DPoP policy from central. Generated: 5 minutes for vps and Kubernetes, 1 minute locally | |
versola-url | yes | Public URL of auth. The browser is redirected here, and a token’s iss is compared with it | |
versola-internal-url | no | versola-url | URL edge itself uses to call auth. Differs from versola-url when edge and the browser reach auth over different networks |
versola-internal-trusted-certificates | no | empty | Pins for the certificate presented by the HTTPS endpoint edge calls when a client authenticates with a certificate and versola-internal-url is https (a TLS terminator in front of auth). Absent: such calls are refused. With a plaintext versola-internal-url and a native block, those calls go to native.auth-mutual-tls-url and are checked against native.trusted-certificates. See Pinned certificates |
edge-url | yes | Public origin clients use to reach edge. The htu of a DPoP proof is built from it, never from the Host header | |
central.url | yes | Base URL of the Central API | |
otel-exporter | no | absent | OTLP gRPC endpoint for traces. Absent: tracing off |
security
| Key | Required | Meaning |
|---|---|---|
token-encryption.key | yes | 32 bytes, base64url. AES key for the refresh tokens in the edge session table. Losing or changing it makes every stored session fail to refresh |
edge-sessions.secret | yes | 32 bytes, base64url. Must be present; this release does not read it |
edge-sessions.ttl | yes | Duration. Must be present; this release does not read it. Cookie lifetime follows the refresh token. Generated: 30 days |
internal-secret | no | Base64url. Password of the non-prod POST /service/configuration/sync, with user edge. Absent: that endpoint stays unreachable |
postgres
Same block as in auth and central: see Configuration reference. edge needs its own database, or its own schema in a shared one (the CLI targets use a shared database with ...?currentSchema=edge). edge listens for revocations on a dedicated connection, so behind a pooler in transaction mode set notifications-url. Generated values for edge: maximum-pool-size = 10, minimum-idle = 10.
cleanup
Deletes expired rows. Generated for edge, with max-threads = 2:
| Table | Batch size | Interval | Key column |
|---|---|---|---|
pending_logins | 1000 | 5 minutes | state |
edge_sessions | 500 | 1 hour | ctid |
revocations | 1000 | 1 hour | revoked_key |
revocation
Optional block. The defaults below are in code.
| Key | Default | Meaning |
|---|---|---|
reload-interval | 600 seconds | How often the in-memory revocation list is reconciled with the database, with jitter. Revocations normally arrive by notification, so this is the backstop |
purge-interval | 60 seconds | How often entries for expired tokens are dropped |
overlap | 30 seconds | How far a reconcile re-reads behind its last position, to catch rows that committed late |
batch-size | 50000 | Rows per read |
dpop
Optional block. Without it, edge refuses requests with the DPoP scheme, but a key-bound token over Bearer is still refused. A native block requires it. The accepted algorithms come from the server metadata in central, and whether a nonce is required is the Require DPoP nonce setting on the edge in Edges; neither is configured here.
| Key | Required in the block | Meaning |
|---|---|---|
nonce-salt | yes | 32 bytes, base64url. Keys edge’s own DPoP-Nonce space. Not shared with auth: a nonce from auth is not valid at edge |
iat-leeway | yes | Maximum distance of a proof’s iat from now, either direction. It also sizes the replay guard. Generated: 60 seconds |
nonce-ttl | yes | How long an issued nonce is accepted. Generated: 600 seconds |
native
Optional block. Absent: every /native/* call answers 404. See Native apps for the flow.
| Key | Required | Default | Meaning |
|---|---|---|---|
auth-mutual-tls-url | yes | How edge reaches the mutual-TLS listener of auth (MPORT) | |
auth-mutual-tls-external-url | no | auth-mutual-tls-url | What auth calls that listener (its mutual-tls.external-url). The htu of a device proof for /token and /revoke is built from it |
trusted-certificates | yes | List of PEM paths: the leaf certificate(s) the listener may present. At least one | |
blob-key | yes | 32 bytes, base64url. AES-256-GCM key that seals the blob /native/start returns. Its own key, separate from token-encryption.key | |
blob-ttl | no | 600 seconds | How long a blob can be redeemed. Also capped by the lifetime of the pushed request at auth |
auth-issuer | no | versola-url | The iss that auth’s authorization responses carry (its jwt.issuer). Set it when that differs from versola-url, or /native/complete refuses the response |
Secrets and where they come from
On vps and the local target, versola configure generates each secret, stores it in OpenBao under secret/versola/<target>/edge and writes it into the container environment as a ${VAR} that the HOCON file references. On Kubernetes the same keys come from a Secret (Kubernetes). See Secrets.
| Variable | Config key | Shape | Notes |
|---|---|---|---|
EDGE_PRIVATE_KEY | private-key | RSA PKCS#8 | Stored with EDGE_KEY_ID. The public half, EDGE_PUBLIC_JWK, goes to central. A new pair on a live system makes every edge call to central fail with 401 |
EDGE_KEY_ID | key-id | text | Must equal the kid of the key registered in central |
EDGE_TOKEN_ENC_KEY | security.token-encryption.key | 32 bytes | Generated on first install only. A missing value on upgrade is an error, not a silent regeneration |
EDGE_SESSIONS_SECRET | security.edge-sessions.secret | 32 bytes | |
EDGE_INTERNAL_SECRET | security.internal-secret | any length | |
EDGE_DPOP_NONCE_SALT | dpop.nonce-salt | 32 bytes | |
EDGE_NATIVE_BLOB_KEY | native.blob-key | 32 bytes | |
POSTGRES_PASSWORD | postgres.password | text |
A 32-byte secret is URL-safe base64 of exactly 32 bytes. A different length stops startup.
Startup validation
edge refuses to start, with a message naming the key, when:
- a required key is missing, a secret has the wrong length, or
private-keyis not a PKCS#8 RSA key; - a path in
versola-internal-trusted-certificatesornative.trusted-certificatesholds no certificate, or holds a certificate authority. Every certificate in every file is checked, and only leaf certificates pass; native.trusted-certificatesis empty;- a
nativeblock exists without adpopblock; - a
trusted-certificatesvalue is a bare string. It must be a list; - the database schema is out of date and
RUN_MIGRATIONSis nottrue; env = "prod"and a non-loopback Postgres URL does not verify TLS;- it cannot read the revocation list from the database. A replica with no list would accept revoked tokens, so it stays out of service.
Annotated minimal example
This is a vps-style file with the optional blocks left out except dpop. Secrets are placeholders resolved from the environment.
env = prod
id = "edge-default" # same ID as the edge in central
key-id = ${EDGE_KEY_ID}
private-key = ${EDGE_PRIVATE_KEY} # PKCS#8 RSA, base64
configuration-cache-refresh-interval = "5 minutes"
versola-url = "https://id.example.com" # what browsers and token iss use
edge-url = "https://id.example.com" # DPoP htu is built from this
central { url = "http://127.0.0.1:8090" }
security {
token-encryption { key = ${EDGE_TOKEN_ENC_KEY} }
edge-sessions {
secret = ${EDGE_SESSIONS_SECRET}
ttl = 30 days
}
}
postgres {
url = "jdbc:postgresql://db.example.com:5432/versola?currentSchema=edge"
user = "versola"
password = ${POSTGRES_PASSWORD}
maximum-pool-size = 10
minimum-idle = 10
connection-timeout = "30 seconds"
max-lifetime = "30 minutes"
leak-detection-threshold = "60 seconds"
}
cleanup {
max-threads = 2
tables = [
{ table-name = "pending_logins", batch-size = 1000, interval = "5 minutes", key-column = "state" }
{ table-name = "edge_sessions", batch-size = 500, interval = "1 hour", key-column = "ctid" }
{ table-name = "revocations", batch-size = 1000, interval = "1 hour", key-column = "revoked_key" }
]
}
dpop {
nonce-salt = ${EDGE_DPOP_NONCE_SALT}
iat-leeway = "60 seconds"
nonce-ttl = "600 seconds"
}
Add a native block only if you serve native apps:
native {
auth-mutual-tls-url = "https://id.example.com:8083"
trusted-certificates = ["/certs/auth-mtls.crt"]
blob-key = ${EDGE_NATIVE_BLOB_KEY}
}
Pinned certificates
versola-internal-trusted-certificates and native.trusted-certificates are lists of PEM file paths. Each entry is a pin on a leaf certificate, not a trust anchor. The HTTP client edge uses does no host-name verification, so trusting a CA would accept any certificate that CA ever issued, for any host. That is why a CA is refused at startup.
native {
trusted-certificates = ["/certs/auth-mtls-old.crt", "/certs/auth-mtls-new.crt"]
}
versola-internal-trusted-certificates = ["/certs/auth-internal.crt"]
A server certificate that matches any entry is accepted. To rotate the certificate of auth’s listener without an outage:
- Put the old and the new leaf in the list on every
edgeand restart them. - Switch
authto the new certificate and restart it. - When every
edgehas the new list, remove the old entry.
edge reads the files at startup, so a changed file needs a restart. The old form trusted-certificates = "/path" no longer parses; wrap it in [...].
Next steps
- Edge client certificates: how
edgegets the certificate it presents toauth. - Secrets: where the generated values live.
- Kubernetes: the same keys in the Helm chart.