Skip to content

Credentials as placeholders, put on by the proxy for bound hosts only - #42

Merged
O6lvl4 merged 2 commits into
developfrom
feat/credential-broker-37
Sep 28, 2026
Merged

O6lvl4 merged 2 commits into
developfrom
feat/credential-broker-37

Conversation

@O6lvl4

@O6lvl4 O6lvl4 commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Closes #37.

What it does

  • --credential NAME=HOST[,HOST] (repeatable) and [[credentials]] in porta.toml (name, from = { env = "..." }, hosts = ["host[:443][/path]"]). The command's NAME is porta-cred-NAME-<random>, minted per run. The real value is read from porta's environment and never enters the child's environment or any log.
  • A credential turns the proxy on:
    • With no --proxy-allow, every host is reachable through it.
    • With an allow-list, the bound hosts are added to it.
    • The child gets HTTPS_PROXY as today.
  • For a bound host only, porta terminates the command's TLS with a leaf certificate from a CA made for this run. It replaces the placeholder wherever it appears in the request line, query or headers, so Authorization: Bearer … and x-api-key both work, and sends the request on over its own TLS connection verified against the webpki roots.
  • Fail-closed: the placeholder in a request to an intercepted host on a path its credential is not bound to gets a 403. Nothing is sent upstream, and the refusal is recorded.
  • Audit: substitute and deny decisions go to stderr and the --proxy-audit JSONL with the credential's name, never its value.
  • Trust:
    • SSL_CERT_FILE, REQUESTS_CA_BUNDLE, CURL_CA_BUNDLE and GIT_SSL_CAINFO point at a bundle holding the system roots plus the run CA, because those variables replace the trust store.
    • NODE_EXTRA_CA_CERTS points at the CA alone.
    • The CA is valid for a week, and its files under /tmp/porta-credentials-* are removed when the proxy stops.
  • Unbound hosts keep the plain CONNECT tunnel, untouched and unread.
  • explain --save writes [[credentials]] tables: where each credential goes and which variable it comes from, never the value.

Design notes

  • native/credential_broker.rs holds the placeholders and bindings, the head rewrite, the CA and leaf minting, and the intercept/forward loop.
  • rustls 0.23 and rcgen 0.13 run on ring, the provider reqwest's rustls already builds. There is no second crypto library and no OpenSSL.
  • Each intercepted connection carries one request. porta asks the server for Connection: close and closes the client's side after the response, so it never has to find response boundaries. ALPN offers http/1.1 only.
  • Leaf certificates carry an Authority Key Identifier, which Python 3.13+ strict verification requires.
  • Test hook: PORTA_TEST_UPSTREAM=host=127.0.0.1:port,... and PORTA_TEST_UPSTREAM_CA are read from porta's own environment, not the child's. They let the integration suite send a CONNECT to a local TLS server past the address guard. They are documented at the function.

Left for later

  • Credential sources other than env: file, Keychain, command.
  • A header field that puts the key on a named header even when the request does not carry the placeholder.
  • Scanning request bodies for placeholders; bodies are passed through.
  • HTTP/2 and keep-alive to bound hosts.
  • Restricting by the connecting binary (binaries = [...], issue item 5).

Verified (macOS, Apple silicon, pinned Almide 0.63.0)

  • almide test: 163 tests in 9 files pass, including new proxy/CLI tests (a credential enables the proxy, --credential parsing, explain --save output).
  • Rust unit tests for the broker (run with cargo test credential_broker in the generated crate): 5 passed. They cover binding parsing and the non-443 refusal, bound/unbound substitution, pass-through with no placeholder, the outgoing head and framing, and date arithmetic.
  • scripts/integration.py: 29 PASS, and 1 SKIP that is not new (the WASI component test skips when .tools/almide is absent from the copy). The new end-to-end test covers:
    • the env holds only the placeholder;
    • the bound request reaches a local HTTPS server with the real key;
    • /admin gets 403 and never reaches the server;
    • an unbound host is tunnelled and verified by the client against the test CA directly, carrying the placeholder;
    • the audit has substitute and deny lines, and the value appears in neither the audit, stderr nor stdout;
    • an unset variable and a non-443 binding are refused before the run.
  • scripts/escapes.py: every attempt held.
  • By hand, against a real host:
    • macOS /usr/bin/curl through --credential BOGUS_KEY=api.anthropic.com got 401 from Anthropic, meaning the placeholder was replaced and upstream TLS was verified;
    • https://example.com in the same run was tunnelled and verified (200);
    • porta up with a [[credentials]] table works, and the CA directory is gone afterwards.
  • CI passes on ubuntu-latest and macos-latest: build, No OpenSSL linked, unit tests, integration, escapes, monkey, and the code-quality floor, now 90 (A). The first push fell to 89; the second commit splits the broker into credential_broker/{tls,exchange,tests}.rs with no change in behaviour.

🤖 Generated with Claude Code

O6lvl4 and others added 2 commits September 28, 2026 09:52
`--credential NAME=HOST[/path]` and `[[credentials]]` hand the command a
placeholder, porta-cred-NAME-<random>, instead of the key. The real value,
read from porta's environment, stays in porta. A credential turns the
proxy on; for a bound host the proxy terminates the command's TLS with a
certificate from a CA made for this run, puts the value where the
placeholder is (request line, query, headers), and sends the request on
over its own TLS connection verified against the webpki roots. The same
placeholder on a path it is not bound to is refused with 403 before
anything is sent. Substitutions and refusals are recorded by name,
never by value. Hosts no credential names are tunnelled as before.

The command trusts the CA through SSL_CERT_FILE, REQUESTS_CA_BUNDLE,
CURL_CA_BUNDLE and GIT_SSL_CAINFO (system roots plus the CA) and
NODE_EXTRA_CA_CERTS (the CA alone). The CA lives for a week, in memory;
its files are removed when the proxy stops.

rustls and rcgen on ring, the provider reqwest already builds: no second
crypto library and no OpenSSL. PORTA_TEST_UPSTREAM(_CA) lets the
integration suite send a CONNECT to a local server; it is read from
porta's own environment.

Closes #37

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
CI's code-quality floor (90) failed at 89: credential_broker.rs was one
488-line file with a deeply nested body copier and five-parameter
functions, http_proxy.rs had passed 300 lines, and so had cli.almd.

- credential_broker.rs keeps the credentials and where each may go;
  credential_broker/tls.rs makes the run's CA, its leaves, the trust
  bundle and the upstream verifier; credential_broker/exchange.rs carries
  one intercepted request; the unit tests move to credential_broker/tests.rs.
  A Target (host, port) replaces the separate arguments.
- The tunnel and wt_is_host_allowed move beside the dial in
  proxy_egress.rs; the proxy's setup with credentials is one function.
- The TOML quoting helpers move to util.almd; --credential parsing and the
  saved [[credentials]] tables to proxy.almd.

No behaviour change: 163 Almide tests, 5 broker unit tests, the whole
integration suite and escapes.py pass as before; codopsy grades 90 (A).

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
@O6lvl4
O6lvl4 merged commit d51cccb into develop Sep 28, 2026
4 checks passed
@O6lvl4
O6lvl4 deleted the feat/credential-broker-37 branch September 28, 2026 02:39
O6lvl4 added a commit to O6lvl4/comide that referenced this pull request Sep 28, 2026
onogoro's default hands golemide placeholders through porta's credential
broker, which needs porta with --credential (almide/porta#42) and an Almide
whose HTTP client honours HTTPS_PROXY and a proxy's CA (almide/almide#2833).
Neither is released, so the bundle names the commits. porta's develop is
rustls only, so the sed for #35 goes; the build checks --credential is there.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
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.

Hand credentials in as placeholders bound to hosts, and put the real key on in the proxy

1 participant