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:
- Extend the gRPC response to include structured hook data (file templates, env var mappings) resolved server-side — keeps the sandbox "dumb"
- Add
navigator-providers as a sandbox dependency so the sandbox can interpret provider types and run plugin hooks locally
- 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
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
Providerhascredentials: map<string, string>andconfig: map<string, string>(protodatamodel.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.GetSandboxProviderEnvironmentgRPC, which returns a flatHashMap<String, String>. These are injected as env vars viaCommand::env()(process.rs:107,ssh.rs:682). There is no mechanism to write config files to disk.objectstable (payload BLOB/BYTEA). No encryption at rest. No encryption infrastructure exists in the codebase.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
crates/navigator-providers/src/lib.rs:45-67ProviderPlugintrait definitioncrates/navigator-providers/src/lib.rs:69-121ProviderRegistrywith all provider registrationscrates/navigator-providers/src/discovery.rsdiscover_with_spec()env/file scanningcrates/navigator-server/src/grpc.rs:1456-1498resolve_provider_environment()crates/navigator-sandbox/src/lib.rs:117-502run_sandbox()full sequencecrates/navigator-sandbox/src/process.rs:88-190spawn_impl()with env injectioncrates/navigator-server/src/persistence/mod.rs:259-285put_message()/get_message()— raw protobuf, no encryptioncrates/navigator-bootstrap/src/pki.rsarchitecture/sandbox-providers.mdProposed 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:
Proposed flow:
Each provider plugin should declare:
api_key,token,endpoint,org_id)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:
.gitconfig,claude.json,config.yml)SandboxPolicy.filesystem.read_onlypaths so the sandboxed child can access written filesArchitectural decision — where hook logic lives:
Currently
navigator-sandboxdoes not depend onnavigator-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:
navigator-providersas a sandbox dependency so the sandbox can interpret provider types and run plugin hooks locallyUse cases:
~/.claude.jsonwith API key config~/.gitconfigwith credential helper,~/.config/gh/hosts.yml~/.config/glab-cli/config.yml3. Credential Encryption at Rest
Provider credentials should be encrypted before being stored in the database.
Approach: Dedicated symmetric encryption key
navigator-encryption-key)navigator-serverpod alongside existing TLS secretsput_message()serializationget_message()deserializationWhy 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 duringreconcile_pki()or a newreconcile_encryption_key()stepnavigator-server/src/persistence: add encrypt/decrypt layer aroundput_message()/get_message()for provider objectsdeploy/helm/navigator/templates/statefulset.yaml): mount the new secretCrypto dependencies:
ringis already in the transitive dependency tree viarustls. It providesaead::AES_256_GCMwhich would be suitable. Alternatively, addaes-gcmorchacha20poly1305as an explicit dependency.Acceptance Criteria
write_ca_files()patternarchitecture/sandbox-providers.md) is updated to reflect the new design