-
-
Notifications
You must be signed in to change notification settings - Fork 0
Secrets
Nenya loads secrets from JSON files. Multiple sources are supported with priority ordering. All JSON files support the same structure. Fields can be split across multiple files.
| Priority | Path | Description |
|---|---|---|
| 1 | $CREDENTIALS_DIRECTORY/secrets |
systemd single file (highest priority) |
| 2 | $CREDENTIALS_DIRECTORY/*.json |
systemd directory |
| 3 | $NENYA_SECRETS_DIR/*.json |
configurable via env var |
| 4 | /run/secrets/nenya/*.json |
K8s/Docker standard path |
The first path that exists is used. If multiple JSON files exist in a directory, they are merged alphabetically (last-wins for duplicate keys).
{
"client_token": "nk-...",
"provider_keys": {
"gemini": "AIza...",
"deepseek": "sk-...",
"openai": "sk-proj-..."
},
"api_keys": {
"dev-user": {
"name": "dev-user",
"token": "nk-...",
"roles": ["user"],
"allowed_agents": ["build", "plan"],
"enabled": true
}
}
}| Field | Type | Required | Description |
|---|---|---|---|
client_token |
string | Yes | Admin bearer token for /v1/* endpoints |
provider_keys |
object | No | Map of provider name → API key |
api_keys |
object | No | Map of key ID → ApiKey config (RBAC) |
openssl rand -hex 32
# Output: nk-abc123def456... (prefix + 48 hex chars)# Generate key with roles (alias: a2a)
go run ./cmd/nenya keygen --name "dev-user" --roles user,read-only --agents build,planManual configuration:
{
"api_keys": {
"dev-user": {
"name": "dev-user",
"token": "nk-...",
"roles": ["user"],
"allowed_agents": ["build", "plan"],
"allowed_endpoints": ["GET /v1/models", "POST /v1/chat/completions"],
"enabled": true
}
}
}ApiKey Field Reference:
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | Human-readable key identifier (for logging/metrics) |
token |
string | Yes | API key token (minimum 16 characters, prefix nk- recommended) |
roles |
[]string | Yes | One or more: "admin", "user", "read-only"
|
allowed_agents |
[]string | No | Agent names this key can access (empty = all agents) |
allowed_endpoints |
[]string | No | HTTP method + path allowlist (empty = role-based defaults) |
created_at |
string | No | ISO 8601 timestamp for audit trail |
expires_at |
string | No | ISO 8601 timestamp for key expiration |
enabled |
bool | No | Enable/disable key without deletion |
Roles:
| Role | Permissions | Agent Access | Endpoint Access |
|---|---|---|---|
admin |
Full access (bypasses RBAC) | All agents | All endpoints |
user |
Chat, models, embed | Scoped by allowed_agents (or all if empty) |
All non-admin endpoints |
read-only |
Models, metrics only | Scoped by allowed_agents (or all if empty) |
GET requests only |
Examples:
Read-only key for model catalog:
{
"api_keys": {
"model-reader": {
"name": "Model Catalog Reader",
"token": "nk-abc123...",
"roles": ["read-only"],
"allowed_agents": [],
"enabled": true
}
}
}User key scoped to specific agents with endpoint allowlist:
{
"api_keys": {
"build-agent-user": {
"name": "Build Agent User",
"token": "nk-def456...",
"roles": ["user"],
"allowed_agents": ["build", "test"],
"allowed_endpoints": ["GET /v1/models", "POST /v1/chat/completions", "POST /v1/embeddings"],
"enabled": true
}
}
}Admin key with expiration:
{
"api_keys": {
"ops-admin": {
"name": "Operations Admin",
"token": "nk-ghi789...",
"roles": ["admin"],
"expires_at": "2026-12-31T23:59:59Z",
"enabled": true
}
}
}The provider_keys object maps provider names to their API keys. See Providers for the full list of built-in providers.
Legacy single-key format:
{
"provider_keys": {
"openai": "sk-proj-..."
}
}Multi-account format (recommended for high-volume providers):
For providers with multiple API keys or credentials, use the accounts field in the provider config instead:
{
"providers": {
"openai": {
"url": "https://api.openai.com/v1/chat/completions",
"accounts": [
{ "id": "account-1", "type": "apikey", "credential": "sk-proj-xxxxx" },
{ "id": "account-2", "type": "apikey", "credential": "sk-proj-yyyyy" }
],
"ratelimit_max_rpm": 500
},
"google": {
"url": "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions",
"accounts": [
{ "id": "oauth-dev", "type": "oauth", "credential": "ya29.a0..." }
]
}
}
}- LRU Selection: Least-recently-used strategy with mutex-guarded access
- Error Classification: 6 error classes (auth, rate_limit, quota, capacity, server, unknown) with semantic cooldowns
- Exponential Backoff: Failed accounts receive exponentially increasing backoff (±10% jitter) up to level 15
- Model Locks: Individual model+provider combinations locked during cooldown
-
Account State Persistence: State persisted in
<provider>.accounts.jsonfiles in config directory
At least one provider key must be present for the corresponding provider to work. If accounts is configured, it takes precedence over provider_keys for that provider.
| Type | Description |
|---|---|
apikey |
Standard API key (most providers) |
oauth |
OAuth bearer token |
cookie |
Cookie-based authentication |
| Status | Behavior |
|---|---|
active |
Normal operation, eligible for selection |
error |
Last request failed (short cooldown) |
disabled |
Permanently excluded from rotation |
# Create secrets file
cat > /etc/nenya/secrets.json << 'EOF'
{
"client_token": "nk-$(openssl rand -hex 32)",
"provider_keys": {
"deepseek": "sk-...",
"gemini": "AIza..."
}
}
EOF
# Configure systemd
cat > /etc/systemd/system/nenya.service << 'EOF'
[Service]
LoadCredential=secrets:/etc/nenya/secrets.json
EOFThe gateway reads from $CREDENTIALS_DIRECTORY/secrets.
mkdir -p /etc/nenya/secrets.d
cat > /etc/nenya/secrets.d/01-client.json << 'EOF'
{"client_token": "nk-..."}
EOF
cat > /etc/nenya/secrets.d/02-providers.json << 'EOF'
{"provider_keys": {"deepseek": "sk-...", "gemini": "AIza..."}}
EOF
cat > /etc/nenya/secrets.d/03-api-keys.json << 'EOF'
{"api_keys": {...}}
EOFNo systemd change needed — gateway auto-detects secrets.d/*.json.
Mount secrets as files. Use NENYA_SECRETS_DIR to point to the mount point:
services:
nenya:
image: ghcr.io/gumieri/nenya:latest
ports:
- "8080:8080"
volumes:
- ./config:/etc/nenya:ro
- ./secrets:/run/secrets/nenya:ro
environment:
NENYA_SECRETS_DIR: /run/secrets/nenya
restart: unless-stoppedExample secrets directory:
secrets/
├── 01-client.json -> {"client_token": "nk-..."}
├── 02-providers.json -> {"provider_keys": {"deepseek": "sk-..."}}
└── 03-keys.json -> {"api_keys": {...}}Mount a Secret as files. Use NENYA_SECRETS_DIR env var:
apiVersion: v1
kind: Secret
metadata:
name: nenya-secrets
type: Opaque
stringData:
01-client.json: |
{"client_token": "nk-..."}
02-providers.json: |
{"provider_keys": {"deepseek": "sk-...", "gemini": "AIza..."}}
03-keys.json: |
{"api_keys": {"dev-user": {...}}}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: nenya
spec:
replicas: 1
template:
spec:
containers:
- name: nenya
image: ghcr.io/gumieri/nenya:latest
env:
- name: NENYA_SECRETS_DIR
value: "/run/secrets/nenya"
volumeMounts:
- name: config
mountPath: /etc/nenya
readOnly: true
- name: secrets
mountPath: /run/secrets/nenya
readOnly: true
livenessProbe:
httpGet:
path: /healthz
port: 8080
readinessProbe:
httpGet:
path: /healthz
port: 8080
volumes:
- name: config
configMap:
name: nenya-config
- name: secrets
secret:
secretName: nenya-secretsNote: NENYA_SECRETS_DIR is optional. If unset, the gateway falls back to /run/secrets/nenya.
- Never commit secrets to version control
- Set
chmod 600on secrets files - Use systemd credential mechanism for secure in-memory storage
- Rotate tokens periodically
- Path traversal (
..) is rejected for all secret paths
Nenya uses mlock to keep tokens in RAM, preventing them from swapping to disk. Requires:
[Service]
LimitMEMLOCK=infinity
LimitCORE=0If this setting is missing, Nenya will fail to start with error:
secure memory allocation failed
See Security for implementation details.
If you previously used NENYA_CLIENT_TOKEN and NENYA_PROVIDER_KEY_* environment variables, migrate to JSON files:
Before (deprecated):
export NENYA_CLIENT_TOKEN="nk-..."
export NENYA_PROVIDER_KEY_DEEPSEEK="sk-..."
export NENYA_PROVIDER_KEY_GEMINI="AIza..."After:
// 01-client.json
{"client_token": "nk-..."}
// 02-providers.json
{"provider_keys": {"deepseek": "sk-...", "gemini": "AIza..."}}Environment variables are no longer supported. Use JSON files only.
sudo mkdir -p /etc/nenya
sudo chown root:nenya /etc/nenya
sudo chmod 750 /etc/nenyaThe LoadCredential mechanism passes files via memfd (anonymous memory), avoiding disk reads after boot.
macOS does not support systemd credentials. Recommended approaches:
Option A: Named file with strict permissions
mkdir -p ~/.config/nenya/secrets.d
chmod 700 ~/.config/nenya/secrets.d
CREDENTIALS_DIRECTORY=~/.config/nenya/secrets.d ./nenya -config config.jsonOption B: macOS Keychain (preferred)
security add-generic-password -s "nenya-secrets" -a "$USER" \
-w "$(cat creds/secrets.json)" -U- Security — Security policy and vulnerability reporting
- Deployment — Container and Kubernetes deployment
- Configuration — Config reference
Getting Started
- Home — Project overview
- Quick Start — Install and run in 5 minutes
- Client Setup — OpenCode, Cursor, and other clients
- Deployment — Bare metal, container, Kubernetes
Core Concepts
- Configuration — Config reference and examples
- Providers — 24 providers, capabilities, special behaviors
- Routing — Latency-aware routing and fallback chains
- Architecture — Package overview and request lifecycle
- MCP Integration — MCP server integration
Reference
- Passthrough Proxy — Raw provider endpoint proxying
- Secrets — Systemd credentials and container secrets
- Model Discovery — Dynamic model catalog fetching
- API Endpoints — Endpoint reference
- Adapters — Provider adapter system
- Billing — Billing-aware routing and quota tracking
- Caching — Exact-match and semantic caching
- Provider Capabilities — Service kinds matrix
- Unknown MaxContext — Unknown context window behavior
Operations
- Demo — Test all pipeline tiers
- Troubleshooting — Common issues and solutions
- FAQ — Frequently asked questions
- Security — Security policy and vulnerability reporting
Project
- Roadmap — Planned features
- Disclaimer — Legal disclaimer