Edge configuration — Versola Docs
VersolaVersola/docs
Pre-1.00.7.1versola.kzGitHub

Edge configuration

Every key of the edge HOCON file with its default, the secrets behind each one, the checks edge makes at startup, an annotated minimal file and the pinned-certificate format.

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.

VariableDefaultMeaning
PORT8080Application port
DPORT8081Diagnostics port: /metrics, /liveness, /readiness
BIND_HOST0.0.0.0Bind address
CONFIG_PATH/app/config/env.confPath of the HOCON file
RUN_MIGRATIONSfalsetrue applies migrations at startup. Otherwise edge only validates the schema and stops on an out-of-date one
WARMUP_ENABLED, WARMUP_BUDGET_SECONDStrue, 20Warm-up before readiness
POSTGRES_POOL_METRICS_INTERVALnoneOverrides 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

KeyRequiredDefaultMeaning
envyesEnvironment name. prod enables production checks and makes /service/configuration/sync answer 404
idyesThis edge’s ID. It must match the edge registered in central (Edges in the console). Generated: edge-default
key-idyeskid of the key below, sent in the header of the token edge signs for central
private-keyyesRSA private key, PKCS#8, base64. edge signs its sync calls to central with it (RS256). central holds the public half
configuration-cache-refresh-intervalyesHow 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-urlyesPublic URL of auth. The browser is redirected here, and a token’s iss is compared with it
versola-internal-urlnoversola-urlURL edge itself uses to call auth. Differs from versola-url when edge and the browser reach auth over different networks
versola-internal-trusted-certificatesnoemptyPins 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-urlyesPublic origin clients use to reach edge. The htu of a DPoP proof is built from it, never from the Host header
central.urlyesBase URL of the Central API
otel-exporternoabsentOTLP gRPC endpoint for traces. Absent: tracing off

security

KeyRequiredMeaning
token-encryption.keyyes32 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.secretyes32 bytes, base64url. Must be present; this release does not read it
edge-sessions.ttlyesDuration. Must be present; this release does not read it. Cookie lifetime follows the refresh token. Generated: 30 days
internal-secretnoBase64url. 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:

TableBatch sizeIntervalKey column
pending_logins10005 minutesstate
edge_sessions5001 hourctid
revocations10001 hourrevoked_key

revocation

Optional block. The defaults below are in code.

KeyDefaultMeaning
reload-interval600 secondsHow often the in-memory revocation list is reconciled with the database, with jitter. Revocations normally arrive by notification, so this is the backstop
purge-interval60 secondsHow often entries for expired tokens are dropped
overlap30 secondsHow far a reconcile re-reads behind its last position, to catch rows that committed late
batch-size50000Rows 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.

KeyRequired in the blockMeaning
nonce-saltyes32 bytes, base64url. Keys edge’s own DPoP-Nonce space. Not shared with auth: a nonce from auth is not valid at edge
iat-leewayyesMaximum distance of a proof’s iat from now, either direction. It also sizes the replay guard. Generated: 60 seconds
nonce-ttlyesHow long an issued nonce is accepted. Generated: 600 seconds

native

Optional block. Absent: every /native/* call answers 404. See Native apps for the flow.

KeyRequiredDefaultMeaning
auth-mutual-tls-urlyesHow edge reaches the mutual-TLS listener of auth (MPORT)
auth-mutual-tls-external-urlnoauth-mutual-tls-urlWhat auth calls that listener (its mutual-tls.external-url). The htu of a device proof for /token and /revoke is built from it
trusted-certificatesyesList of PEM paths: the leaf certificate(s) the listener may present. At least one
blob-keyyes32 bytes, base64url. AES-256-GCM key that seals the blob /native/start returns. Its own key, separate from token-encryption.key
blob-ttlno600 secondsHow long a blob can be redeemed. Also capped by the lifetime of the pushed request at auth
auth-issuernoversola-urlThe 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.

VariableConfig keyShapeNotes
EDGE_PRIVATE_KEYprivate-keyRSA PKCS#8Stored 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_IDkey-idtextMust equal the kid of the key registered in central
EDGE_TOKEN_ENC_KEYsecurity.token-encryption.key32 bytesGenerated on first install only. A missing value on upgrade is an error, not a silent regeneration
EDGE_SESSIONS_SECRETsecurity.edge-sessions.secret32 bytes
EDGE_INTERNAL_SECRETsecurity.internal-secretany length
EDGE_DPOP_NONCE_SALTdpop.nonce-salt32 bytes
EDGE_NATIVE_BLOB_KEYnative.blob-key32 bytes
POSTGRES_PASSWORDpostgres.passwordtext

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-key is not a PKCS#8 RSA key;
  • a path in versola-internal-trusted-certificates or native.trusted-certificates holds no certificate, or holds a certificate authority. Every certificate in every file is checked, and only leaf certificates pass;
  • native.trusted-certificates is empty;
  • a native block exists without a dpop block;
  • a trusted-certificates value is a bare string. It must be a list;
  • the database schema is out of date and RUN_MIGRATIONS is not true;
  • 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:

  1. Put the old and the new leaf in the list on every edge and restart them.
  2. Switch auth to the new certificate and restart it.
  3. When every edge has 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