# Egress Auth

Egress auth rules tell Sandbox0 when to apply outbound authentication for matching traffic.

This configuration is carried on the network surface as `network.egress.credentialRules`, but conceptually it belongs to the credential system:

- `credentialBindings` define what auth material exists.
- `credentialRules` define where and when that material is used.
- `trafficRules` still decide whether the traffic itself is allowed.

Bindings are configured under the same `network` object everywhere:

- template `spec.network`
- sandbox claim `config.network`
- runtime update `PUT /api/v1/sandboxes/{id}/network`

## Binding Fields

| Field | Required | Description |
|-------|----------|-------------|
| `ref` | Yes | Stable local name matched by `credentialRules[*].credentialRef` |
| `sourceRef` | Yes | Region-scoped credential source reference to resolve |
| `projection` | Yes | How the source is shaped for runtime injection |
| `cachePolicy.ttl` | No | Optional ctld network-runtime cache lifetime for resolved material, such as `5m` or `1h` |

## Binding Example

This binding projects a stored token into an `Authorization` header:

```yaml
credentialBindings:
    - ref: gh-token
      sourceRef: github-source
      projection:
          type: http_headers
          httpHeaders:
              headers:
                  - name: Authorization
                    valueTemplate: "Bearer {{ .token }}"
```

In this example, `{{ .token }}` refers to the `token` key inside a `static_headers` source's `values` map. If the referenced key is missing, header rendering fails.

## Resolve Cache And Rotation

Runtime egress auth resolution is cached by the ctld network runtime, not by manager. Manager resolves the current sandbox binding from PostgreSQL and returns an `expiresAt` timestamp. Ctld reuses that resolved material until `expiresAt`, then asks manager again.

Credential source rotation actively invalidates cached resolved material in ctld. After `PUT /api/v1/credential-sources/{'{name}'}`, already-claimed sandboxes should use the new source version on the next matching egress resolve. If realtime invalidation delivery is interrupted by ctld, manager, or database restarts, cache expiry remains the fallback consistency bound.

The default resolved-material lifetime is 5 minutes. Set `cachePolicy.ttl` on a binding when that binding needs a shorter or longer fallback cache window.

## Placeholder Substitution

Use `placeholder_substitution` when the sandbox process should see only an opaque placeholder, while the ctld network runtime replaces that placeholder with the resolved secret at the egress boundary.

This projection works for HTTP-family rules. For `protocol: https` or `protocol: grpc`, the rule must use `tlsMode: terminate-reoriginate` so the ctld network runtime can inspect and rewrite the HTTP request. Body substitution requires a known `Content-Length`, an uncompressed body, and a body size of 1 MiB or less.

```yaml
credentialBindings:
    - ref: api-token
      sourceRef: api-source
      projection:
          type: placeholder_substitution
          placeholderSubstitution:
              replacements:
                  - placeholder: s0env_api_token_placeholder
                    valueTemplate: "{{ .token }}"
                    locations:
                        - header
                        - query
                        - body
```

With that binding, a sandbox process can send the placeholder in a header, query parameter, or request body. Matching outbound HTTP requests receive the resolved `token` value before they are sent upstream.

## Runnable Placeholder Example

The snippets below configure a sandbox so the process can send `s0env_api_token` in a header, query value, or body, while the egress path replaces it with the `token` stored in `api-token-source`.

Set these values before running the snippets:

- `SANDBOX_ID`: sandbox to update
- `API_CIDR`: destination CIDR such as `203.0.113.10/32`
- `API_ORIGIN`: destination origin such as `http://203.0.113.10:8080`
- `API_PORT`: destination port, defaults to `8080`
- `API_TOKEN`: secret value to inject, defaults to `docs-secret-token`

<Tabs
  tabs={[
    {
      label: "Go",
      language: "go",
      code: `apiCIDR := os.Getenv("API_CIDR")
apiPort := int32(8080)
if value := os.Getenv("API_PORT"); value != "" {
    parsed, err := strconv.ParseInt(value, 10, 32)
    if err != nil {
        log.Fatal(err)
    }
    apiPort = int32(parsed)
}
apiToken := os.Getenv("API_TOKEN")
if apiToken == "" {
    apiToken = "docs-secret-token"
}
sourceName := os.Getenv("SOURCE_NAME")
if sourceName == "" {
    sourceName = "api-token-source"
}

_, err := client.CreateCredentialSource(ctx, apispec.CredentialSourceWriteRequest{
    Name: sourceName,
    ResolverKind: apispec.CredentialSourceResolverKindStaticHeaders,
    Spec: apispec.CredentialSourceWriteSpec{
        StaticHeaders: apispec.NewOptStaticHeadersSourceSpec(apispec.StaticHeadersSourceSpec{
            Values: apispec.NewOptStaticHeadersSourceSpecValues(apispec.StaticHeadersSourceSpecValues{
                "token": apiToken,
            }),
        }),
    },
})
if err != nil {
    log.Fatal(err)
}

_, err = sandbox.UpdateNetworkPolicy(ctx, apispec.SandboxNetworkPolicy{
    Mode: apispec.SandboxNetworkPolicyModeBlockAll,
    Egress: apispec.NewOptNetworkEgressPolicy(apispec.NetworkEgressPolicy{
        TrafficRules: []apispec.TrafficRule{
            {
                Name: apispec.NewOptString("allow-example-api"),
                Action: apispec.TrafficRuleActionAllow,
                Cidrs: []string{apiCIDR},
                Ports: []apispec.PortSpec{
                    {
                        Port: apiPort,
                        Protocol: apispec.NewOptString("tcp"),
                    },
                },
            },
        },
        CredentialRules: []apispec.EgressCredentialRule{
            {
                Name: apispec.NewOptString("api-token"),
                CredentialRef: "api-token",
                Protocol: apispec.NewOptEgressAuthProtocol(apispec.EgressAuthProtocolHTTP),
                Ports: []apispec.PortSpec{
                    {
                        Port: apiPort,
                        Protocol: apispec.NewOptString("tcp"),
                    },
                },
                FailurePolicy: apispec.NewOptEgressAuthFailurePolicy(apispec.EgressAuthFailurePolicyFailClosed),
            },
        },
    }),
    CredentialBindings: []apispec.CredentialBinding{
        {
            Ref: "api-token",
            SourceRef: sourceName,
            Projection: apispec.ProjectionSpec{
                Type: apispec.CredentialProjectionTypePlaceholderSubstitution,
                PlaceholderSubstitution: apispec.NewOptPlaceholderSubstitutionProjection(apispec.PlaceholderSubstitutionProjection{
                    Replacements: []apispec.PlaceholderReplacement{
                        {
                            Placeholder: "s0env_api_token",
                            ValueTemplate: "{{ .token }}",
                            Locations: []apispec.PlaceholderSubstitutionLocation{
                                apispec.PlaceholderSubstitutionLocationQuery,
                                apispec.PlaceholderSubstitutionLocationHeader,
                                apispec.PlaceholderSubstitutionLocationBody,
                            },
                        },
                    },
                }),
            },
        },
    },
})
if err != nil {
    log.Fatal(err)
}`
    },
    {
      label: "Python",
      language: "python",
      code: `import os

from sandbox0.apispec.models.credential_source_write_request import CredentialSourceWriteRequest
from sandbox0.apispec.models.sandbox_network_policy import SandboxNetworkPolicy

api_cidr = os.environ["API_CIDR"]
api_port = int(os.environ.get("API_PORT", "8080"))
api_token = os.environ.get("API_TOKEN", "docs-secret-token")
source_name = os.environ.get("SOURCE_NAME", "api-token-source")

client.create_credential_source(
    CredentialSourceWriteRequest.from_dict({
        "name": source_name,
        "resolverKind": "static_headers",
        "spec": {
            "staticHeaders": {
                "values": {
                    "token": api_token,
                },
            },
        },
    })
)

sandbox.update_network_policy(
    SandboxNetworkPolicy.from_dict({
        "mode": "block-all",
        "egress": {
            "trafficRules": [
                {
                    "name": "allow-example-api",
                    "action": "allow",
                    "cidrs": [api_cidr],
                    "ports": [{"port": api_port, "protocol": "tcp"}],
                },
            ],
            "credentialRules": [
                {
                    "name": "api-token",
                    "credentialRef": "api-token",
                    "protocol": "http",
                    "ports": [{"port": api_port, "protocol": "tcp"}],
                    "failurePolicy": "fail-closed",
                },
            ],
        },
        "credentialBindings": [
            {
                "ref": "api-token",
                "sourceRef": source_name,
                "projection": {
                    "type": "placeholder_substitution",
                    "placeholderSubstitution": {
                        "replacements": [
                            {
                                "placeholder": "s0env_api_token",
                                "valueTemplate": "{{ .token }}",
                                "locations": ["query", "header", "body"],
                            },
                        ],
                    },
                },
            },
        ],
    })
)`
    },
    {
      label: "TypeScript",
      language: "typescript",
      code: `const apiCidr = process.env.API_CIDR ?? '';
const apiPort = Number(process.env.API_PORT ?? '8080');
const apiToken = process.env.API_TOKEN ?? 'docs-secret-token';
const sourceName = process.env.SOURCE_NAME ?? 'api-token-source';

await client.credentialSources.create({
    name: sourceName,
    resolverKind: 'static_headers',
    spec: {
        staticHeaders: {
            values: {
                token: apiToken,
            },
        },
    },
});

await sandbox.updateNetworkPolicy({
    mode: 'block-all',
    egress: {
        trafficRules: [
            {
                name: 'allow-example-api',
                action: 'allow',
                cidrs: [apiCidr],
                ports: [{ port: apiPort, protocol: 'tcp' }],
            },
        ],
        credentialRules: [
            {
                name: 'api-token',
                credentialRef: 'api-token',
                protocol: 'http',
                ports: [{ port: apiPort, protocol: 'tcp' }],
                failurePolicy: 'fail-closed',
            },
        ],
    },
    credentialBindings: [
        {
            ref: 'api-token',
            sourceRef: sourceName,
            projection: {
                type: 'placeholder_substitution',
                placeholderSubstitution: {
                    replacements: [
                        {
                            placeholder: 's0env_api_token',
                            valueTemplate: '{{ .token }}',
                            locations: ['query', 'header', 'body'],
                        },
                    ],
                },
            },
        },
    ],
});`
    },
    {
      label: "CLI",
      language: "bash",
      code: `if [ -z "$API_PORT" ]; then API_PORT=8080; fi
if [ -z "$API_TOKEN" ]; then API_TOKEN=docs-secret-token; fi
if [ -z "$SOURCE_NAME" ]; then SOURCE_NAME=api-token-source; fi

cat <<EOF > api-token-source.yaml
name: $SOURCE_NAME
resolverKind: static_headers
spec:
    staticHeaders:
        values:
            token: $API_TOKEN
EOF
s0 credential create -f api-token-source.yaml

cat <<EOF > placeholder-network.yaml
mode: block-all
egress:
    trafficRules:
        - name: allow-example-api
          action: allow
          cidrs:
              - $API_CIDR
          ports:
              - port: $API_PORT
                protocol: tcp
    credentialRules:
        - name: api-token
          credentialRef: api-token
          protocol: http
          ports:
              - port: $API_PORT
                protocol: tcp
          failurePolicy: fail-closed
credentialBindings:
    - ref: api-token
      sourceRef: $SOURCE_NAME
      projection:
          type: placeholder_substitution
          placeholderSubstitution:
              replacements:
                  - placeholder: s0env_api_token
                    valueTemplate: "{{ .token }}"
                    locations:
                        - query
                        - header
                        - body
EOF
s0 sandbox network update --policy-file placeholder-network.yaml -s "$SANDBOX_ID"`
    }
  ]}
/>

For raw HTTP, also set `SANDBOX0_API_URL`, `SANDBOX0_TOKEN`, and `TEAM_ID`.

```bash
# Requires jq.
if [ -z "$API_PORT" ]; then API_PORT=8080; fi
if [ -z "$API_TOKEN" ]; then API_TOKEN=docs-secret-token; fi
if [ -z "$SOURCE_NAME" ]; then SOURCE_NAME=api-token-source; fi
if [ -z "$PLACEHOLDER" ]; then PLACEHOLDER=s0env_api_token; fi

curl -fsS -X POST "$SANDBOX0_API_URL/api/v1/credential-sources" \
    -H "Authorization: Bearer $SANDBOX0_TOKEN" \
    -H "X-Team-ID: $TEAM_ID" \
    -H "Content-Type: application/json" \
    --data "$(jq -n \
        --arg name "$SOURCE_NAME" \
        --arg token "$API_TOKEN" \
        '{
            name: $name,
            resolverKind: "static_headers",
            spec: {
                staticHeaders: {
                    values: {
                        token: $token
                    }
                }
            }
        }')"

curl -fsS -X PUT "$SANDBOX0_API_URL/api/v1/sandboxes/$SANDBOX_ID/network" \
    -H "Authorization: Bearer $SANDBOX0_TOKEN" \
    -H "X-Team-ID: $TEAM_ID" \
    -H "Content-Type: application/json" \
    --data "$(jq -n \
        --arg apiCidr "$API_CIDR" \
        --argjson apiPort "$API_PORT" \
        --arg source "$SOURCE_NAME" \
        --arg placeholder "$PLACEHOLDER" \
        '{
            mode: "block-all",
            egress: {
                trafficRules: [
                    {
                        name: "allow-example-api",
                        action: "allow",
                        cidrs: [$apiCidr],
                        ports: [
                            {
                                port: $apiPort,
                                protocol: "tcp"
                            }
                        ]
                    }
                ],
                credentialRules: [
                    {
                        name: "api-token",
                        credentialRef: "api-token",
                        protocol: "http",
                        ports: [
                            {
                                port: $apiPort,
                                protocol: "tcp"
                            }
                        ],
                        failurePolicy: "fail-closed"
                    }
                ]
            },
            credentialBindings: [
                {
                    ref: "api-token",
                    sourceRef: $source,
                    projection: {
                        type: "placeholder_substitution",
                        placeholderSubstitution: {
                            replacements: [
                                {
                                    placeholder: $placeholder,
                                    valueTemplate: "{{ .token }}",
                                    locations: ["query", "header", "body"]
                                }
                            ]
                        }
                    }
                }
            ]
        }')"
```

After the policy is applied, the sandbox process can send the placeholder without reading the secret:

```bash
curl -fsS \
    -H "Authorization: Bearer s0env_api_token" \
    "$API_ORIGIN/echo?token=s0env_api_token" \
    --data "token=s0env_api_token"
```

The upstream service receives the resolved `API_TOKEN` value in the header, query parameter, and request body.

## Rule Fields

| Field | Required | Description |
|-------|----------|-------------|
| `credentialRef` | Yes | Binding reference to resolve |
| `protocol` | No | Supported values: `http`, `https`, `grpc`, `tls`, `ssh`, `socks5`, `mqtt`, `redis` |
| `domains` | No | Domain match list |
| `ports` | No | Port/protocol constraints |
| `httpMatch` | No | HTTP request-level matcher for `http`, `https`, and `grpc` rules |
| `tlsMode` | No | `passthrough` or `terminate-reoriginate` |
| `failurePolicy` | No | `fail-closed` or `fail-open` |
| `rollout` | No | `enabled` or `disabled` |

## HTTPS Trust Material

When a credential rule uses TLS interception such as `protocol: https`, `protocol: grpc`, or `protocol: tls` with `tlsMode: terminate-reoriginate`, Sandbox0 provides the ctld network runtime's MITM CA to sandbox containers and configures procd-managed processes to trust it automatically.

- File: `/var/run/sandbox0/networking/mitm-ca.crt`
- Env: `SANDBOX0_NETWORK_MITM_CA_FILE=/var/run/sandbox0/networking/mitm-ca.crt`

At procd startup, Sandbox0 writes a combined CA bundle containing the image's system roots plus the network-runtime MITM CA, then exports it through common TLS environment variables such as `SSL_CERT_FILE`, `NODE_EXTRA_CA_CERTS`, `REQUESTS_CA_BUNDLE`, `CURL_CA_BUNDLE`, `GIT_SSL_CAINFO`, and `AWS_CA_BUNDLE`. Applications normally do not need to handle the MITM CA themselves.

For HTTPS credential injection, the ctld network runtime supports both HTTP/1.1 and HTTP/2 on `terminate-reoriginate` connections. Downstream and upstream ALPN are negotiated independently, so an HTTP/2 client can still reach an HTTP/1.1-only HTTPS origin through the proxy. Ctld keeps gRPC HTTP/2-only and rejects an upstream connection that cannot negotiate `h2`.

## Example Policy

This policy allows access to the GitHub API, binds a token source, and injects an `Authorization` header for matching HTTPS requests:

```yaml
mode: block-all
egress:
    trafficRules:
        - name: allow-github-api
          action: allow
          domains:
              - api.github.com
          ports:
              - port: 443
                protocol: tcp
    credentialRules:
        - name: github-auth
          credentialRef: gh-token
          protocol: https
          domains:
              - api.github.com
          ports:
              - port: 443
                protocol: tcp
credentialBindings:
    - ref: gh-token
      sourceRef: github-source
      projection:
          type: http_headers
          httpHeaders:
              headers:
                  - name: Authorization
                    valueTemplate: "Bearer {{ .token }}"
```

Here, `{{ .token }}` is rendered from the `token` entry stored in the bound `static_headers` source. Keep the projection template keys aligned with the source contents.

## HTTP Request Matching

Use `httpMatch` when a credential should only be injected for specific API requests on a broader allowed destination. Non-matching requests still follow `trafficRules`, but do not resolve or inject the credential.

For `protocol: https` and `protocol: grpc`, request matching requires `tlsMode: terminate-reoriginate`.

When multiple `credentialRules` match the same destination, Sandbox0 keeps the destination-matching rules as candidates and evaluates their `httpMatch` blocks in declaration order after the HTTP request is visible. The first rule with a matching `httpMatch` is selected. A rule without `httpMatch` matches any remaining request, so place narrower path, header, method, or query rules before catch-all rules.

```yaml
mode: block-all
egress:
    trafficRules:
        - name: allow-github-api
          action: allow
          domains:
              - api.github.com
          ports:
              - port: 443
                protocol: tcp
    credentialRules:
        - name: github-write-auth
          credentialRef: gh-token
          protocol: https
          tlsMode: terminate-reoriginate
          domains:
              - api.github.com
          ports:
              - port: 443
                protocol: tcp
          httpMatch:
              methods:
                  - POST
                  - PUT
                  - PATCH
                  - DELETE
              pathPrefixes:
                  - /repos/
              headers:
                  - name: accept
                    values:
                        - application/vnd.github+json
credentialBindings:
    - ref: gh-token
      sourceRef: github-source
      projection:
          type: http_headers
          httpHeaders:
              headers:
                  - name: Authorization
                    valueTemplate: "Bearer {{ .token }}"
```

You can use that ordering to inject different credentials for different paths on the same host:

```yaml
mode: block-all
egress:
    trafficRules:
        - name: allow-github
          action: allow
          domains:
              - github.com
          ports:
              - port: 443
                protocol: tcp
    credentialRules:
        - name: github-emu-auth
          credentialRef: github-emu
          protocol: https
          tlsMode: terminate-reoriginate
          domains:
              - github.com
          ports:
              - port: 443
                protocol: tcp
          httpMatch:
              pathPrefixes:
                  - /emu-org/
        - name: github-cloud-auth
          credentialRef: github-cloud
          protocol: https
          tlsMode: terminate-reoriginate
          domains:
              - github.com
          ports:
              - port: 443
                protocol: tcp
          httpMatch:
              pathPrefixes:
                  - /cloud-org/
```

## SSH Transparent Proxy

SSH egress auth uses a transparent proxy model. The sandbox-side SSH client authenticates to the ctld network runtime with a fake public/private key pair, and ctld authenticates to the upstream SSH server with the private key stored in a `static_ssh_private_key` credential source.

The upstream private key is never written into the sandbox. The sandbox only needs the fake private key that matches `projection.sshProxy.sandboxPublicKeys`.

```yaml
mode: block-all
egress:
    trafficRules:
        - name: allow-git-ssh
          action: allow
          domains:
              - github.com
          ports:
              - port: 22
                protocol: tcp
          appProtocols:
              - ssh
    credentialRules:
        - name: git-ssh-auth
          credentialRef: git-ssh
          protocol: ssh
          domains:
              - github.com
          ports:
              - port: 22
                protocol: tcp
credentialBindings:
    - ref: git-ssh
      sourceRef: github-ssh-key
      projection:
          type: ssh_proxy
          sshProxy:
              upstreamUsername: git
              sandboxPublicKeys:
                  - ssh-ed25519 AAAA... sandbox-fake-key
              knownHosts:
                  - github.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA...
```

For Git-over-SSH, use the normal Git SSH URL and configure Git or OpenSSH in the sandbox to use the fake private key. The ctld network runtime terminates the sandbox SSH session, verifies the fake key, verifies the upstream host key against `knownHosts`, and re-originates the session with the stored upstream private key.

SSH does not carry a hostname in the protocol. When `credentialRules[*].domains` or hostname-based `knownHosts` entries are used for SSH, the ctld network runtime associates the SSH connection with the sandbox's recent DNS response for that destination IP. The association is per sandbox and expires with the DNS TTL. Direct IP connections, stale DNS entries, or DNS traffic that bypasses the ctld network runtime are treated as hostless SSH traffic.

## Update Runtime Policy

<Endpoint method="PUT">
/api/v1/sandboxes/{'{id}'}/network
</Endpoint>

<Tabs
  tabs={[
    {
      label: "Go",
      language: "go",
      code: `_, err = sandbox.UpdateNetworkPolicy(ctx, apispec.SandboxNetworkPolicy{
    Mode: apispec.SandboxNetworkPolicyModeBlockAll,
    Egress: apispec.NewOptNetworkEgressPolicy(apispec.NetworkEgressPolicy{
        TrafficRules: []apispec.TrafficRule{
            {
                Name: apispec.NewOptString("allow-github-api"),
                Action: apispec.TrafficRuleActionAllow,
                Domains: []string{"api.github.com"},
                Ports: []apispec.PortSpec{
                    {
                        Port: 443,
                        Protocol: apispec.NewOptString("tcp"),
                    },
                },
            },
        },
        CredentialRules: []apispec.EgressCredentialRule{
            {
                Name: apispec.NewOptString("github-auth"),
                CredentialRef: "gh-token",
                Protocol: apispec.NewOptEgressAuthProtocol(apispec.EgressAuthProtocolHTTPS),
                Domains: []string{"api.github.com"},
                Ports: []apispec.PortSpec{
                    {
                        Port: 443,
                        Protocol: apispec.NewOptString("tcp"),
                    },
                },
                FailurePolicy: apispec.NewOptEgressAuthFailurePolicy(apispec.EgressAuthFailurePolicyFailClosed),
            },
        },
    }),
    CredentialBindings: []apispec.CredentialBinding{
        {
            Ref: "gh-token",
            SourceRef: "github-source",
            Projection: apispec.ProjectionSpec{
                Type: apispec.CredentialProjectionTypeHTTPHeaders,
                HttpHeaders: apispec.NewOptHTTPHeadersProjection(apispec.HTTPHeadersProjection{
                    Headers: []apispec.ProjectedHeader{
                        {
                            Name: "Authorization",
                            ValueTemplate: "Bearer {{ .token }}",
                        },
                    },
                }),
            },
        },
    },
})
if err != nil {
    log.Fatal(err)
}`
    },
    {
      label: "Python",
      language: "python",
      code: `from sandbox0.apispec.models.sandbox_network_policy import SandboxNetworkPolicy

sandbox.update_network_policy(
    SandboxNetworkPolicy.from_dict({
        "mode": "block-all",
        "egress": {
            "trafficRules": [
                {
                    "name": "allow-github-api",
                    "action": "allow",
                    "domains": ["api.github.com"],
                    "ports": [{"port": 443, "protocol": "tcp"}],
                },
            ],
            "credentialRules": [
                {
                    "name": "github-auth",
                    "credentialRef": "gh-token",
                    "protocol": "https",
                    "domains": ["api.github.com"],
                    "ports": [{"port": 443, "protocol": "tcp"}],
                    "failurePolicy": "fail-closed",
                },
            ],
        },
        "credentialBindings": [
            {
                "ref": "gh-token",
                "sourceRef": "github-source",
                "projection": {
                    "type": "http_headers",
                    "httpHeaders": {
                        "headers": [
                            {
                                "name": "Authorization",
                                "valueTemplate": "Bearer {{ .token }}",
                            },
                        ],
                    },
                },
            },
        ],
    })
)`
    },
    {
      label: "TypeScript",
      language: "typescript",
      code: `await sandbox.updateNetworkPolicy({
    mode: 'block-all',
    egress: {
        trafficRules: [
            {
                name: 'allow-github-api',
                action: 'allow',
                domains: ['api.github.com'],
                ports: [{ port: 443, protocol: 'tcp' }],
            },
        ],
        credentialRules: [
            {
                name: 'github-auth',
                credentialRef: 'gh-token',
                protocol: 'https',
                domains: ['api.github.com'],
                ports: [{ port: 443, protocol: 'tcp' }],
                failurePolicy: 'fail-closed',
            },
        ],
    },
    credentialBindings: [
        {
            ref: 'gh-token',
            sourceRef: 'github-source',
            projection: {
                type: 'http_headers',
                httpHeaders: {
                    headers: [
                        {
                            name: 'Authorization',
                            valueTemplate: 'Bearer {{ .token }}',
                        },
                    ],
                },
            },
        },
    ],
});`
    },
    {
      label: "CLI",
      language: "bash",
      code: `# Structured flags: good for scripts and one-off updates
s0 sandbox network update --mode block-all \\
  --traffic-rule '{"name":"allow-github-api","action":"allow","domains":["api.github.com"],"ports":[{"port":443,"protocol":"tcp"}]}' \\
  --credential-binding '{"ref":"gh-token","sourceRef":"github-source","projection":{"type":"http_headers","httpHeaders":{"headers":[{"name":"Authorization","valueTemplate":"Bearer {{ .token }}"}]}}}' \\
  --credential-rule '{"name":"github-auth","credentialRef":"gh-token","protocol":"https","domains":["api.github.com"],"ports":[{"port":443,"protocol":"tcp"}],"failurePolicy":"fail-closed"}' \\
  -s sb_abc123

# Policy file: easier to read and review for larger policies
cat <<'EOF' > network.yaml
mode: block-all
egress:
    trafficRules:
        - name: allow-github-api
          action: allow
          domains:
              - api.github.com
          ports:
              - port: 443
                protocol: tcp
    credentialRules:
        - name: github-auth
          credentialRef: gh-token
          protocol: https
          domains:
              - api.github.com
          ports:
              - port: 443
                protocol: tcp
          failurePolicy: fail-closed
credentialBindings:
    - ref: gh-token
      sourceRef: github-source
      projection:
          type: http_headers
          httpHeaders:
              headers:
                  - name: Authorization
                    valueTemplate: "Bearer {{ .token }}"
EOF

s0 sandbox network update --policy-file network.yaml -s sb_abc123`
    }
  ]}
/>

<Callout variant="info">
For supported protocols, auth material is resolved and injected by the egress path. The sandbox process does not need direct access to the raw credential source contents.
</Callout>

## Next Steps

<CardGroup>
  <Card title="Overview" href="/docs/sandbox/template" cta="Continue">
    Define reusable sandbox environments before creating or scaling sandboxes.
  </Card>

  <Card title="Custom Images" href="/docs/sandbox/template/images" cta="Continue">
    Build and reference custom container images for sandbox templates.
  </Card>
</CardGroup>
