Support runtime-resolved store-backed secrets without persisting plaintext

Author: roninjin10Created Jul 18, 2026Updated Sep 17, 2026
LabelsSecurityVulnerability

Environment

  • Microsandbox runtime and Go SDK: v0.6.6
  • Downstream provider: Plue's Go Microsandbox worker using github.com/superradcompany/microsandbox/sdk/go v0.6.6
  • Spike host: macOS 26.2 on Apple Silicon (arm64), Go 1.26.5, native HVF runtime
  • The rejection is in the shared Rust SDK core before hypervisor-specific execution, so it also applies to KVM hosts.

Minimal reproduction

Create a sandbox, then ask the Go SDK to plan a secret backed by the public Store source field:

bash
msb create alpine --name store-source-repro
mkdir store-source-repro && cd store-source-repro
go mod init store-source-repro
go get github.com/superradcompany/microsandbox/sdk/[email protected]
go
package main

import (
	"context"
	"fmt"
	"log"

	microsandbox "github.com/superradcompany/microsandbox/sdk/go"
)

func main() {
	ctx := context.Background()
	handle, err := microsandbox.GetSandbox(ctx, "store-source-repro")
	if err != nil {
		log.Fatal(err)
	}

	plan, err := handle.Modify(ctx, microsandbox.ModifyOptions{
		DryRun: true,
		Secrets: map[string]microsandbox.SecretModifySpec{
			"IRON_CAPABILITY": {
				Store:        "file:///run/iron-capabilities/workspace-repro",
				Placeholder:  "$IRON_CAPABILITY",
				AllowedHosts: []string{"iron.internal.example"},
			},
		},
	})
	if err != nil {
		log.Fatal(err)
	}
	for _, conflict := range plan.Conflicts {
		fmt.Println(conflict.Message)
	}
}

go run . prints:

secret IRON_CAPABILITY: store-backed secret sources are not supported yet

The shared contract defines SecretSource::Store { reference }, and the Go SDK serializes SecretModifySpec.Store as {"kind":"store","reference":"..."}. The Rust core then rejects that variant in modification planning, live resolution, and start-time config resolution. The same source can therefore be represented by SDK callers but cannot be used.

Expected

Microsandbox should persist only the opaque store reference, resolve it on the host when a sandbox starts or a secret rotates, and pass the resolved value through the existing private launch-config or live secret-update channel. The guest should receive only its placeholder. The resolved value should not appear in the durable sandbox config, database, inspect output, logs, errors, process arguments, guest disk, or snapshots.

A cold start should resolve the reference again so a rotated short-lived capability is used without rewriting durable sandbox state. A dry run should validate the reference shape and resolver availability without reading the secret.

Actual

SecretSource::Store is exposed by the shared types and SDK modification APIs, but Microsandbox v0.6.6 reports it as unsupported. The raw Go creation helper Secret.Env accepts the credential value and persists that value in the durable sandbox config. An environment-variable source avoids that persistence, but it requires the value to be placed in the host process environment and does not provide a per-sandbox external-store contract.

Impact on downstream users

Downstream control planes cannot safely connect a runtime credential broker such as Iron to Microsandbox. Iron can issue a short-lived proxy capability with method and path limits, audit, signing, approval, MCP policy, rate limits, and response filtering, but there is no supported way to resolve that capability for one sandbox without either placing plaintext in durable Microsandbox state or managing per-sandbox secrets through the host process environment.

Plue therefore fails secret-carrying exec requests before calling the SDK and returns 501 secret_delivery_unavailable. Persistent services that need a credential after cold recovery cannot be restarted automatically and enter an explicit degraded / secrets_required state. This affects any downstream user integrating Vault, cloud secret managers, Kubernetes secret mounts, or a credential proxy, not only Iron.

Proposed fix and API change

Implement SecretSource::Store behind a host-side resolver registry. A resolver should accept the opaque reference and return a zeroizing value. Runtime configuration should register resolvers by URI scheme, for example file, vault, or an application-owned iron scheme. Ship a file resolver first, scoped to an operator-configured root and using no-follow, traversal-safe reads, so Kubernetes or local secret mounts work without a language-specific callback.

Resolve store references at create/start and during source-based live rotation. Persist only SecretSource::Store { reference }. Use the existing ephemeral config clone and private launch-config file descriptor for start, and the existing live secret control socket for rotation. Errors, plans, and debug output should contain only the secret name and reference, never the resolved value.

Expose creation parity in every SDK. For Go, add a helper such as Secret.Store(envVar, reference, SecretStoreOptions) alongside Secret.Env, while retaining SecretModifySpec.Store for rotation. A conformance test should change the backing store value, stop and start the sandbox, confirm that the new value is substituted only for an allowed host, and scan durable config, inspect output, logs, guest files, process environments, and snapshots for both values.

Source: superradcompany/microsandbox