Skip to content

🤖 feat: serve the aggregated API server certificate from a managed CA - #140

Merged
ThomasK33 merged 2 commits into
mainfrom
feat/apiserver-managed-serving-cert
Sep 25, 2026
Merged

ThomasK33 merged 2 commits into
mainfrom
feat/apiserver-managed-serving-cert

Conversation

@ThomasK33

@ThomasK33 ThomasK33 commented Sep 25, 2026 •

Copy link
Copy Markdown
Member

Refs #137. First of two PRs.

What changed

In a Pod, the aggregated API server now serves a certificate signed by its own CA instead of a throwaway self-signed localhost certificate. The CA and serving certificate live in a Secret the server manages. This PR does not change the APIService yet: it still has insecureSkipTLSVerify: true, so nothing verifies the new certificate until the second PR sets caBundle and turns verification on.

  • New package internal/aggregated/servingcert:
    • Creates coder-system/coder-k8s-apiserver-tls (type coder.com/aggregated-apiserver-serving-ca, app.kubernetes.io/* labels) with a 10-year CA and a 1-year serving certificate for coder-k8s-apiserver, .<ns>, .<ns>.svc and .<ns>.svc.cluster.local.
    • Adopts any valid existing Secret unchanged. Renews the serving certificate with the same CA at startup and every 12 hours when less than a third of its lifetime is left (or its names are for another namespace), and handles races with another replica.
    • Implements the vendored dynamiccertificates.CertKeyContentProvider, so the vendored serving-certificate controller swaps in a renewed certificate without a restart. Listeners are notified on change, which the second PR will use to keep caBundle in sync.
    • A corrupt Secret fails startup with a message naming the broken field (16 cases: wrong type, missing keys, unparsable PEM, not a CA, expired, mismatched keys, wrong signer). It is never overwritten.
  • Outside a Pod (no ServiceAccount namespace file), the old self-signed localhost certificate is kept.
  • Docs: how the certificate works, CA rotation (delete the Secret and restart), the corrupt-Secret error in troubleshooting, and that anyone who can read Secrets in coder-system can issue certificates this CA vouches for.

Validation

  • Unit tests for generation, Secret handling (create, adopt, reuse, renew, races, every corruption mode) and live TLS: verification succeeds with only the Secret's CA and the .svc name; it fails for another namespace's name and for a wrong CA; replacing the Secret swaps the served certificate without a restart.
  • 13 of 13 mutation checks caught (for example: dropping the .svc SAN, the self-signed default replacing the managed certificate, regenerating over a corrupt Secret, skipping the signature or key-match checks, renewal creating a new CA).
  • make verify-vendor, make build, make lint, make test, make test-integration, make docs-check, markdownlint and cspell pass.
  • Kind check, before coding: the API rejects caBundle together with insecureSkipTLSVerify: true, and accepts one merge patch that sets caBundle and insecureSkipTLSVerify: false. That is the patch the second PR will use.
  • Live Kind run with an image from this branch, built like the CI E2E: from another pod, curl --cacert <Secret CA> to the Service returns 200 and a wrong CA exits 60; the CA is reused after a restart; a corrupt Secret stops a new pod with the field named while the Secret stays unchanged; deleting the Secret and restarting rotates the CA. The APIService stayed as before and kubectl through it kept working; the existing auth check and workspace lifecycle driver passed.

Served certificate verifies against the Secret CA; wrong CA fails; CA reused on restart; rotation works; APIService unchanged

A corrupt Secret stops the server with the missing field named, and is not overwritten

Recording of the proof run (rendered from the terminal recording; idle time capped, so shorter than wall time):

137-pr1-proof.mp4

Next (second PR, which completes #137)

Keep the APIService caBundle in sync from the managed CA (watch-based, Event and log on failure, readiness not tied to it), add the narrow RBAC for that one APIService, drop insecureSkipTLSVerify from the manifest, and add CI checks for verification on and for a wrong CA.


Generated with xum • Model: anthropic:claude-opus-5-5 • Thinking: medium • Cost: $1777.85

Refs #137

In a Pod, the aggregated API server now serves a certificate signed by
its own CA instead of an in-memory self-signed localhost certificate.
This is the first half of #137; the APIService still uses
insecureSkipTLSVerify, and registering the CA in caBundle follows.

- New internal/aggregated/servingcert: the CA and serving certificate
  live in Secret coder-k8s-apiserver-tls (type
  coder.com/aggregated-apiserver-serving-ca, coder-k8s labels) in the
  pod's namespace. Generation uses client-go certutil/keyutil; the
  serving cert covers coder-k8s-apiserver, .<ns>, .<ns>.svc and
  .<ns>.svc.cluster.local.
- The Manager implements dynamiccertificates.CertKeyContentProvider
  and is set as SecureServingOptions.ServerCert.GeneratedCert, so the
  vendored DynamicServingCertificateController hot-swaps renewals.
  Listeners are notified on change (hook for the caBundle controller).
- Get-or-create adopts a Secret created by another replica; renewal
  (less than a third of the 1-year lifetime left, or SANs for another
  namespace) keeps the CA and adopts on update conflicts. Checked at
  startup and every 12h.
- An unusable Secret (wrong type, missing key, unparsable PEM, key and
  certificate mismatch, serving certificate not signed by the CA,
  expired or not-yet-valid CA) fails startup with a message naming the
  field; it is never overwritten.
- Outside a Pod the old self-signed localhost certificate is kept.
- Docs: serving certificate, rotation, CA-key warning, troubleshooting.

---
_Generated with `xum` • Model: `anthropic:claude-opus-5-5` • Thinking: `high`_
…er-runtime logger

Refs #137

Keeps k8s.io/klog/v2 an indirect dependency (make verify-vendor).

---
_Generated with `xum` • Model: `anthropic:claude-opus-5-5` • Thinking: `high`_
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-25T09:54:44.496522Z a7f93ae PR opened
🔒 Security Review ✅ Completed 2026-09-25T09:57:44.029203Z a7f93ae Manual request
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@ThomasK33

Copy link
Copy Markdown
Member Author

@codex security review

@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review

Security review completed. No security issues were found in this pull request.

Reviewed commit: a7f93ae9b0

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ 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.

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