Skip to content

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:

  1. Looks up {capability-name}-bind-{parent-dir} in the namespace
  2. Mounts it as a volume at the parent directory (e.g. /app/shared/)
  3. 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.