Secrets Backend Interface¶
Security-sensitive interface
The secrets backend handles API keys, tokens, and credentials. Never log secret values — only log key names. Prefer SecretKeyRef references over direct values.
Overview¶
The secrets backend retrieves sensitive values (API keys, tokens, credentials) from external secret stores and builds Kubernetes environment variable references for injection into agent job pods. It abstracts the underlying secrets store so that the controller and engines can request secrets without knowing whether they come from Kubernetes Secrets, HashiCorp Vault, AWS Secrets Manager, or another provider.
Interface Summary¶
| Property | Value |
|---|---|
| Proto definition | proto/secrets.proto |
| Go interface | pkg/plugin/secrets/secrets.go |
| Interface version | 1 |
| Role in lifecycle | Called during job creation to inject secrets into agent pod environment |
| Criticality | Critical — if the secrets backend is unavailable, jobs cannot be created |
Go Interface¶
type Backend interface {
// GetSecret retrieves a single secret value by key.
GetSecret(ctx context.Context, key string) (string, error)
// GetSecrets retrieves multiple secrets in a single call.
GetSecrets(ctx context.Context, keys []string) (map[string]string, error)
// BuildEnvVars translates secret references into Kubernetes EnvVar specs.
// Keys in the map are environment variable names, values are secret keys.
BuildEnvVars(secretRefs map[string]string) ([]corev1.EnvVar, error)
// Name returns the unique identifier for this backend.
Name() string
// InterfaceVersion returns the interface version.
InterfaceVersion() int
}
RPC Methods¶
Handshake¶
Version negotiation called once at plugin startup.
GetSecret¶
Retrieves a single secret value by key.
rpc GetSecret(GetSecretRequest) returns (GetSecretResponse);
message GetSecretRequest {
string key = 1; // The secret key to retrieve.
}
message GetSecretResponse {
string value = 1; // The secret value.
}
Implementation guidance:
- Return a gRPC
NOT_FOUNDerror if the key does not exist. - Never log the secret value — only log the key name.
- Values are typically short-lived (API keys, tokens). Consider caching with a short TTL (30–60 seconds) if the backing store is slow.
- Respect the
context.Contextdeadline — secret retrieval should complete within 5 seconds.
GetSecrets¶
Retrieves multiple secrets in a single call. More efficient than calling GetSecret repeatedly.
rpc GetSecrets(GetSecretsRequest) returns (GetSecretsResponse);
message GetSecretsRequest {
repeated string keys = 1;
}
message GetSecretsResponse {
map<string, string> secrets = 1; // Key-value pairs. Missing keys are omitted.
}
Implementation guidance:
- Omit keys from the response map that were not found, rather than returning errors for individual missing keys.
- If any keys are critical and missing, the caller will detect the omission and handle it.
- Batch requests to the backing store where possible (e.g., Vault's batch secret reads).
BuildEnvVars¶
Translates secret references into Kubernetes EnvVar specs. This is the preferred method for injecting secrets into agent pods because it supports Kubernetes-native SecretKeyRef references.
rpc BuildEnvVars(BuildEnvVarsRequest) returns (BuildEnvVarsResponse);
message BuildEnvVarsRequest {
repeated SecretRef secret_refs = 1;
}
message SecretRef {
string env_name = 1; // The environment variable name in the pod.
string secret_key = 2; // The key in the secrets store.
}
message BuildEnvVarsResponse {
repeated EnvVar env_vars = 1;
}
message EnvVar {
string name = 1;
string value = 2; // Direct value (mutually exclusive with value_from).
SecretKeyRef value_from = 3; // Reference to a K8s Secret (preferred).
}
message SecretKeyRef {
string name = 1; // The Kubernetes Secret name.
string key = 2; // The key within the Secret.
}
Implementation guidance:
- Prefer
SecretKeyRefover direct values. When returningSecretKeyRefreferences, secrets are injected by the kubelet without passing through the controller process. This is more secure and avoids having plaintext secrets in memory. - For external backends (Vault, AWS SM), consider creating ephemeral Kubernetes Secrets and returning references to them, rather than passing plaintext values through the controller.
- The
valueandvalue_fromfields are mutually exclusive — set one or the other, not both.
Built-in: Kubernetes Secrets¶
The default backend reads secrets directly from Kubernetes Secret objects in the controller's namespace.
Configuration¶
config:
secret_resolver:
backends:
- scheme: "k8s"
backend: "k8s"
config:
namespace: "robodev" # Optional — defaults to the controller's namespace.
No additional configuration is required. The backend uses the controller's service account credentials to read Secrets.
Key Format¶
Secret keys use the format secretName/key:
robodev-anthropic-key/api_keyreads theapi_keydata key from therobodev-anthropic-keySecret.github-token/tokenreads thetokendata key from thegithub-tokenSecret.
Behaviour¶
| Method | Kubernetes Action |
|---|---|
GetSecret |
Reads the named Secret and returns the value for the specified key |
GetSecrets |
Reads multiple Secrets and returns all requested key-value pairs |
BuildEnvVars |
Returns SecretKeyRef entries pointing directly to K8s Secrets |
Required RBAC¶
The controller's service account needs read access to Secrets in its namespace:
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: robodev-secrets-reader
namespace: robodev
rules:
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "list"]
This Role is included in the Helm chart by default.
Built-in: HashiCorp Vault¶
A built-in Vault backend (pkg/plugin/secrets/vault/) reads secrets from HashiCorp Vault using its HTTP API with Kubernetes authentication and KV v2 storage.
Configuration¶
config:
secret_resolver:
backends:
- scheme: "vault"
backend: "vault"
config:
address: "https://vault.internal:8200"
role: "robodev"
auth_method: "kubernetes" # Currently the only supported method.
secrets_path: "secret" # KV v2 mount path.
Key Format¶
Secret keys use the path#field format:
stripe/test-key#api_keyreads theapi_keyfield from thesecret/data/stripe/test-keyKV v2 path.db/production#urlreads theurlfield from thesecret/data/db/productionpath.
If no #field is provided, the entire data map is returned as JSON.
Built-in: AWS Secrets Manager¶
A built-in AWS Secrets Manager backend (pkg/plugin/secrets/awssm/) reads secrets directly from AWS Secrets Manager using the AWS SDK for Go v2.
Authentication¶
The backend uses the AWS SDK default credential chain, which automatically supports:
- IRSA (IAM Roles for Service Accounts) on EKS — the recommended approach for production.
- Environment variables (
AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY). - Shared credentials file (
~/.aws/credentials). - EC2 instance metadata / ECS task roles.
No custom authentication code is needed, but IRSA requires prior EKS/IAM setup:
- Create an IAM role with
secretsmanager:GetSecretValuepermission (see Required IAM Permissions below). - Annotate the RoboDev controller's ServiceAccount with the role ARN:
- For cross-account access, ensure the source role has
sts:AssumeRolepermission and configureassume_role_arnin the backend config. The target account's role must have a trust policy allowing the source role to assume it.
Configuration¶
config:
secret_resolver:
backends:
- scheme: "aws-sm"
backend: "aws-secrets-manager"
config:
region: "eu-west-1"
assume_role_arn: "arn:aws:iam::123456789:role/robodev-secrets" # Optional
cache_ttl: "5m" # Optional (default: 5m)
| Key | Type | Default | Description |
|---|---|---|---|
region |
string | SDK default | AWS region for the Secrets Manager client |
assume_role_arn |
string | (none) | IAM role ARN to assume via STS before reading secrets |
cache_ttl |
duration string | 5m |
How long to cache secret values in memory |
Key Format¶
Secret keys use the secret-name#json-field format:
myapp/api-keys#stripe_key— reads thestripe_keyfield from the JSON-valued secret namedmyapp/api-keys.myapp/database-url— reads the raw secret string (no JSON parsing).arn:aws:secretsmanager:eu-west-1:123456789:secret:myapp/config#db_host— uses a full ARN.
URI Format¶
aws-sm://myapp/api-keys#stripe_key
aws-sm://arn:aws:secretsmanager:eu-west-1:123456789:secret:myapp/config#db_host
Behaviour¶
| Method | AWS Action |
|---|---|
GetSecret |
Calls GetSecretValue, caches the result, optionally extracts a JSON field |
GetSecrets |
Calls GetSecret per unique secret name (cache deduplicates) |
BuildEnvVars |
Returns direct value EnvVar entries (secrets are fetched at resolution time) |
Required IAM Permissions¶
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["secretsmanager:GetSecretValue"],
"Resource": "arn:aws:secretsmanager:eu-west-1:123456789:secret:robodev/*"
}
]
}
For cross-account access, the target account's role must have this policy and a trust policy allowing the controller's IRSA role to assume it.
Using AWS Secrets Manager via External Secrets Operator¶
The fastest path to AWS Secrets Manager integration is the External Secrets Operator (ESO). ESO syncs secrets from AWS Secrets Manager into Kubernetes Secrets, which the built-in K8s backend already reads. No RoboDev code changes needed.
Setup¶
- Install ESO on your cluster:
helm repo add external-secrets https://charts.external-secrets.io
helm install external-secrets external-secrets/external-secrets \
--namespace external-secrets --create-namespace
- Create a
ClusterSecretStorepointing to AWS Secrets Manager (authenticating via IRSA):
apiVersion: external-secrets.io/v1beta1
kind: ClusterSecretStore
metadata:
name: aws-secrets-manager
spec:
provider:
aws:
service: SecretsManager
region: eu-west-1
auth:
jwt:
serviceAccountRef:
name: external-secrets-sa
namespace: external-secrets
- Create an
ExternalSecretthat syncs the secrets your agents need:
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: robodev-anthropic-key
namespace: robodev
spec:
refreshInterval: 1h
secretStoreRef:
name: aws-secrets-manager
kind: ClusterSecretStore
target:
name: robodev-anthropic-key
data:
- secretKey: api_key
remoteRef:
key: robodev/anthropic-api-key
property: api_key
- RoboDev's K8s secrets backend reads the synced Secret as normal:
This approach works today with no changes to RoboDev.
Other Backends¶
The secrets plugin interface supports additional backends via gRPC plugins. The following are planned or can be implemented using the SecretsBackend protobuf service:
| Backend | Description | Authentication | Status |
|---|---|---|---|
aws-sm |
AWS Secrets Manager (native) | IRSA (IAM Roles for Service Accounts) | Built-in (see above) |
1password |
1Password Connect server or CLI | Connect token | Not implemented |
external-secrets |
Kubernetes External Secrets Operator | Delegated to ESO | Use K8s backend (see above) |
azure-kv |
Azure Key Vault | Workload identity federation | Not implemented |
See Writing a Plugin for a TypeScript example implementing the SecretsBackend interface.
Security Considerations¶
Secrets handling is the most security-sensitive area of RoboDev. Follow these principles:
Never Log Secret Values¶
The controller and all plugins must never log secret values. Only log key names and metadata:
// Correct
logger.Info("retrieved secret", "key", key)
// NEVER do this
logger.Info("retrieved secret", "key", key, "value", value)
Prefer Kubernetes-Native References¶
When possible, use SecretKeyRef in BuildEnvVars rather than direct values. This ensures:
- Secrets are injected by the kubelet, never transiting the controller process.
- Secrets are visible in the pod spec only as references, not plaintext.
- Kubernetes RBAC controls who can read the underlying Secret objects.
Scope Secrets Narrowly¶
Each agent job should only have access to the secrets it needs:
- API key for the chosen engine (e.g.,
anthropic-api-keyfor Claude Code). - Repository access token (e.g.,
github-token). - Any task-specific credentials declared in the ticket metadata.
Do not mount the controller's full secret set into agent pods.
Rotate Credentials Regularly¶
Use short-lived tokens where possible:
| Credential Type | Rotation Strategy |
|---|---|
| GitHub App installation tokens | Automatic 1-hour expiry |
| Vault dynamic secrets | TTL-based automatic rotation |
| AWS STS temporary credentials | Via IRSA with automatic refresh |
| Static API keys (Anthropic, OpenAI) | Manual rotation on a schedule |
Audit Secret Access¶
Log every secret access (key name only) for audit trails. This allows security teams to:
- Track which secrets are accessed and by which components.
- Detect unusual access patterns (e.g., a secret being read far more often than expected).
- Investigate incidents by correlating secret access with job creation events.
Protobuf Definition¶
The complete protobuf service is defined in proto/secrets.proto:
service SecretsBackend {
rpc Handshake(HandshakeRequest) returns (HandshakeResponse);
rpc GetSecret(GetSecretRequest) returns (GetSecretResponse);
rpc GetSecrets(GetSecretsRequest) returns (GetSecretsResponse);
rpc BuildEnvVars(BuildEnvVarsRequest) returns (BuildEnvVarsResponse);
}
See proto/common.proto for the shared HandshakeRequest/HandshakeResponse message definitions.