Skip to content

refactor(providers): introduce provider properties, sandbox hooks, and credential encryption #147

@drew

Description

@drew

Summary

The current provider implementation tightly couples provider credentials to environment variable names, offers no mechanism for providers to write config files into sandboxes, and stores credentials as plaintext in the database. This issue proposes three improvements to the provider system.

Context

Current State

  • Data model: A Provider has credentials: map<string, string> and config: map<string, string> (proto datamodel.proto:77-87). Credential keys are environment variable names (e.g., ANTHROPIC_API_KEY). There is no abstraction layer between what a provider "knows" and how it's projected into a sandbox.
  • Injection: The sandbox supervisor fetches credentials via GetSandboxProviderEnvironment gRPC, which returns a flat HashMap<String, String>. These are injected as env vars via Command::env() (process.rs:107, ssh.rs:682). There is no mechanism to write config files to disk.
  • Persistence: Credentials are stored as raw protobuf bytes in the objects table (payload BLOB/BYTEA). No encryption at rest. No encryption infrastructure exists in the codebase.
  • Hooks: ProviderPlugin::apply_to_sandbox() exists as a no-op stub (lib.rs:60-66). It is never called or overridden by any provider plugin.

Key Files

Area File Description
Plugin trait crates/navigator-providers/src/lib.rs:45-67 ProviderPlugin trait definition
Registry crates/navigator-providers/src/lib.rs:69-121 ProviderRegistry with all provider registrations
Discovery crates/navigator-providers/src/discovery.rs discover_with_spec() env/file scanning
Credential resolution crates/navigator-server/src/grpc.rs:1456-1498 resolve_provider_environment()
Sandbox startup crates/navigator-sandbox/src/lib.rs:117-502 run_sandbox() full sequence
Process spawn crates/navigator-sandbox/src/process.rs:88-190 spawn_impl() with env injection
Persistence crates/navigator-server/src/persistence/mod.rs:259-285 put_message()/get_message() — raw protobuf, no encryption
PKI bootstrap crates/navigator-bootstrap/src/pki.rs Cluster PKI generation
Architecture doc architecture/sandbox-providers.md Current design documentation

Proposed Changes

1. Provider Properties Abstraction

Decouple provider properties from environment variable names. Instead of credential keys being env var names directly, providers should have typed properties with explicit mappings.

Current flow:

Host env: ANTHROPIC_API_KEY=sk-abc
  → discover: credentials["ANTHROPIC_API_KEY"] = "sk-abc"
  → persist: credentials map with key "ANTHROPIC_API_KEY"
  → inject: cmd.env("ANTHROPIC_API_KEY", "sk-abc")

Proposed flow:

Host env: ANTHROPIC_API_KEY=sk-abc
  → discover FROM env var: property "api_key" = "sk-abc" (via env_var_mappings)
  → persist: properties map with key "api_key"
  → apply TO sandbox env vars: "api_key" → ANTHROPIC_API_KEY (via env_var_mappings)
  → apply TO config files: "api_key" used by hooks to write config

Each provider plugin should declare:

  • Properties it supports (e.g., api_key, token, endpoint, org_id)
  • Env var mappings: which env vars to discover FROM and which to project TO
  • Config file templates: what files to write in the sandbox (see hooks below)

This enables a single property value to be projected as both an env var AND a config file entry, and allows discovery from one env var name while projecting to a different one.

2. Sandbox Pre-Spawn Hooks

The sandbox supervisor should execute provider-specific hooks before spawning the child process, following the existing write_ca_files() pattern (lib.rs:177-211).

Design considerations:

  • Hooks run in the sandbox supervisor process (root), before privilege drop and sandboxing
  • Hooks can write arbitrary files to disk (e.g., .gitconfig, claude.json, config.yml)
  • Hooks must update the SandboxPolicy.filesystem.read_only paths so the sandboxed child can access written files
  • File paths should be passed to the child via env vars where appropriate

Architectural decision — where hook logic lives:

Currently navigator-sandbox does not depend on navigator-providers. The server resolves providers into a flat env var map and the sandbox only sees key-value pairs with no type information.

To enable provider-specific hooks, the system needs to communicate more than just env vars. Options include:

  1. Extend the gRPC response to include structured hook data (file templates, env var mappings) resolved server-side — keeps the sandbox "dumb"
  2. Add navigator-providers as a sandbox dependency so the sandbox can interpret provider types and run plugin hooks locally
  3. Hybrid: server sends properties + type metadata, sandbox has a lightweight hook executor

Use cases:

  • Claude: write ~/.claude.json with API key config
  • GitHub: write ~/.gitconfig with credential helper, ~/.config/gh/hosts.yml
  • GitLab: write ~/.config/glab-cli/config.yml
  • Generic: write arbitrary config files from provider config map

3. Credential Encryption at Rest

Provider credentials should be encrypted before being stored in the database.

Approach: Dedicated symmetric encryption key

  • Generate a symmetric encryption key (e.g., AES-256-GCM) at cluster bootstrap time
  • Store it as a new K8s secret (e.g., navigator-encryption-key)
  • Mount it to the navigator-server pod alongside existing TLS secrets
  • Encrypt credential values (or the entire credentials map) before put_message() serialization
  • Decrypt after get_message() deserialization

Why not use existing TLS keys:
The mTLS CA private key is not persisted on the cluster (explicitly set to empty on reload, bootstrap/src/lib.rs:683). The server TLS key could technically work, but coupling encryption to TLS cert rotation means rotating certs would make encrypted data unrecoverable without a re-encryption migration.

Implementation areas:

  • navigator-bootstrap: generate and store encryption key as K8s secret during reconcile_pki() or a new reconcile_encryption_key() step
  • navigator-server/src/persistence: add encrypt/decrypt layer around put_message()/get_message() for provider objects
  • Helm chart (deploy/helm/navigator/templates/statefulset.yaml): mount the new secret
  • Migration: handle existing unencrypted providers (detect and re-encrypt on first access, or a one-time migration)

Crypto dependencies: ring is already in the transitive dependency tree via rustls. It provides aead::AES_256_GCM which would be suitable. Alternatively, add aes-gcm or chacha20poly1305 as an explicit dependency.

Acceptance Criteria

  • Provider plugins declare typed properties with explicit env var discovery/projection mappings
  • Provider properties are persisted independently of env var names
  • Backward compatibility: existing providers continue to work (migration path for credential key format)
  • Sandbox supervisor executes provider-specific hooks before spawning the child process
  • Hooks can write config files to the sandbox filesystem following the write_ca_files() pattern
  • Written files are accessible to the sandboxed child via policy filesystem rules
  • A dedicated symmetric encryption key is generated at cluster bootstrap
  • Provider credentials are encrypted at rest in the database
  • Existing unencrypted credentials are migrated gracefully
  • Architecture doc (architecture/sandbox-providers.md) is updated to reflect the new design
  • Existing provider tests updated; new tests for properties mapping, hooks, and encryption

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions