Alpha. Vary is under active development and not ready for production use. Syntax, APIs, performance, and behaviour may change between releases.

Secrets and config

Via has two kinds of runtime settings. Plain config is for values that can appear in command output, deploy logs, or support bundles. Secrets are for values that should never travel through argv, shell history, generated manifests, or diagnostic JSON.

The config server delivers both kinds of values to running apps, but secret access has a narrower path: the app must declare the secret name, the runner must mint a workload token for that instance, and the config server must authorize the request before it reads the stored value.

Tracked components

After install, via status prints one line per component with the unit's state and bound endpoint.

Componentsystemd unitEndpoint
control-planevia-server.service127.0.0.1:8080
buildervia-builder.service(no listener)
runnervia-runner.service(no listener)
config-servervia-config.servicehttp://0.0.0.0:8181
proxymanaged proxy serviceloopback admin endpoint

Config server endpoint

Runtime containers use the config server to fetch declared configuration and secrets with workload identity. By default it binds on 0.0.0.0:8181 so runtime containers can reach it through the Docker host-gateway alias. The bind address comes from the [config_server] section of /etc/via/server.toml:

[config_server]
host = "0.0.0.0"
port = 8181

Workload identity JWT verification is the security boundary. Firewall port 8181 externally on production hosts; it is for host-to-container traffic, not a public API.

MethodPathPurpose
GET/healthLiveness probe. Returns 200 with body ok\n. No auth.
GET/config/<env>/<name>Returns the current secret value if the workload token authorizes it. Requires Authorization: Bearer <jwt> with aud = "via-config".
POST/v1/runtime/config/resolveReturns authorized env values and add-on descriptors for the running release. Requires the same workload audience.
POST/v1/runtime/config/refreshRefreshes selected env or descriptor keys by app/release/deploy/profile/scope/generation.
GET/v1/apps/<app>/config-server/policyOperator API for the app runtime-config policy: audience, env scopes, add-on scopes, descriptor policy, and refresh window.
PATCH/v1/apps/<app>/config-server/policyUpdates that policy by appending a new config profile.

The secret endpoint maps response codes to typed runtime-client errors:

StatusBody shapeRuntime mapping
200 OKraw value bytesconfig.secret returns the value
404 Not Foundmissing\nConfigMissingError
403 Forbiddendenied undeclared_secret <reason>ConfigUndeclaredError
403 Forbiddendenied <code> <reason>ConfigPermissionDeniedError
500 Internal Server Errorerror <reason>ConfigServerUnavailableError

Successful 200 responses set Cache-Control: no-store so intermediaries do not retain the value past the request.

Workload identity flow

When the runner promotes an ARTIFACT_READY deploy to RUNNING, it issues a per-instance signed JWT and bind-mounts it into the container. No secret value or long-lived credential is shared with the container.

StepAction
1Pre-generate the runtime instance id (rti_inst_<hex>).
2Mint a signed JWT under the active identity key with claims aud = "via-config", app, deploy, artifact, instance, secrets, iat, exp, jti.
3Write the JWT to a per-instance file under /var/lib/via/runners/identity/<instance-id>/token (mode 0600, dir mode 0700).
4Bind-mount that file into the container at /run/vary/identity/token read-only.
5Set bootstrap env: VARY_CONFIG_URL and VARY_CONFIG_TOKEN_PATH=/run/vary/identity/token.
6Append --add-host=via-config-host:host-gateway so the container resolves the host loopback over the docker bridge.
7Audit row: audit_events.action = runtime_identity_issued, target_type = runtime_instance.

Token bounds:

FieldDefaultCapNotes
TTL900s3600sRenewals overwrite the on-disk token in place with a fresh jti.
audvia-config-Build containers receive a different audience (vary-build); the config server rejects them.
issidentity issuer-Stamped by the active identity signing key.

Verification at the config server

Every /config/<env>/<name> request runs through a single authorization choke point:

CheckResult on failure
Signature, audience, expdenied token_invalid
App row existsdenied unknown_app
Runtime instance existsdenied unknown_instance
App id matches tokendenied app_mismatch
Deploy id matches tokendenied deploy_mismatch
Runtime is runningdenied instance_not_running
Deploy is runningdenied deploy_not_active
Name in secrets claimdenied undeclared_secret
Secret value lookup200 OK (raw value), 404 (missing), or 500 (error)

Every outcome (allowed, denied, missing, error) writes one audit_events row with action = config_secret_access, target_id = <secret name>, and flat JSON metadata carrying token id, app id, deploy id, instance id, environment, secret name, and (for denials) the typed code. Plaintext values are never serialized.

Declaring secrets in the app manifest

A runtime container is only allowed to fetch a secret whose name appears in [capabilities] secrets = [...]:

[capabilities]
secrets = ["DATABASE_URL", "STRIPE_KEY"]

Names not declared produce denied undeclared_secret regardless of what is stored. This gate is enforced at issuance (the secrets claim) and at the config server (membership check before the value lookup).

App env and config CLI

Use the HTTP target flow for app runtime configuration:

vary target add prod https://vary.example.com
vary login --target prod --name alice --token-stdin

vary app env list api --target prod
vary app env set api DATABASE_URL --target prod --secret
vary app env rotate api DATABASE_URL --target prod
vary app env history api DATABASE_URL --target prod
vary app env set api LOG_LEVEL --target prod --value info
vary app config patch api --target prod --replicas 2 --memory-mb 512
vary app config diff api --target prod
vary app config redeploy api --target prod

For one secret, prefer the masked prompt shown above. It keeps the value out of shell history, ps, terminal scrollback, and copied runbooks. In automation, redirect stdin from an existing secret file or use your CI secret store's masked-stdin facility:

vary app env set api DATABASE_URL --target prod --secret < /run/secrets/api-database-url

For several values, render a template, fill it in outside your shell history, apply it once, then delete the local copy:

vary app config template api --target prod --workdir . > api.prod.env
chmod 600 api.prod.env
$EDITOR api.prod.env
vary app config apply api --target prod --file api.prod.env
rm api.prod.env

The template uses KEY=value for plain config and secret:KEY=value for secrets. Treat a filled template like a password file. Do not commit it, paste it into tickets, or keep it in a shared shell transcript.

Avoid any command that places the secret value directly in the command text. Even if the CLI rejects argv secrets, the shell may have already recorded the line.

Secret env writes read from stdin or a masked TTY prompt. Secret values are not accepted in argv. Default output renders them as <redacted>; --json responses are also redacted. Rotate and history responses show generation, status, timestamps, source, and provider metadata only.

Secret provider metadata is provider-neutral:

FieldContract
providerOpaque string. Current local storage writes local_encrypted; clients must tolerate unknown future values.
sourceOpaque string. Current local storage writes local; clients must tolerate unknown future values.
generationInteger generation served to operators and runtimes for refresh validation.
metadataString map for provider-neutral details. Local storage paths, key ids, and key-file locations are never returned.

Rotation and runtime cache

vary app env rotate <app> <name> --target <target> creates a new generation for a secret-backed env value. Previous generations remain in the control plane for audit, but the config server returns the current generation by default.

Host operators can also use via app secret rotate <name> --app <app> for the local app-secret store. That command runs on the Via host, not through a workstation target.

Each running app's runtime config client memoizes a successful config.secret(...) response for 30 seconds. The cache is per-process, keyed by the full request URL (base + environment + name), and is bypassed for any non-200 response.

SettingBehavior
(unset / default)30s memoization for 200 responses
VARY_CONFIG_CACHE_TTL_SECONDS=0No memoization; every config.secret(...) hits the wire
VARY_CONFIG_CACHE_TTL_SECONDS=N>0N-second memoization (operator-tuned)

After a rotation, every running app sees the new value within the cache TTL. For an immediate rollout, restart the affected runtime containers; a fresh process starts with an empty cache. Errors are never cached, so a transient denial or upstream failure does not pin a stale outcome.

Runtime refresh requests include the app, release, deploy, config profile version, key scope, key name, and generation. The server accepts the currently running profile for authorization, then materializes the newest active app profile so env rotation and add-on credential rotation can reach running workloads without changing the release that was originally deployed.

Calling config from Vary

import config

def main() {
    let url = config.secret("DATABASE_URL")
    let debug = config.bool("DEBUG", False)
    print(url)
    print(debug)
}

config.secret(name) returns the declared secret as a string or raises one of five typed errors (ConfigMissingError, ConfigUndeclaredError, ConfigPermissionDeniedError, ConfigServerUnavailableError, ConfigBadTypeError). Catch them individually with except ConfigMissingError as e or fall back to the family with except ConfigError as e.

config.bool(name, default) returns a typed public config value with a default. Both call the same wire shape and use the same cache policy.

Avoid these patterns

Anti-patternWhy it fails
Set VARY_DATABASE_URL in vary.toml [runtime] envThe runner refuses any env name under the forbidden prefixes (VARY_IDENTITY_*, VARY_SIGNING_*, etc.). Even if not refused, container env is inspectable via docker inspect.
Bake a secret into the imageVary uses a fixed runtime image and bind-mounts the artifact read-only. Image layers carry no per-app code.
Pass a secret on the deploy command lineThe CLI never accepts secret values via argv.
Read a secret from a build containerBuild containers carry aud = "vary-build"; the config server rejects them at the audience check.

Continue to Operations for status, doctor, history, and key rotation.