Skip to content

feat: add provider-neutral std/secrets#161

Merged
larimonious merged 3 commits into
mainfrom
feat/std-secrets
Jul 13, 2026
Merged

feat: add provider-neutral std/secrets#161
larimonious merged 3 commits into
mainfrom
feat/std-secrets

Conversation

@larimonious

Copy link
Copy Markdown
Contributor

Summary

  • add provider-neutral std/secrets with get_secret and require_secret
  • add a first-class opaque Secret runtime/typechecker value with redacted display/debug and zeroized storage
  • require declared secret slots from [secrets.<NAME>] in ntnt.toml when a project manifest exists; reject values and undeclared access
  • add the HA provider boundary now: bounded per-endpoint attempts, transient-only retry/failover, terminal missing/denied/config errors, and one authorization scope across all failover endpoints
  • ship the environment provider as development-only; it is disabled when NTNT_ENV=production|prod
  • expose secrets only through explicit outbound HTTP sinks (headers, cookies, basic auth, raw body, JSON leaves, form fields) and disable redirects for secret-bearing requests
  • reject or redact secrets across templates, JSON/HTTP responses, logs, KV, jobs, CSV, URL query construction, collection sorting, stringification, binary operations, and debug/error paths
  • generate the stdlib reference for std/secrets and the updated HTTP sink semantics

Production boundary

This PR intentionally does not couple ntnt to Infisical. The production Larri Unix-socket provider/control plane remains a follow-up implementation behind the provider contract introduced here. No production provider silently falls back to environment variables.

Verification

  • cargo test --all-targets
  • cargo build --profile dev-release
  • cargo run --quiet -- docs --generate
  • cargo run --quiet -- docs --validate
  • cargo fmt --all -- --check
  • git diff --check
  • Clippy completed with zero diagnostics on added/changed lines; the repository still has pre-existing warnings outside this change
  • independent final security/code review: approved

@greptile-apps

greptile-apps Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR introduces std/secrets — a provider-neutral secret management module for ntnt — along with an opaque Secret runtime/type-checker value and a comprehensive set of guards that prevent secrets from escaping through any non-approved output path.

  • New std/secrets module (get_secret, require_secret) with an HA ProviderGroup that bounds per-endpoint attempts, treats only Unavailable as retriable, and makes Missing/AccessDenied/InvalidConfiguration terminal. The v0.5.1 environment provider is gated behind NTNT_ENV != production|prod.
  • Opaque SecretValue type backed by Arc<Zeroizing<String>> with redacted Display/Debug; secrets are exposed only at explicitly audited HTTP client sinks with redirect following disabled on any secret-bearing request.
  • Pervasive rejection/redaction guards across templates, std/json, std/kv, std/jobs, std/csv, std/url, std/string, binary operators, and HTTP server response paths.

Confidence Score: 5/5

Safe to merge. Secret plaintext only flows through explicitly audited outbound HTTP sinks; all other serialization paths reject or redact; manifest enforcement fails closed on every error condition.

Large security-critical addition with thorough defence-in-depth: expose() is pub(crate), RwLock poison recovery fails closed to Invalid, env provider is production-gated, and canary-value tests confirm secrets never appear in error messages or serialized output. The two flagged observations are forward-looking quality notes, not correctness gaps.

No files require special attention. src/stdlib/secrets.rs and src/secret.rs are the new security-critical core; both are well-tested and invariants are upheld consistently.

Important Files Changed

Filename Overview
src/stdlib/secrets.rs New module: provider-neutral secret lookup with HA failover boundary, manifest enforcement via configure_for_source, env-only development provider gated behind NTNT_ENV != production, and well-tested error classification.
src/secret.rs New module: opaque SecretValue type backed by Arc<Zeroizing> with redacted Display/Debug and crate-local expose() for audited sinks.
src/stdlib/http.rs Secret-aware HTTP client: approved sinks call expose(); redirect following disabled for any secret-bearing request; improved type validation throughout.
src/stdlib/json.rs Added SecretSerialization policy enum (Redact/Reject/Expose) and three public helpers; public serialization now rejects nested secrets.
src/stdlib/kv.rs serialize_value and serialize_value_envelope now return Result and reject secrets; value_to_json maps Value::Secret to [REDACTED] instead of Null.
src/interpreter.rs Added Value::Secret variant, contains_secret() recursive predicate, template guards at every interpolation site, and a binary-operator guard.
src/stdlib/http_server_async.rs create_json_response changed from -> Value to -> Result using intent_value_to_json_reject; new test verifies nested secrets are rejected.
src/stdlib/jobs.rs reject_secret_job_payload helper added and called in all three job-enqueue entry points with new canary test.
src/typechecker.rs Added Type::Secret, std/secrets module signatures, and guards rejecting user-defined Secret struct/enum/type aliases.
src/std_secrets_tests.rs Comprehensive integration tests covering redaction, canary containment, production-mode blocking, manifest enforcement, and template rejection.
src/stdlib/string.rs join() and concat() now reject secrets in both array and delimiter arguments.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant App as ntnt App Code
    participant Secrets as std/secrets
    participant Decl as DeclarationState (RwLock)
    participant PG as ProviderGroup
    participant Env as EnvSecretProvider
    participant HTTP as std/http.fetch()
    participant Net as Remote API

    App->>Secrets: require_secret("API_KEY")
    Secrets->>Secrets: validate_secret_name
    Secrets->>Decl: enforce_declared
    Decl-->>Secrets: Ok or Err
    Secrets->>PG: configured_provider_group().lookup
    loop per endpoint
        PG->>Env: lookup
        Env-->>PG: Found or Missing or Error
    end
    PG-->>Secrets: SecretValue (opaque)
    Secrets-->>App: Value::Secret([REDACTED])
    App->>HTTP: fetch with secret header
    HTTP->>HTTP: disable redirects
    HTTP->>HTTP: expose() at approved sink
    HTTP->>Net: request with plaintext
    Net-->>HTTP: Response
    HTTP-->>App: "Result<Response>"
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant App as ntnt App Code
    participant Secrets as std/secrets
    participant Decl as DeclarationState (RwLock)
    participant PG as ProviderGroup
    participant Env as EnvSecretProvider
    participant HTTP as std/http.fetch()
    participant Net as Remote API

    App->>Secrets: require_secret("API_KEY")
    Secrets->>Secrets: validate_secret_name
    Secrets->>Decl: enforce_declared
    Decl-->>Secrets: Ok or Err
    Secrets->>PG: configured_provider_group().lookup
    loop per endpoint
        PG->>Env: lookup
        Env-->>PG: Found or Missing or Error
    end
    PG-->>Secrets: SecretValue (opaque)
    Secrets-->>App: Value::Secret([REDACTED])
    App->>HTTP: fetch with secret header
    HTTP->>HTTP: disable redirects
    HTTP->>HTTP: expose() at approved sink
    HTTP->>Net: request with plaintext
    Net-->>HTTP: Response
    HTTP-->>App: "Result<Response>"
Loading

Reviews (3): Last reviewed commit: "fix: stabilize manifest declaration enfo..." | Re-trigger Greptile

Comment thread src/stdlib/secrets.rs
Comment thread src/stdlib/secrets.rs
Comment thread src/stdlib/secrets.rs
@larimonious

Copy link
Copy Markdown
Contributor Author

Addressed the second Greptile pass in 9a5f5f0: declaration identity is now always derived from the canonical source directory, so adding ntnt.toml after startup does not create a false cross-app conflict; poisoned declaration locks are recovered into an explicit Invalid fail-closed state and the poison is cleared so lookups report the manifest error. Added a regression for identity stability before/after manifest creation. Full cargo test --all-targets passes.

@larimonious larimonious merged commit 5cc850a into main Jul 13, 2026
9 checks passed
@larimonious

Copy link
Copy Markdown
Contributor Author

Late architecture-review follow-up in 3e94e48: cache_fetch() now rejects secret-bearing request options before URL-only cache lookup/storage; SQLite and PostgreSQL parameter conversion is fallible and recursively rejects Secret rather than persisting the redaction marker; task/channel serialization now has an explicit nested-secret canary regression. Full cargo test --all-targets, docs validation, dev-release build, changed-line Clippy, and focused review all pass.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant