Configuration
Agent configuration
secretserver-agent has no configuration file and reads no environment variables. Everything is set with command-line flags, and enrollment state lives in a private state directory.
Commands
secretserver-agent login|status|access|render|run [options]
| Command | Purpose |
|---|---|
login | Generate the device key and enroll with an API key file or OAuth2 device approval |
status | Print identity and grant metadata as JSON |
access | Call one assigned grant by alias and write the result (may contain secrets) to stdout |
render | Resolve %%NAME%% tokens in text or JSON through the device identity |
run | Keep secret.read and variable.resolve grants refreshed as private JSON files |
Common flag
| Flag | Default | Notes |
|---|---|---|
--state-dir | ~/.config/secretserver-agent | Absolute, clean path to a 0700 directory that is not a symlink. Created if missing. With no home directory, it must be given explicitly. |
Flags go after the command name, for example secretserver-agent run --state-dir /var/lib/secretserver-agent ....
login flags
| Flag | Default | Notes |
|---|---|---|
--server | https://api.secretserver.io | HTTPS origin with no path, query or credentials |
--account | required | Expected account UUID, canonical lowercase |
--profile | required | Admin-assigned access profile UUID |
--name | required | Device name: 1 to 128 letters, digits, dots, underscores or hyphens, starting with a letter or digit |
--api-key-file | not set | Regular 0600 file with one account-admin API key (up to 64 KiB). Omit it to use OAuth2 device approval. |
--allow-loopback-http | false | Development only: allow http to a literal loopback IP. Saved with the identity. |
login refuses to replace a completed identity. If a pending identity exists, its server, account, profile, name and HTTP setting must match the new command.
status, access, render and run flags
| Flag | Default | Used by | Notes |
|---|---|---|---|
--alias | not set | access | Grant alias to call |
--input | not set | access, render | Request JSON file for access (2 MiB max, must be valid JSON) or template file for render (1 MiB max). - reads stdin. Without it, access sends {} and render reads stdin. |
--json | false | render | Resolve string values inside a JSON document instead of raw text |
--output-dir | required for run | run | Dedicated delivery directory; must be separate from the state directory in both directions |
--poll | 30s | run | Refresh interval, 1s to 1h |
Files and permissions
| Path | Contents |
|---|---|
<state-dir>/identity.json | server, allow_loopback_http, account_id, profile_id, device_id, name and the Ed25519 private_key. Mode 0600; unknown fields are rejected. |
<state-dir>/.lock | PID of the process holding the directory. Left behind after a crash; check that the process is gone before removing it. |
<output-dir>/.secretserver-agent | Ownership marker (server, account, device). A directory marked by another agent is refused. |
<output-dir>/<alias>.json | The grant's data fields as JSON, mode 0600, replaced atomically |
<output-dir>/.lock | Lock for the run loop |
The output directory must be empty on first use and mode 0700. Treat it as agent-managed: stale .json files are removed on each refresh, on failure, on shutdown and at restart before any network call. Aliases that differ only in case fail the refresh.
Profile grants
Admins configure what a device may use with PUT /api/v1/agents/profiles/{UUID} (requires admin:*). Resources must belong to the same account; wildcards are not allowed.
| service | resource | Behavior |
|---|---|---|
secret.read | secret UUID | Delivered by run; readable with access |
variable.resolve | variable UUID | Delivered by run as {"value": ...}; used by render |
key.sign | key handle, plus backend | access with an input of {"message":"BASE64","purpose":"..."}; never exports key material |
database.issue | database role UUID | access issues database credentials on demand; run never does |
profile body
{
"name": "web-production",
"enabled": true,
"grants": [
{"alias": "app-config", "service": "secret.read", "resource": "SECRET_UUID"},
{"alias": "release-signing", "service": "key.sign", "backend": "softhsm", "resource": "KEY_HANDLE"},
{"alias": "database", "service": "database.issue", "resource": "DATABASE_ROLE_UUID"},
{"alias": "sudo-password", "service": "variable.resolve", "resource": "VARIABLE_UUID"}
]
}Admin endpoints
| Method | Endpoint | Purpose |
|---|---|---|
| PUT | /api/v1/agents/profiles/{UUID} | Create or update a profile |
| GET | /api/v1/agents/profiles | List profiles and grants |
| POST | /api/v1/agents/devices | API-key enrollment |
| GET | /api/v1/agents/devices | List enrolled devices |
| DELETE | /api/v1/agents/devices/{UUID} | Revoke a device |
Network behavior
| Setting | Value |
|---|---|
| TLS | 1.3 minimum, certificates verified against system roots |
| Redirects | Refused |
| HTTP timeout | 15 seconds per request |
| Refresh cycle | Bounded to 30 seconds; a failure clears delivered files |
| Response limit | 4 MiB |
| Signed headers | X-SecretServer-Account, -Device, -Time, -Nonce, -Signature |
| Clock skew | Server accepts proofs up to 60 seconds old and 5 seconds in the future |
| OAuth2 | RFC 8628 device flow, client ID secretserver-agent; the token is scoped agent:enroll |
A 401 or 403 during run stops the agent. Other failures retry at the polling interval.
Server-side settings
| Setting | Default | Notes |
|---|---|---|
AGENT_VERIFICATION_URI | https://secretserver.io/agent/authorize | Approval page URL shown to users during OAuth2 login. Set it when self-hosting. |
NEXT_PUBLIC_API_URL | none | Web approval page setting; point it at the same API |
| Migration | 034_agents.sql | Applied on API startup if missing |
TLS termination in front of the API must keep the request path and query exactly, because they are signed.
systemd unit
deploy/secretserver-agent.service (excerpt)
[Service] Type=simple User=secretserver-agent Group=secretserver-agent StateDirectory=secretserver-agent StateDirectoryMode=0700 RuntimeDirectory=secretserver-agent RuntimeDirectoryMode=0700 UMask=0077 ExecStart=/usr/local/bin/secretserver-agent run --state-dir /var/lib/secretserver-agent --output-dir /run/secretserver-agent --poll 30s Restart=on-failure RestartSec=15 NoNewPrivileges=true CapabilityBoundingSet= ProtectSystem=strict ProtectHome=true PrivateDevices=true RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX MemoryDenyWriteExecute=true SystemCallFilter=@system-service
systemd removes /run/secretserver-agent whenever the service stops. After revocation the agent exits with status 1 and systemd retries every 15 seconds; disable the unit on a decommissioned host.
Exit codes
| Code | Meaning |
|---|---|
| 0 | Success, or -h / --help |
| 1 | Any error, printed as secretserver-agent: <message> |