Skip to content

Decide consumer stream TLS: native termination, or operator-provided proxy (ADR-0011) #70

Description

@CryptoFewka

Context

ADR-0011 requires TLS on the consumer stream but leaves the provider open. From Exposure requirement:

Any non-loopback bind (stream_addr / stream_grpc_addr beyond 127.0.0.1) requires TLS, native or a trusted TLS-terminating proxy.

and again in Consequences, listing "TLS (proxy or native)" among the mitigations. Since the choice was left open, it may be worth closing it before the stream stack (#57, #65, #68) reaches main, because neither option has been taken yet.

Current state

  • No TLS is served on either transport. WS is httpSrv.ListenAndServe(); the gRPC server added in feat: add consumer block-stream gRPC transport #68 is grpc.NewServer() with no credentials. There is no cert or key config in pkg/config.
  • Nothing enforces the requirement. validateStreamListener covers only the auth rule from the sentence that follows it in the ADR (!requireAuth && !isLoopbackHost), so a non-loopback bind without TLS passes startup validation.
  • The defaults are 0.0.0.0:9600 and 0.0.0.0:9601, exactly the non-loopback case the ADR says requires TLS. OPT_STREAM_ENABLE=true alone therefore yields a plaintext consumer stream on all interfaces, with the consumer JWT crossing the wire in the clear.
  • Nothing outside the ADR documents the ports, so the Helm guide from docs: add kubernetes Helm guide #66 does not tell an operator that a terminating proxy is expected. The runbook in Test: consumer block-stream API token flow (ADR-0011) #69 uses grpcurl -plaintext.

Options

1. Native TLS in the gateway. Config for cert and key paths, ServeTLS for WS, credentials.NewTLS for gRPC. Self-contained and correct by default, at the cost of certificate lifecycle in the gateway (renewal, reload on rotation, one more failure mode in the container).

2. Proxy-only, with the requirement made unavoidable. Both listeners stay plaintext, but the unsafe configuration stops starting silently. Since the process cannot detect an upstream proxy, the honest form is an explicit acknowledgement, for example OPT_STREAM_INSECURE_NO_TLS defaulting to false and required for a non-loopback bind. Certificate handling stays out of the gateway while plaintext becomes a deliberate choice rather than the default. Framed as opt-in-to-insecure rather than opt-in-to-TLS on purpose: an affirmative "TLS is present" flag would be equally unverifiable, and a false default is the safer shape.

3. Operator responsibility, documented only. Status quo, with the Helm guide and config reference stating that a TLS-terminating proxy is required in front of stream_addr and stream_grpc_addr. Cheapest and consistent with how the ADR frames it, but a misconfiguration stays silent and looks like a working deployment.

These are not exclusive. 2 and 3 together are probably the smallest change that closes the gap, with 1 available later if operators want native termination.

Why it matters more here than for the gateway's own credentials

A consumer JWT is a bearer credential handed to a third party, and the gateway verifies locally and never introspects, so the token's TTL is the entire revocation window. optimum-auth sets that to 1h by default for stream tokens specifically, deliberately shorter than the 6h gateway token, on the grounds that a credential given to a third party needs a tighter window. Leaking one in transit is correspondingly more costly.

Ask

Which of the three is intended. Whichever it is, the gap worth closing before the stack merges is that nothing currently distinguishes "correctly fronted by a proxy" from "plaintext on 0.0.0.0".

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestquestionFurther information is requested

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions