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.

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.