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 {{REGISTRY_TOKEN}} and {{DOCKYARD_API_KEY}} 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"         # logical name used in spec variables
    description: "Registry credentials"
    location: "file:///./shared/secrets.yaml"  # file:// path inside the pod
    keys:
      REGISTRY_TOKEN: "registry-bearer-token"  # variable → key in the Secret
      REGISTRY_VERSION: "registry-api-version"
      MCP_SERVER_TOKEN: "mcp-server-token"
Field Description
namespace Logical name — used to scope {{VARIABLE}} 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 {{VARIABLE_NAME}}:

capability:
  consumes:
    - namespace: registry
      type: http
      baseUri: "https://api.example.com"
      authentication:
        type: bearer
        token: "{{REGISTRY_TOKEN}}"      # injected from bind
  exposes:
    - type: mcp
      port: 3001
      namespace: shipyard-tools
      authentication:
        type: bearer
        token: "{{MCP_SERVER_TOKEN}}"    # injected from bind

What the Operator Creates

For each unique bind location, the operator:

  1. Looks up the Secret {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 ReconcileError 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 (in ConfigMap)
ikanos: "1.0.0-alpha3"
binds:
  - namespace: "registry-env"
    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"
    location: "file:///./shared/secrets.yaml"   # same path → same Secret
    keys:
      DOCKYARD_API_KEY: "dockyard-api-key"
# Create the Secret with ALL keys combined (same path = one Secret)
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 shipyard-capability.yaml

# Check the engine started with secrets mounted
kubectl logs -l naftiko.io/capability=shipyard -n default | grep -i "secret\|bind\|credential"