Repository navigation
🤖 feat: verify a TLS trust in the shared Coder transport and move every operator client onto it - #434
Conversation
… Coder transport (#275) Change-Id: Ia6d57ba9bc939d05a095c607979b817ef9337365 Signed-off-by: Thomas Kosiewski <tk@coder.com>
Change-Id: I6c4c9b145e8680b0fec3d04cfb8d395d32263da9 Signed-off-by: Thomas Kosiewski <tk@coder.com>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
Security findingsAdvisory findings (1)
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
@codex security review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7eb80da739
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
🛡️ Codex Security Review
Here are some automated security review suggestions for this pull request.
Reviewed commit: 7eb80da739
ℹ️ About Codex security reviews in GitHub
This is an experimental Codex feature. Security reviews are triggered when:
- You comment "@codex security review"
- A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review
Once complete, Codex will leave suggestions, or a comment if no findings are found.
… every caller Change-Id: I7d0399f56324e15e20fcdc5a9a4fb27dfd8675f0 Signed-off-by: Thomas Kosiewski <tk@coder.com>
|
@codex review |
|
@codex security review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: fee7c6d37c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
🛡️ Codex Security ReviewSecurity review completed. No security issues were found in this pull request. Reviewed commit: Only the user who started this review can view the report in Codex. ℹ️ About Codex security reviews in GitHubThis is an experimental Codex feature. Security reviews are triggered when:
Once complete, Codex will leave suggestions, or a comment if no findings are found. |
Change-Id: I650b1c4f37095eefb2ae1d47911c83488d913d17 Signed-off-by: Thomas Kosiewski <tk@coder.com>
|
@codex review |
|
@codex security review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ae581e4066
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
🛡️ Codex Security ReviewSecurity review completed. No security issues were found in this pull request. Reviewed commit: Only the user who started this review can view the report in Codex. ℹ️ About Codex security reviews in GitHubThis is an experimental Codex feature. Security reviews are triggered when:
Once complete, Codex will leave suggestions, or a comment if no findings are found. |
Change-Id: I69584ace20ca19ce1856b47cd651671209999729 Signed-off-by: Thomas Kosiewski <tk@coder.com>
|
@codex review |
|
@codex security review |
|
Codex Review: Didn't find any major issues. Hooray! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
🛡️ Codex Security ReviewSecurity review completed. No security issues were found in this pull request. Reviewed commit: Only the user who started this review can view the report in Codex. ℹ️ About Codex security reviews in GitHubThis is an experimental Codex feature. Security reviews are triggered when:
Once complete, Codex will leave suggestions, or a comment if no findings are found. |
Summary
Part 1 of 3 for #275. The shared Coder transport (
coder.NewSDKClient) can now verify HTTPS with a TLS trust (a CA bundle and a server name), keyed per trust. The control-plane controller's license client and the CoderProvisioner key client move onto that transport, so every client that sends the operator token uses it. No caller passes a trust yet and the API does not change: part 2 addsspec.tls.operatorAccesstogether with the trust at every caller. The design below covers all three parts.Background
With built-in TLS (
spec.tls.secretNames), every operator component sends the site-owner operator token to Coder over plain HTTP on the control plane Service. The operator image trusts only public roots, and public CAs do not sign*.svc.cluster.local, so the operator cannot verifystatus.url(https://<cp>.<ns>.svc.cluster.local:443). #275 tracks verified in-cluster HTTPS.Design: verified in-cluster HTTPS for operator calls (#275)
API
operatorAccessset: every operator call with the operator token goes tostatus.urlover HTTPS. The operator verifies the Coder certificate with the CA bundle, or with the public roots of its image whencaBundleRefis absent.serverNamedefaults to the host ofstatus.url.operatorAccessunset: no change. The operator keeps the plain HTTP Service URL.operatorAccessneedssecretNames, and it needscaBundleReforserverName(public roots never sign the Service name).Decisions
operatorAccessset, the operator never sends the token over HTTP. A missing ConfigMap, a missing key, a bundle without a valid certificate, or astatus.urlthat is not HTTPS yet stop the call. A certificate error is an error, never a reason to retry on HTTP. Redirects that leave the HTTPS origin stay refused (sameorigin).controlplaneurl.NewTLS(pem, serverName)parses the bundle once into aTLSvalue (cert pool, server name, and a key with the SHA-256 of the bundle). Every PEM block must be a parseableCERTIFICATE, so a private key pasted into the ConfigMap is an error, not a silent skip.controlplaneurl.OperatorTLS(ctx, reader, cp)reads the ConfigMap and returns that value. Every caller gets its URL fromCoderAPIURLand its trust fromOperatorTLS.coder.NewSDKClient. Its transport registry keys each pool by origin plus trust (server name and CA hash). Two control planes with different CAs never share a pool, and a new CA gets a new pool. net/http verifies anhttps://proxy with the same TLS configuration, so a transport with a trust refuseshttps://proxies with a clear error.http://proxies keep working: the TLS connection to Coder goes through CONNECT, verified end to end.get/list/watch, so RBAC does not change. CA certificates are not secret, and trust-manager andkubectl create configmap --from-fileboth produce this shape.ControlPlaneNotReadywait with the CA error, CoderProvisioner reportsOperatorAccessReady=False(OperatorTLSUnavailable), and the aggregated API answers 503. The control plane gets anOperatorCABundleResolvedcondition (True/False withConfigMapNotFound,KeyNotFound,InvalidCABundle) and re-reconciles when the ConfigMap changes. A CA that parses but does not sign the Coder certificate shows as the x509 error of the failed calls (license condition, dependents'CoderUnavailable).status.url, which the opt-in does not change, so opting in rotates no key. The applied key's API URL becomesstatus.url. Astatus.pendingKeysrecord from before the opt-in holds the plain HTTP Service URL. For such a record, the operator callsstatus.urlinstead when the record's URL is the control plane's own internal HTTP URL (same Service, andtokenBoundToalready binds the record to the control plane UID). It refuses any other plain HTTP URL for a control plane withoperatorAccess.Delivery
The security property (no operator token over HTTP once
operatorAccessis set) lands whole: the API field arrives in the same PR that wires every caller.coder.NewSDKClient), so every operator-token client uses it. No API change.spec.tls.operatorAccess,OperatorTLS,CoderAPIURLreturning HTTPSstatus.url, the trust at every caller (controlPlaneCoderClientfor all CRD controllers, the aggregated API, the license and entitlements calls, CoderProvisioner key calls including records from before the opt-in), and docs.OperatorCABundleResolvedcondition with a ConfigMap watch, and a Kind E2E phase with built-in TLS on: the script makes a self-signed CA and a serving certificate with openssl, checks that the operator reaches Coder over HTTPS, and checks that a wrong CA stops the calls.Implementation
internal/controlplaneurl/tls.go:TLS, a parsed trust (cert pool, server name, key), built byNewTLS. The bundle must hold only PEM certificates: a private key block, a broken certificate, or a cut-off certificate after valid ones is an error.ClientConfigreturns a copy of the pool.internal/aggregated/coder/config.go:Config.TLS. The transport registry adds the trust key to the pool key, setsTLSClientConfigon a new transport, and refuseshttps://proxies for it. The client stack around the transport is unchanged (markMaybeSent, thesameoriginround tripper and redirect policy, connection wait).codercontrolplane_controller.go(newSDKLicenseClient) andcoderbootstrap(newAuthenticatedClient) build their clients withcoder.NewSDKClientinstead of a cloned default transport per call. They keep their timeouts, and the provisioner client keeps the rate-limit bypass header. This also adds thesameoriginround tripper to both.internal/tlstest: a test CA that signs HTTPS test servers, for this PR and the next two.Validation
config_tls_test.godrivesNewSDKClientagainst HTTPS servers signed by a test CA. The signing CA passes. Another CA, the system roots, and a wrong server name fail with a certificate error, and the operator token never reaches a server that failed verification.http://proxy carries the verified connection (and a wrong CA still fails through it), and that anhttps://proxy gets no request.tls_test.gocovers the bundle checks, including the cut-off certificate (the check fails without the trailing-data rule).make verify-vendor test build lint test-integration,make codegen manifests docs-reference build-installerwith no diff,make docs-check.Risks
markMaybeSentmarks. Their tests pass unchanged.Refs #275
Generated with
mux• Model:anthropic:claude-opus-5-5• Thinking:xhigh• Cost:$8.28