Actions Developer Guide

Security and Secrets

Learn how Rescile handles secrets securely, including the Offline Sealed Secrets architecture using X25519.

Action Security and Secrets Management

Rescile treats actions as untrusted, isolated execution boundaries. A core part of this isolation is ensuring that sensitive data is securely delivered to the runner without exposing it to the underlying operating system or logging infrastructure prematurely.

Secret Injection via Vault

When an action executes, the Rescile Controller can dynamically fetch secrets from the connected rescile-vault. In your action definitions, you can reference these secrets using the ${secrets.<secret_name>} syntax within your [runtime] configurations.

To instruct the Controller on exactly which Vault collection contains the required secrets, you must explicitly declare a [vault_secrets] mapping in your action TOML.

name = "deploy_app"
description = "Deploys the application and authenticates with the internal API"

# Explicitly map the local secret keys to their corresponding Vault collections
[vault_secrets.api_key]
collection = "production_api_credentials"
secret = "api_key"

[vault_secrets.db_pass]
collection = "shared_database_credentials"
secret = "postgres_password"

[runtime]
engine = "http"

[[runtime.steps]]
method = "POST"
url = "https://api.internal/v1/auth"
# The injected secret is available using the exact key defined above
headers = { "Authorization" = "Bearer ${secrets.api_key}" }

When this action is invoked, the caller does not need to provide api_key or db_pass in the input payload. Instead, the Controller reads the [vault_secrets] block, securely fetches the material from the designated collections, and resolves the templates before execution.

Automatic Runner Credential Injection

When a Rescile Agent runner is started with a Controller API Token (e.g., via --api-token or RESCILE_API_TOKEN), the runner securely injects its own connection credentials directly into the action’s execution context.

This enables actions to securely query or update the Controller’s GraphQL and REST APIs without requiring you to manually configure or map Vault secrets.

These credentials are automatically exposed in two ways:

  1. Template Variables: Available in the http engine or configuration templates as ${secrets.rescile_api_token} and ${secrets.rescile_controller_url}.
  2. Environment Variables: Automatically exported in system, nix, and container runtimes as $RESCILE_API_TOKEN and $RESCILE_CONTROLLER_URL.
[runtime]
engine = "http"

[[runtime.steps]]
method = "POST"
url = "${secrets.rescile_controller_url}/api/graphql"
headers = { "Authorization" = "Bearer ${secrets.rescile_api_token}" }
graphql_query = """
  query {
    server { id name status }
  }
"""

No vault configured? Without a connected vault, ${secrets.<name>} templates are not resolved — the literal placeholder string (e.g. ${secrets.aws_access_key_id}) is rendered into the bundle, and downstream calls fail at execution time. Seed the vault (rescile-ce serve --vault vault.json / VAULT_JSON_PATH) before triggering secret-dependent actions, and design scripts to fail visibly on unresolved credentials.

Offline Sealed Secrets (X25519)

In distributed, edge, or air-gapped environments, transmitting plaintext secrets (even over TLS) or storing them inside the action’s manifest.json artifact can be a significant security risk.

To solve this, Rescile implements an Offline Sealed Secrets architecture using X25519 asymmetric cryptography.

How Sealed Secrets Work

  1. Key Generation: When rescile-runner starts in Agent or Watch mode (or executes a manual bundle), it automatically generates an X25519 key pair. The private key is securely kept in memory and on disk at ~/.rescile/runner.key.
  2. Key Transmission: The runner transmits its X25519 public_key to the Controller via the secure gRPC CapabilityProfile (in Agent mode) or as an HTTP header/payload (in fetch mode).
  3. Dynamic Encryption: When an action is dispatched to that specific runner, the Controller fetches the required Vault secrets and dynamically encrypts them using the runner’s public_key. The resulting encrypted blob is attached to the action manifest as a sealed_secrets payload, completely omitting the plaintext secrets.
  4. In-Memory Decryption: Upon receiving the bundle (either via streaming or manual transfer), the runner uses its private key to decrypt the sealed_secrets entirely in memory just milliseconds before the execution engine template rendering occurs.

Why this matters:

  • Air-Gapped Delivery: You can safely download an action bundle as a tar.gz archive, place it on a USB drive, and walk it over to an air-gapped machine. The secrets inside the bundle are sealed and useless to anyone who intercepts the archive. They can only be unsealed by the specific runner instance whose public key was used to generate the bundle.
  • Zero-Trust Dispatch: The controller acts as the orchestrator, but the actual plaintext secret is only ever held in memory within the execution sandbox boundary.

Runtime Isolation Constraints

Beyond secrets, Rescile enforces security via engine isolation:

  • WASM (wasi): Complete I/O and CPU sandbox. No host OS access.
  • Nix (nix): Reproducible, hermetic shell bounds. No dependency bleed.
  • Container (container): Ephemeral namespaces and cgroups.

To learn more about engine choices, see Pluggable Runtimes.