Secrets & Binds¶
Binds are how you inject secrets and credentials into a capability spec. You declare what variables you need in the spec — the operator mounts the corresponding Kubernetes Secrets into the engine pod at the declared path.
How Binds Work¶
Capability spec declares: Operator expects: Engine reads:
────────────────────── ───────────────── ─────────────
binds: Secret /app/shared/
- namespace: "registry-env" {name}-bind-shared secrets.yaml
location: "file:///./shared/ (all keys combined)
secrets.yaml"
keys:
REGISTRY_TOKEN: "..."
- namespace: "dockyard-env"
location: "file:///./shared/
secrets.yaml"
keys:
DOCKYARD_API_KEY: "..."
The engine resolves and at
runtime by reading the mounted file.
Secret Naming Convention¶
The operator derives the Secret name from the parent directory of the
bind location path — not from the bind namespace:
location: "file:///./shared/secrets.yaml"
→ parent dir: "shared"
→ Secret name: {capability-name}-bind-shared
location: "file:///./config/db.yaml"
→ parent dir: "config"
→ Secret name: {capability-name}-bind-config
Why parent-dir, not namespace? Multiple bind namespaces often point to the same file path. If the Secret were named per-namespace, two Secrets with the same filename key (
secrets.yaml) could not be mounted into the same directory — Kubernetes projected volumes would silently overwrite one with the other. One Secret per file path eliminates this conflict entirely.
Creating Bind Secrets¶
All bind namespaces that share the same location path must have their keys
combined into one Secret:
# Two bind namespaces, same location → one Secret with all keys
kubectl create secret generic my-capability-bind-shared \
--from-literal=secrets.yaml=$'registry-bearer-token: "abc123"\nregistry-api-version: "1.0"\nmcp-server-token: "sk-mcp-xxx"\ndockyard-api-key: "dock-key"' \
-n default
The Secret key (secrets.yaml) must match the filename in the location path.
Multiple Bind Locations¶
If your binds point to different file paths, create one Secret per path:
binds:
- namespace: "registry-env"
location: "file:///./shared/secrets.yaml" # → {name}-bind-shared
keys:
REGISTRY_TOKEN: "registry-bearer-token"
- namespace: "db-env"
location: "file:///./config/db.yaml" # → {name}-bind-config
keys:
DB_PASSWORD: "database-password"
kubectl create secret generic my-capability-bind-shared \
--from-literal=secrets.yaml='registry-bearer-token: "abc123"' \
-n default
kubectl create secret generic my-capability-bind-config \
--from-literal=db.yaml='database-password: "secret"' \
-n default
Bind Spec Fields¶
binds:
- namespace: "registry-env"
description: "Registry credentials"
location: "file:///./shared/secrets.yaml"
keys:
REGISTRY_TOKEN: "registry-bearer-token"
REGISTRY_VERSION: "registry-api-version"
MCP_SERVER_TOKEN: "mcp-server-token"
| Field | Description |
|---|---|
namespace |
Logical name — scopes `` references |
description |
Human-readable description |
location |
file:// URI of the secrets file inside the pod |
keys |
Map of variable name → key name in the Secret |
Using Bind Variables in the Spec¶
Reference bind variables anywhere in the spec using ``:
capability:
consumes:
- namespace: registry
type: http
baseUri: "https://api.example.com"
authentication:
type: bearer
token: ""
exposes:
- type: mcp
address: "0.0.0.0"
port: 3001
namespace: shipyard-tools
authentication:
type: bearer
token: ""
What the Operator Creates¶
For each unique bind location, the operator:
- Looks up
{capability-name}-bind-{parent-dir}in the namespace - Mounts it as a volume at the parent directory (e.g.
/app/shared/) - The Secret key (
secrets.yaml) becomes a file at/app/shared/secrets.yaml
# Check the volume mounts on the generated Deployment
kubectl get deployment my-capability -n default \
-o jsonpath='{.spec.template.spec.containers[0].volumeMounts}' \
| python3 -m json.tool
Secret Must Exist Before the CR¶
The operator throws a reconcile error if the Secret does not exist:
Bind Secret 'my-capability-bind-shared' not found in namespace 'default'.
Create it before applying the Capability CR.
kubectl create secret generic my-capability-bind-shared \
--from-file=secrets.yaml=<path> -n default
Always create your Secrets before applying the Capability CR.
Complete Example¶
ikanos spec:
ikanos: "1.0.0-alpha4"
info:
display: Shipyard
description: "MCP capability for fleet management"
labels:
naftiko.io/tier: standard
binds:
- namespace: "registry-env"
description: "Maritime Registry API credentials"
location: "file:///./shared/secrets.yaml"
keys:
REGISTRY_TOKEN: "registry-bearer-token"
REGISTRY_VERSION: "registry-api-version"
MCP_SERVER_TOKEN: "mcp-server-token"
- namespace: "dockyard-env"
description: "Legacy Dockyard API credentials"
location: "file:///./shared/secrets.yaml" # same path → same Secret
keys:
DOCKYARD_API_KEY: "dockyard-api-key"
capability:
exposes:
- type: mcp
address: "0.0.0.0"
port: 3001
namespace: shipyard-tools
authentication:
type: bearer
token: ""
- type: control
address: "0.0.0.0"
port: 9090
observability:
enabled: true
metrics:
local:
enabled: true
Create the Secret (all keys from both namespaces combined — same location):
kubectl create secret generic shipyard-bind-shared \
--from-literal=secrets.yaml=$'registry-bearer-token: "abc"\nregistry-api-version: "1.0"\nmcp-server-token: "sk-mcp-xxx"\ndockyard-api-key: "dock-key"' \
-n default
Apply the Capability CR:
kubectl apply -f - <<EOF
apiVersion: naftiko.io/v1alpha3
kind: Capability
metadata:
name: shipyard
namespace: default
labels:
naftiko.io/tier: standard
spec:
specRef:
configMap: shipyard-spec
EOF
In a GitOps Repository¶
When managing capabilities via ArgoCD, include the bind Secret alongside the Capability CR and use sync-wave annotations to ensure the Secret exists before the CR is applied:
capabilities/
└── shipyard/
├── configmap.yaml ← sync-wave: "0"
├── bind-secret.yaml ← sync-wave: "0"
└── capability.yaml ← sync-wave: "1"
bind-secret.yaml:
apiVersion: v1
kind: Secret
metadata:
name: shipyard-bind-shared
namespace: default
annotations:
argocd.argoproj.io/sync-wave: "0"
type: Opaque
stringData:
secrets.yaml: |
registry-bearer-token: "abc123"
registry-api-version: "1.0"
mcp-server-token: "sk-mcp-xxx"
dockyard-api-key: "dock-key"
Security note: Storing secrets in plain text in Git is acceptable for development. For production, use Sealed Secrets or External Secrets Operator to encrypt values before committing.
See Import Consumes for mounting external consumes files.