Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ jsonwebtoken = "9"
pulldown-cmark = "0.13.0"
hickory-resolver = "0.24.4"
socket2 = "0.6"
zeroize = "1.8.2"
zeroize = { version = "1.8.2", features = ["serde"] }

[build-dependencies]
# Doc comment scanner (build.rs extracts /// @ntnt blocks)
Expand Down
16 changes: 16 additions & 0 deletions docs/RUNTIME_REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ Environment variables that control NTNT runtime behavior
| `NTNT_LINT_MODE` | `default`, `warn`, `strict` | default | Controls lint strictness for type annotations. `default`: only check annotated code. `warn`: also warn about missing annotations (non-fatal). `strict`: missing annotations are errors. CLI flags (`--strict`, `--warn-untyped`) override this. |
| `NTNT_MAX_RECURSION` | integer | 256 | Maximum recursion depth for function calls. Prevents stack overflow from runaway recursion. |
| `NTNT_NET_ALLOW_PRIVATE` | `1`, `true`, `yes` | unset (disabled — private targets blocked) | Process-level opt-in for `std/net` probes (`ping`, `tcp_connect`, `reachable`, `port_scan`, `tls_info`, `traceroute`) against private/internal targets. Each call must also pass `allow_private: true`. Special-purpose targets such as cloud metadata, multicast, broadcast, unspecified, and documentation ranges remain blocked. This is separate from `NTNT_ALLOW_PRIVATE_IPS`, which only applies to `fetch()`. |
| `NTNT_SECRETS_AUTHORIZATION_SCOPE` | opaque ASCII deployment identifier | unset (required for unix-socket) | Trusted non-credential deployment scope expected in every secrets-agent response. It detects endpoint miswiring but is not authentication and is never rendered in diagnostics. Leading or trailing whitespace is rejected. |
| `NTNT_SECRETS_PROVIDER` | `env`, `unix-socket` | env | Selects the provider behind `std/secrets`. The exact-name environment provider is development-only and is rejected when `NTNT_ENV` is `production` or `prod`. `unix-socket` is Unix-only and talks to any local secrets agent implementing the documented protocol, without depending directly on a secrets backend. |
| `NTNT_SECRETS_SOCKET_ENDPOINTS` | comma-separated absolute Unix socket paths | unset (required for unix-socket) | Ordered local secrets-agent endpoints for secret lookup. One through eight unique paths are allowed. Each endpoint is tried twice, and failover occurs only for bounded unavailable results. Production paths must be beneath the host-controlled `/run/ntnt-secrets` root; raw paths are never included in runtime diagnostics. |
| `NTNT_SECRETS_TIMEOUT_MS` | integer (milliseconds) | 1000 | Per-attempt Unix-socket connect, write, and read timeout. Values must be between 10 and 10000 milliseconds. |
| `NTNT_STRICT` | `1`, `true` | unset (disabled) | **Deprecated — use `NTNT_LINT_MODE=strict` instead.** Enable strict type checking. Still works but emits a deprecation warning. |
| `NTNT_TIMEOUT` | integer (seconds) | 30 | Request timeout for HTTP server in seconds. |
| `NTNT_TYPE_MODE` | `strict`, `warn`, `forgiving` | warn | Controls runtime behavior for type mismatches. `strict`: type mismatches crash (fail-closed, recommended for auth/payments). `warn`: log `[WARN]` and continue (default). `forgiving`: silent degradation (pre-v0.4 behavior). See [Type Safety Modes](#type-safety-modes). |
Expand All @@ -54,6 +58,18 @@ NTNT_MAX_RECURSION=512 ntnt run server.tnt
# Process-level opt-in for `std/net` probes (`ping`, `tcp_connect`, `reachable`, `port_scan`, `tls_info`, `traceroute`) against private/internal targets
NTNT_NET_ALLOW_PRIVATE=1 ntnt run monitor.tnt

# Trusted non-credential deployment scope expected in every secrets-agent response
NTNT_SECRETS_AUTHORIZATION_SCOPE=deployment-a

# Selects the provider behind `std/secrets`
NTNT_SECRETS_PROVIDER=unix-socket ntnt run server.tnt

# Ordered local secrets-agent endpoints for secret lookup
NTNT_SECRETS_SOCKET_ENDPOINTS=/run/ntnt-secrets/secrets.sock

# Per-attempt Unix-socket connect, write, and read timeout
NTNT_SECRETS_TIMEOUT_MS=500

# **Deprecated — use `NTNT_LINT_MODE=strict` instead.** Enable strict type checking
NTNT_STRICT=1 ntnt run server.tnt

Expand Down
4 changes: 2 additions & 2 deletions docs/STDLIB_REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -10905,7 +10905,7 @@ get_secret(name: String) -> Option<Secret>

Looks up a secret by its provider-neutral logical name.

The v0.5.1 development-only environment provider reads the exact environment variable name and is disabled when `NTNT_ENV` is `production` or `prod`. Projects with an `ntnt.toml` must declare accessible names under `[secrets.<NAME>]`; undeclared lookups fail before contacting the provider. Declaration metadata may contain `label`, `description`, `required`, and `environments`; secret values are never accepted in the manifest. Secret values remain opaque and redact themselves in output and diagnostics.
The environment provider reads the exact environment variable name and is disabled when `NTNT_ENV` is `production` or `prod`. Unix deployments can instead select the generic local secrets-agent provider with `NTNT_SECRETS_PROVIDER=unix-socket`, a comma-separated list of absolute `NTNT_SECRETS_SOCKET_ENDPOINTS`, and the expected non-credential deployment identifier in `NTNT_SECRETS_AUTHORIZATION_SCOPE`. Optional `NTNT_SECRETS_TIMEOUT_MS` is bounded from 10 through 10000 milliseconds. Production socket paths must be beneath `/run/ntnt-secrets`; endpoints are retried twice and failed over only for bounded unavailable results. Plaintext caching remains in the host agent rather than ntnt. Projects with an `ntnt.toml` must declare accessible names under `[secrets.<NAME>]`; undeclared lookups fail before contacting the provider. Declaration metadata may contain `label`, `description`, `required`, and `environments`; secret values are never accepted in the manifest. Secret values remain opaque and redact themselves in output and diagnostics.

**Parameters:**

Expand All @@ -10921,7 +10921,7 @@ get_secret("STRIPE_SECRET_KEY") // => Some([REDACTED]) // Optional lookup

**Errors:**

- **RuntimeError**: Unsupported secrets provider — *Fix: Set NTNT_SECRETS_PROVIDER=env for v0.5.1*
- **RuntimeError**: Unsupported secrets provider — *Fix: Select env for development or unix-socket with deployment-scoped socket configuration*

**See also:** `require_secret`

Expand Down
28 changes: 28 additions & 0 deletions docs/runtime.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,34 @@ description = "Controls runtime mode. Production mode disables hot-reload for be
example = "NTNT_ENV=production ntnt run server.tnt"
affects = ["hot-reload"]

[env_vars.NTNT_SECRETS_PROVIDER]
values = ["env", "unix-socket"]
default = "env"
description = "Selects the provider behind `std/secrets`. The exact-name environment provider is development-only and is rejected when `NTNT_ENV` is `production` or `prod`. `unix-socket` is Unix-only and talks to any local secrets agent implementing the documented protocol, without depending directly on a secrets backend."
example = "NTNT_SECRETS_PROVIDER=unix-socket ntnt run server.tnt"
affects = ["std-secrets", "security"]

[env_vars.NTNT_SECRETS_SOCKET_ENDPOINTS]
type = "comma-separated absolute Unix socket paths"
default = "unset (required for unix-socket)"
description = "Ordered local secrets-agent endpoints for secret lookup. One through eight unique paths are allowed. Each endpoint is tried twice, and failover occurs only for bounded unavailable results. Production paths must be beneath the host-controlled `/run/ntnt-secrets` root; raw paths are never included in runtime diagnostics."
example = "NTNT_SECRETS_SOCKET_ENDPOINTS=/run/ntnt-secrets/secrets.sock"
affects = ["std-secrets", "security"]

[env_vars.NTNT_SECRETS_AUTHORIZATION_SCOPE]
type = "opaque ASCII deployment identifier"
default = "unset (required for unix-socket)"
description = "Trusted non-credential deployment scope expected in every secrets-agent response. It detects endpoint miswiring but is not authentication and is never rendered in diagnostics. Leading or trailing whitespace is rejected."
example = "NTNT_SECRETS_AUTHORIZATION_SCOPE=deployment-a"
affects = ["std-secrets", "security"]

[env_vars.NTNT_SECRETS_TIMEOUT_MS]
type = "integer (milliseconds)"
default = "1000"
description = "Per-attempt Unix-socket connect, write, and read timeout. Values must be between 10 and 10000 milliseconds."
example = "NTNT_SECRETS_TIMEOUT_MS=500"
affects = ["std-secrets", "security"]

[env_vars.NTNT_TIMEOUT]
type = "integer (seconds)"
default = "30"
Expand Down
92 changes: 92 additions & 0 deletions docs/secrets-agent-protocol.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# Local secrets-agent protocol v1

ntnt's `unix-socket` secrets provider can use any local agent that implements this protocol. The protocol is backend-neutral: an agent may obtain values from a managed secrets service, an operating-system key store, an HSM-backed service, or another deployment-owned source.

This is an integration contract for `std/secrets`; it is not a general socket API for ntnt programs.

## Transport

- Unix domain stream socket.
- One request and one response per connection.
- Each message is one UTF-8 JSON object followed by `\n`.
- The client half-closes its write side after the request.
- The agent should half-close its write side after the response. The newline completes the frame; the client does not wait for EOF, but rejects any trailing byte that is already available.
- Requests and responses must not use carriage returns or additional frames.
- Filesystem ownership and permissions authenticate the local endpoint. The protocol `scope` field detects routing mistakes but is not authentication.

## Request

```json
{"protocol":1,"request_id":1,"op":"get","name":"API_KEY","scope":"deployment-a"}
```

Fields are exact; unknown, duplicate, or missing fields are rejected.

| Field | Meaning |
|---|---|
| `protocol` | Integer protocol version; currently `1`. |
| `request_id` | Positive client-generated integer echoed by the response. It is correlation data, not a credential. |
| `op` | Operation; v1 supports only `get`. |
| `name` | Declared logical secret name. |
| `scope` | Deployment-owned authorization/routing scope configured outside application source. |

## Responses

Comment thread
greptile-apps[bot] marked this conversation as resolved.
Found:

```json
{"protocol":1,"request_id":1,"status":"found","scope":"deployment-a","value":"<opaque>"}
```

Non-value result:

```json
{"protocol":1,"request_id":1,"status":"missing","scope":"deployment-a"}
```

Supported statuses:

| Status | Meaning | Client retry/failover |
|---|---|---|
| `found` | A non-empty opaque value is present. | No |
| `missing` | The declared name has no value. | No |
| `access_denied` | The caller or deployment scope is not authorized. | No |
| `unavailable` | A bounded transient agent/backend outage. | Yes |
| `invalid_request` | The request is semantically invalid. | No |
| `invalid_configuration` | The agent cannot safely serve the configured deployment. | No |

Agents must not return backend error text, stack traces, paths, credentials, or secret fragments. ntnt validates the protocol version, request ID, exact scope, status shape, frame size, value size, and connection completion before exposing a value to `std/secrets`.

## Deployment configuration

```text
NTNT_SECRETS_PROVIDER=unix-socket
NTNT_SECRETS_SOCKET_ENDPOINTS=/run/ntnt-secrets/primary.sock,/run/ntnt-secrets/secondary.sock
NTNT_SECRETS_AUTHORIZATION_SCOPE=deployment-a
NTNT_SECRETS_TIMEOUT_MS=1000
```

Production endpoints must be below `/run/ntnt-secrets`. The deployment owner creates and protects that directory and its sockets; application code and `ntnt.toml` do not select endpoint paths or authorization scope.

The provider permits one through eight unique ordered endpoints, performs two bounded attempts per endpoint, and fails over only after `unavailable`. All endpoints in one configured group must represent the same authorization scope.

## Limits

| Limit | v1 value |
|---|---:|
| Response frame | 65,536 bytes |
| Decoded secret value | 32,768 bytes |
| Endpoints | 1–8 |
| Attempts per endpoint | 2 |
| Attempt timeout | 10–10,000 ms |

One monotonic deadline covers connect, request write, response read, and frame completion for each attempt. Partial responses cannot extend the deadline by making slow progress.

## Out of scope

- A plaintext secret cache inside ntnt.
- Remote TCP, HTTP, or WebSocket agents.
- Backend-specific behavior or dependencies.
- A general-purpose Unix-socket or WebSocket API for ntnt application code.

Those are separate security and language-design decisions rather than accidental side effects of the secrets-provider contract.
9 changes: 8 additions & 1 deletion src/secret.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,19 @@ pub struct SecretValue {

impl SecretValue {
/// Construct a secret after validating its logical name.
#[cfg(test)]
pub(crate) fn new(name: impl Into<String>, value: impl Into<String>) -> Result<Self> {
Self::new_zeroizing(name, Zeroizing::new(value.into()))
}

/// Construct a secret from provider-owned zeroizing storage without copying
/// plaintext through an ordinary String at the provider boundary.
pub(crate) fn new_zeroizing(name: impl Into<String>, value: Zeroizing<String>) -> Result<Self> {
let name = name.into();
validate_secret_name(&name)?;
Ok(Self {
name: Arc::from(name),
value: Arc::new(Zeroizing::new(value.into())),
value: Arc::new(value),
})
}

Expand Down
Loading
Loading