Skip to content

fix: renew authn's own client certificate and clamp the login JWT to it (#28) - #40

Merged
braghettos merged 5 commits into
mainfrom
fix/certificate-renewal
Oct 29, 2026
Merged

braghettos merged 5 commits into
mainfrom
fix/certificate-renewal

Conversation

@braghettos

Copy link
Copy Markdown
Collaborator

Closes #28.

Summary

This ports krateoplatformops#76 (by @EdmondDantes21, merged in the old repo on 2026-09-07). It's the only commit from the old repo that never reached this one; the fix itself is theirs.

  • authn's own certificate (authn-clientconfig) is renewed in the background (internal/certrenewal). It's issued at boot and re-issued at AUTHN_CRT_RENEWAL_THRESHOLD (default 0.66) of the granted lifetime. Before this, it was written once and expired in place, breaking every RESTAction call until the pod restarted.
  • The granted window is recorded. Every clientconfig Secret is annotated with authn.krateo.io/certificate-not-{before,after}, and authn logs a warn when the signer grants under 90% of the request (OpenShift caps at 720h).
  • Every login JWT is clamped to its certificate's real NotAfter (encode.tokenDuration), so a session can't outlive its credential. This now includes the serviceaccount strategy, which Smoke-check contract for a roll; /strategies answers application/json #76 predates.
  • New knobs, defaulting to today's behaviour: AUTHN_SERVICE_CRT_EXPIRES_IN (8760h), AUTHN_CRT_RENEWAL_THRESHOLD (0.66), AUTHN_CRT_RENEWAL_ENABLED (true). They're in the chart's env and typed in values.schema.json; the schema rejects a threshold outside (0,1).
  • No new RBAC.

Adaptation choices

Test plan

🤖 Generated with Claude Code

https://claude.ai/code/session_01TLFnUe4HXFphxQNAfqFawZ

EdmondDantes21 and others added 4 commits October 29, 2026 15:00
…it (#28)

Port of krateoplatformops#76 (5088c74), which never reached this repo.

authn-clientconfig was written once at boot and never renewed, so a pod that outlived
its certificate failed every RESTAction call with an x509 expiry. The signer may also
grant much less than requested (OpenShift: 720h cap, 30-day CA rotation).

- Read the granted NotAfter and record it as authn.krateo.io/certificate-not-{before,after}
  Secret annotations; warn when the grant is under 90% of the request.
- Renew authn-clientconfig in the background (internal/certrenewal) at
  AUTHN_CRT_RENEWAL_THRESHOLD of the granted lifetime.
- Clamp every login JWT to its certificate's real NotAfter (encode.tokenDuration).

Adapted to this repo:
- go/authn layout and krateo-platformops module paths
- RS256 signing: Extras keeps JwtPrivateKey/JwtKeyID and adds CertNotAfter
- the clamp also covers the serviceaccount strategy, which #76 predates
- knobs in chart values and schema
- docs/certificate-renewal.md replaces #76's README section; architecture, behavior,
  configuration, gotchas, index, llms.txt and log are updated
- #76's CI and Dockerfile changes are dropped, since main already supersedes them
- the verify script's stale --wait description is corrected

Co-authored-by: EdmondDantes21 <redi.vreto@krateo.io>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TLFnUe4HXFphxQNAfqFawZ
…nce from log

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TLFnUe4HXFphxQNAfqFawZ
openssl prints English month names and date(1) parses %b in the current locale,
so under any non-English locale (e.g. it_IT on macOS) the granted-window parse
failed and the script aborted. Found running the script against a kind cluster.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TLFnUe4HXFphxQNAfqFawZ
@braghettos
braghettos force-pushed the fix/certificate-renewal branch from 3f3a42a to d614a38 Compare October 29, 2026 14:03
…ext's apiserver

The Secret's server-url is the in-cluster address consumers use (chart default
https://kubernetes.default.svc), which does not resolve from a workstation, so
step 3 always failed off-cluster. What is under test is the client certificate:
present it to the current context's apiserver, trusting that context's CA. Also
print sub-hour grants in minutes instead of "0h".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TLFnUe4HXFphxQNAfqFawZ
@braghettos

Copy link
Copy Markdown
Collaborator Author

End-to-end on kind with a capped signer: pass

I ran the branch image on kind v1.36 with kube-controller-manager --cluster-signing-duration=20m. That simulates OpenShift's non-configurable signing cap, the case #28 lists as unverified.

Check Result
Granted window is read, not assumed authn requested 8760h and the signer granted 25m. authn logged warn with requested/granted/renewAt.
Validity annotations authn.krateo.io/certificate-not-{before,after} are present on authn-clientconfig and match the certificate.
Renewal fires before expiry Re-issued at the 0.66 threshold: the first cert was valid 13:51:12 to 14:16:12, and the new one 14:02:45 to 14:27:45. The pod had 0 restarts.
Renewed cert accepted by the apiserver kubectl auth whoami with the renewed cert returns authn / [authn …].
Login JWT clamped /serviceaccount/login: with AUTHN_KUBECONFIG_CRT_EXPIRES_IN=24h, the JWT lifetime is 19m and its exp equals the certificate's NotAfter (14:16:40Z) to the second.

Recorded row: | kind (capped 20m) | requested 8760h | granted 25m | renewal ok | apiserver ok |

The run found two bugs in the verify script, both fixed on this branch:

  • date parsed openssl's English month names in the current locale, so the script failed under non-English locales (it_IT). It now pins LC_ALL=C.
  • Step 3 connected to the Secret's in-cluster server-url (kubernetes.default.svc), which doesn't resolve off-cluster. It now presents the certificate to the current context's apiserver. Sub-hour grants also print as minutes.

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.

Client certificates in *-clientconfig Secrets are never renewed

2 participants