Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Secure configuration (rolter init and rolter check)

rolter has two deployment paths, and they are deliberately not converging.

Local (rolter easy-up) is frictionless and may stay loose on security: no database, no provider keys, no secrets at all, answering immediately on the built-in fake-llm model. It prompts for nothing, and rolter check does not change that.

Production is guided and should be hard to misconfigure. rolter check validates a production deployment before the process starts, so a misconfiguration fails loudly instead of starting degraded.

rolter init                       # generate a production config and its secrets
rolter check                      # validate the current environment
rolter check --config rolter.toml # also parse and validate the config file
rolter check --strict             # treat warnings as failures too
rolter check --connect            # also probe that the datastores accept a connection

The two are a pair. check tells an operator what is wrong; init tells them what to write. A test in the tree renders a fresh production environment and runs the full check suite over it, so what the generator emits is exactly what the gate accepts — if they ever drift, an operator would follow the documented path and still be told their deployment is broken.

It exits non-zero when anything fatal is found, which makes it usable as a container entrypoint step, a Kubernetes init container, or a Helm pre-install hook.

Why this exists

The motivating case is worth stating plainly, because it is the shape of every problem this command is meant to catch.

Provider credentials are encrypted at rest with a key-encryption key read from ROLTER_KEK. When that variable is missing, the control plane’s default-provider seed does not fail — it logs a warning and stores the inline api_key unsealed. Nothing goes red. The deployment serves traffic normally. A credential the operator believed was encrypted at rest simply is not, and there is no signal saying so.

Until recently several docs pages — including the production deployment guide and .env.example — told operators to set ROLTER_MASTER_KEY, a variable nothing in the codebase has ever read. An operator who followed those instructions exactly ended up in precisely that degraded state. Those pages are now corrected, and rolter check reports the mistake by name if the old variable is still set anywhere.

That is the class of failure this gate targets: not a crash, but a deployment that looks healthy while quietly not doing what it was configured to do.

What it checks

CheckSeverityWhy
ROLTER_KEK presenterrorWithout it provider credentials are stored unsealed, with only a warning
ROLTER_KEK length ≥ 16errorThe KEK is stretched through SHA-256, so a weak secret still yields a valid — and brute-forceable — key
ROLTER_ADMIN_TOKEN presenterrorOtherwise the management API and /internal/snapshot are unauthenticated
ROLTER_DATABASE_URL present and postgres://errorOtherwise the control plane falls back to an in-memory store and loses all config on restart
No example values surviveerrorThe example database credentials and the e2e throwaway KEK are published in the repository
ROLTER_REDIS_URL presentwarningWithout it rate limits and budgets are enforced per replica, not per deployment
Control plane not bound to 0.0.0.0warningThe management API should not be reachable on every interface
ROLTER_KEY_PEPPER presentwarningWithout it a leaked virtual-key digest is usable as-is against this deployment
CORS does not allow *errorThe dashboard is served same-origin; a wildcard lets any page a logged-in operator visits drive the management API as them
Datastores accept a connection (with --connect)error / warningA URL that parses but resolves to nothing fails at first use rather than at rollout
Config file parses (with --config)errorA config that fails to load leaves the gateway on whatever it last had

--connect is opt-in because a check that opens sockets cannot be the default for a command meant to run offline and without side effects. It is a TCP connect, not a protocol handshake: it needs no database driver and answers the question that actually goes wrong during a rollout — the name does not resolve, or nothing is listening. A datastore that accepts TCP but rejects the credentials is a different failure, and one the process reports loudly on its own.

Redis and the bind address are warnings rather than errors because a single-replica deployment behind an ingress is legitimately fine without either. Use --strict in an environment where they are not.

Anything resembling credentials in a URL is redacted before printing, since this output routinely lands in CI logs.

Generating the configuration (rolter init)

rolter check failing on a missing ROLTER_KEK and suggesting openssl rand -hex 32 still leaves an operator to invent the rest of the file around it. rolter init writes it.

rolter init                                  # rolter.toml + .env, production profile
rolter init --profile local                  # the loose defaults, for development
rolter init --print                          # stdout only, for piping into a secret manager
rolter init --env-only --database-url ...    # just the environment

It generates four distinct secrets — the KEK, the admin token, the internal token and the virtual-key pepper — each 256 bits from the system CSPRNG. They are distinct by construction and a test asserts it: reusing one value in two roles would mean a leaked admin token also decrypts every stored credential.

Nothing here is interactive. A generator that prompts cannot run in the Dockerfile, the Helm hook or the CI job where a production deployment is actually assembled, and an operator who has to answer six questions will hand-write a .env instead. Flags carry the choices and the defaults are the secure ones.

It will not overwrite without --force, and the refusal says why: regenerating ROLTER_KEK strands every credential encrypted under the old one. That failure is total, delayed, and looks like data corruption.

On unix the generated files are created 0600 at creation time rather than chmod’ed afterwards — a .env holding the KEK is a credential, and fixing the mode after the fact leaves a window.

What the profiles differ on

--profile production (default)--profile local
ROLTER_CONTROL_HOST127.0.0.1 — the management plane is an admin surface0.0.0.0
[server] require_authtrue — revoking the last key closes the data planeunset, defers to the deployment shape
Passes rolter checkyes, cleanlyno, and deliberately so

The data plane binds 0.0.0.0 under both: it is the public surface.

In a container

Run it as a pre-start step so the container never reaches a serving state while misconfigured:

CMD ["sh", "-c", "rolter check --strict && rolter control"]

Docker Compose

The bundled docker/docker-compose.yml is the local stack and is deliberately loose — example postgres credentials, no KEK, management plane wide open. Gating local bring-up on production rules would break the one path that is meant to have no friction, so the check lives behind a profile and never runs on docker compose up:

docker compose -f docker/docker-compose.yml --profile preflight \
               run --rm --env-file /path/to/production.env preflight

Kubernetes and Helm

The chart ships the init container already, enabled by default:

preflight:
  enabled: true
  strict: true   # a warning means the deployment works but is not what you meant
  connect: false # opt in to also probe the datastores

It renders with the exact env block the workload container gets — both come from one shared template, because three deployment paths that each re-implement “is this configured safely” will drift, and the one that drifts is the one nobody notices until a credential was stored unencrypted. A helm chart CI job lints the chart and renders every branch of that template.

If you are writing your own manifests, prefer an init container so the failure is visible as a distinct pod status rather than a crash-looping main container:

initContainers:
  - name: preflight
    image: ghcr.io/rolter-ai/rolter:latest
    command: ["/usr/local/bin/rolter", "check", "--strict"]
    envFrom:
      - secretRef:
          name: rolter-secrets