Skip to content

feat: add generic Unix-socket secrets provider#165

Open
larimonious wants to merge 7 commits into
mainfrom
feat/secrets-unix-socket-provider
Open

feat: add generic Unix-socket secrets provider#165
larimonious wants to merge 7 commits into
mainfrom
feat/secrets-unix-socket-provider

Conversation

@larimonious

Copy link
Copy Markdown
Contributor

Summary

  • add a backend-neutral unix-socket production provider for std/secrets
  • publish a protocol-v1 contract that any local secrets agent can implement
  • keep endpoint paths, authorization scope, retry policy, and backend choice outside application source
  • bound connect, write, read, frame, value, endpoint, and retry behavior
  • fail closed on malformed frames, unsafe paths, scope/ID mismatches, and terminal agent results

Language boundary

This PR does not add a Larri provider, Infisical dependency, or application-specific behavior. The public selector is NTNT_SECRETS_PROVIDER=unix-socket, the production trust root is /run/ntnt-secrets, and the protocol documentation describes a generic local agent.

It also does not expose a general socket API to ntnt programs. Unix domain sockets are an internal transport for the secrets-provider contract, where filesystem ownership and permissions authenticate a same-host endpoint. WebSockets are a separate remote, long-lived duplex networking feature and are not needed for bounded local secret lookup.

Protocol: docs/secrets-agent-protocol.md

Security properties

  • one strict newline-delimited request/response per connection
  • exact protocol version, request ID, logical name, and configured-scope validation
  • one monotonic deadline per complete provider attempt
  • maximum 65,536-byte response frame and 32,768-byte decoded value
  • production endpoint and parent-component checks beneath /run/ntnt-secrets
  • zeroizing response/value storage
  • diagnostics omit paths, scopes, response fragments, backend errors, and values
  • only unavailable retries or fails over
  • no plaintext cache inside ntnt

Verification

  • cargo test --all-targets
  • cargo build --profile dev-release
  • cargo fmt --all -- --check
  • generated docs + docs --validate
  • all examples validate
  • example lint has zero errors; existing warnings/suggestions remain
  • changed public files contain no Larri-specific names or paths

Platform note

The provider is Unix-only and fails closed on other targets. Linux tests pass locally; native CI is authoritative for macOS and Windows behavior.

@greptile-apps

greptile-apps Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a unix-socket secrets provider for std/secrets, backed by a documented generic protocol that any local agent (Vault, OS keystore, HSM proxy) can implement. It also adds scope to the request serialization struct and the integration test validates all 5 request fields.

  • New socket.rs: one-connection-per-lookup Unix stream protocol with a monotonic deadline covering connect, write, and read; Zeroizing storage through the entire value path; nonblocking trailing-byte check after frame receipt; symlink-rejection at both config-parse and connect time for production endpoints.
  • secrets.rs changes: parse_socket_provider_config validates path safety and authorization scope, configured_provider_group_from_values dispatches to SocketSecretProvider, and the ProviderLookup::Found payload is promoted from String to Zeroizing<String> to avoid a plaintext copy at the provider boundary.
  • secret.rs change: SecretValue::new is gated #[cfg(test)]; production callers use the new new_zeroizing constructor that accepts already-owned zeroizing storage.

Confidence Score: 5/5

The PR is safe to merge. The socket provider implementation is carefully bounded: one connection per lookup, a single monotonic deadline, zeroizing storage throughout, and fail-closed on every validation failure. The three concerns raised in prior review threads — missing scope in request serialization, WouldBlock misclassification, and deadline-expiry during the trailing-byte check — are all resolved in this version.

Protocol scope is now serialized in SocketRequest and verified by both unit and integration tests (5-field assertion). WouldBlock is explicitly matched alongside ConnectionReset/ConnectionAborted in the trailing-byte check, returning Ok(response) rather than InvalidConfiguration. The trailing-byte read is a single nonblocking call, not a spin loop, so deadline expiry is no longer a concern there. The one remaining minor point — set_nonblocking failure mapping to InvalidConfiguration instead of Unavailable — is negligible in practice and does not affect correctness of any realistic code path.

src/stdlib/secrets/socket.rs — the set_nonblocking error classification at the trailing-byte check (line 226) is the only open nit.

Important Files Changed

Filename Overview
src/stdlib/secrets/socket.rs New Unix-domain socket provider: deadline writer, per-attempt connect/write/read with bounded frame reading, nonblocking trailing-byte check, symlink re-validation at connect time. set_nonblocking failure is mapped to InvalidConfiguration (terminal) rather than Unavailable; otherwise framing and classification logic is correct.
src/stdlib/secrets.rs Socket provider config parsing, path/scope/timeout validation, dual symlink check (parse + connect), and ProviderLookup::Found upgrade to Zeroizing<String>. Endpoint deduplication, production-root enforcement, and group authorization-scope invariant all appear correct.
src/secret.rs Adds new_zeroizing constructor and gates new to #[cfg(test)], preventing plaintext String copies at the provider boundary in production.
docs/secrets-agent-protocol.md New protocol spec; request now includes scope field (matching the updated SocketRequest struct) and the integration test asserts all 5 fields are sent.
tests/language_features_tests.rs Integration test asserts the request object has exactly 5 fields (protocol, request_id, op, name, scope) and validates scope echo in response.
Cargo.toml Enables zeroize's serde feature so Zeroizing<String> can implement Deserialize for direct response deserialization.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant ntnt as ntnt runtime
    participant sg as secrets.rs (ProviderGroup)
    participant sp as SocketSecretProvider
    participant fs as Filesystem checks
    participant agent as Local secrets agent (Unix socket)

    ntnt->>sg: lookup_secret("API_KEY")
    sg->>sg: validate_secret_name, enforce_declared
    sg->>sg: configured_provider_group_from_values()
    sg->>fs: validate_existing_socket_path, validate_no_symlink_components (production)
    sg->>sp: SocketSecretProvider::new[_with_trusted_root]

    loop Up to 2 attempts per endpoint, failover on Unavailable only
        sp->>fs: symlink_metadata (re-validate at connect time)
        sp->>agent: connect (deadline-bounded)
        sp->>agent: "{protocol,request_id,op,name,scope} + newline"
        sp->>agent: shutdown(Write) half-close
        agent-->>sp: "{protocol,request_id,status,scope,value} + newline"
        sp->>sp: nonblocking read — reject any trailing byte
        sp->>sp: validate protocol, request_id, scope, frame size, value size
        sp-->>sg: "ProviderLookup::Found(Zeroizing<String>)"
    end

    sg->>sg: SecretValue::new_zeroizing (no plaintext copy)
    sg-->>ntnt: Some(SecretValue [REDACTED])
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 ntnt as ntnt runtime
    participant sg as secrets.rs (ProviderGroup)
    participant sp as SocketSecretProvider
    participant fs as Filesystem checks
    participant agent as Local secrets agent (Unix socket)

    ntnt->>sg: lookup_secret("API_KEY")
    sg->>sg: validate_secret_name, enforce_declared
    sg->>sg: configured_provider_group_from_values()
    sg->>fs: validate_existing_socket_path, validate_no_symlink_components (production)
    sg->>sp: SocketSecretProvider::new[_with_trusted_root]

    loop Up to 2 attempts per endpoint, failover on Unavailable only
        sp->>fs: symlink_metadata (re-validate at connect time)
        sp->>agent: connect (deadline-bounded)
        sp->>agent: "{protocol,request_id,op,name,scope} + newline"
        sp->>agent: shutdown(Write) half-close
        agent-->>sp: "{protocol,request_id,status,scope,value} + newline"
        sp->>sp: nonblocking read — reject any trailing byte
        sp->>sp: validate protocol, request_id, scope, frame size, value size
        sp-->>sg: "ProviderLookup::Found(Zeroizing<String>)"
    end

    sg->>sg: SecretValue::new_zeroizing (no plaintext copy)
    sg-->>ntnt: Some(SecretValue [REDACTED])
Loading

Reviews (3): Last reviewed commit: "fix: complete socket responses at newlin..." | Re-trigger Greptile

Comment thread docs/secrets-agent-protocol.md
Comment thread src/stdlib/secrets/socket.rs Outdated
Comment thread src/stdlib/secrets/socket.rs Outdated
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