Skip to content

fix: redirect config-server dependency to config-server-release for SHA-1 fix - #748

Open
G-Gobi wants to merge 1 commit into
carvel-dev:developfrom
G-Gobi:fix/config-server-sha1-removal
Open

fix: redirect config-server dependency to config-server-release for SHA-1 fix#748
G-Gobi wants to merge 1 commit into
carvel-dev:developfrom
G-Gobi:fix/config-server-sha1-removal

Conversation

@G-Gobi

@G-Gobi G-Gobi commented Aug 10, 2026

Copy link
Copy Markdown

Summary

  • github.com/cloudfoundry/config-server is archived (read-only on GitHub) and can never receive new tags.
  • Its actual source of truth has moved to cloudfoundry/config-server-release's src/config-server directory, which merged cloudfoundry/config-server-release#21 — replacing SHA-1 with SHA-256 (RFC 7093 Method 4) for X.509 SubjectKeyIdentifier computation in certificate_generator.go.
  • Adds a replace directive redirecting the existing github.com/cloudfoundry/config-server import path to github.com/cloudfoundry/config-server-release/src/config-server, pinned to the merge commit (132a0f6) via its auto-derived pseudo-version.

Why replace and not a version bump

No v0.2.0-equivalent tag exists for the nested Go module — the repo's v0.2.0/v0.1.x tags are bare, repo-root BOSH release tags, not the src/config-server/vX.Y.Z-prefixed tags Go module tooling requires for a subdirectory module (confirmed directly: attempting to resolve src/config-server@v0.2.0 fails with unknown revision src/config-server/v0.2.0). Separately, the nested go.mod's declared module path (github.com/cloudfoundry/config-server) doesn't match the config-server-release import path, so a plain require bump wouldn't resolve regardless — replace is the correct mechanism here either way.

Impact

  • CertificateReconciler unconditionally calls Generate()generateCertificate() for every Certificate CRD, which previously computed SubjectKeyId via SHA-1 unconditionally. Verified directly against the vendored dependency (via an in-module test, since removed) that newly generated certificates now get a 32-byte SHA-256-based SubjectKeyId instead of the old 20-byte SHA-1 one.
  • No public API surface changed (Generate(interface{}) (interface{}, error), CertResponse{Certificate, PrivateKey, CA} are untouched) — this is a drop-in swap.
  • The reconciler's drift detection (GenerateInputs.IsChanged) only compares spec-derived certParams, not generated cert bytes, so this does not force regeneration of already-existing certificates on upgrade.
  • Bumping the dependency also pulled in several transitive dependency updates via Go's minimal version selection (bosh-utils, go-logr, golang.org/x/{crypto,mod,net,sync,sys,term,text,tools}), since config-server-release's own go.mod requires newer versions of those than this module currently did. This inflates the vendor diff beyond just the SHA-1 fix, but is a mechanical, expected consequence of MVS — not a manual change.

Test plan

  • go build ./... and go build -mod=vendor ./...
  • go vet ./... (pre-existing unrelated findings only, unchanged by this PR)
  • go mod tidy && go mod vendor produce no further diff (fully consistent)
  • go test ./pkg/generator/... passes
  • Directly verified (in-module test, not committed) that a certificate generated via cfgtypes.NewCertificateGenerator(nil).Generate(...) now has a 32-byte SubjectKeyId, confirming the SHA-256 fix is actually exercised through this dependency, not just present in source

Live Cluster Validation — secretgen-controller SHA-1 → SHA-256 fix (branch fix/config-server-sha1-removal, commit de54e5e8)

Validated on `ubuntu@10.85.2.109` using a kind cluster. Tests: (1) stock secretgen-controller v0.21.1 genuinely produces SHA-1-based `SubjectKeyId`s, (2) after upgrading to our fixed image, pre-existing certificates are left byte-for-byte untouched, (3) newly created certificates get SHA-256-based `SubjectKeyId`s and correctly chain against pre-existing (old, SHA-1) CAs, and (4) if an existing certificate's `Secret` is deleted post-upgrade, it regenerates using the new SHA-256 code.

1. Create kind cluster

$ kind create cluster --config kind-config.yaml
Creating cluster "kind" ...
 ✓ Ensuring node image (kindest/node:v1.35.0) 🖼
 ✓ Preparing nodes 📦 📦 📦
 ✓ Writing configuration 📜
 ✓ Starting control-plane 🕹️
 ✓ Installing CNI 🔌
 ✓ Installing StorageClass 💾
 ✓ Joining worker nodes 🚜
Set kubectl context to "kind-kind"

2. Install stock secretgen-controller v0.21.1

$ kubectl apply -f https://github.com/carvel-dev/secretgen-controller/releases/download/v0.21.1/release.yml
namespace/secretgen-controller created
customresourcedefinition.apiextensions.k8s.io/secretexports.secretgen.carvel.dev created
customresourcedefinition.apiextensions.k8s.io/secretimports.secretgen.carvel.dev created
customresourcedefinition.apiextensions.k8s.io/secrettemplates.secretgen.carvel.dev created
customresourcedefinition.apiextensions.k8s.io/certificates.secretgen.k14s.io created
customresourcedefinition.apiextensions.k8s.io/passwords.secretgen.k14s.io created
customresourcedefinition.apiextensions.k8s.io/rsakeys.secretgen.k14s.io created
customresourcedefinition.apiextensions.k8s.io/sshkeys.secretgen.k14s.io created
deployment.apps/secretgen-controller created
serviceaccount/secretgen-controller-sa created
clusterrole.rbac.authorization.k8s.io/secretgen-controller-cluster-role created
clusterrolebinding.rbac.authorization.k8s.io/secretgen-controller-cluster-role-binding created

$ kubectl -n secretgen-controller wait --for=condition=Available deployment/secretgen-controller --timeout=120s
deployment.apps/secretgen-controller condition met

$ kubectl -n secretgen-controller get pods
NAME                                    READY   STATUS    RESTARTS   AGE
secretgen-controller-579f676666-dqwb9   1/1     Running   0          10s

3. Seed brownfield state — CA + leaf cert on stock (SHA-1) controller

$ cat test-ca.yaml
apiVersion: secretgen.k14s.io/v1alpha1
kind: Certificate
metadata:
  name: test-ca
spec:
  isCA: true
  commonName: test-ca

$ cat test-leaf-before.yaml
apiVersion: secretgen.k14s.io/v1alpha1
kind: Certificate
metadata:
  name: test-leaf-before
spec:
  commonName: test-leaf-before.example.com
  caRef:
    name: test-ca

$ kubectl apply -f test-ca.yaml -f test-leaf-before.yaml
certificate.secretgen.k14s.io/test-ca created
certificate.secretgen.k14s.io/test-leaf-before created

$ kubectl get certificates
NAME               DESCRIPTION           AGE
test-ca            Reconcile succeeded   5s
test-leaf-before   Reconcile succeeded   5s

4. Baseline — confirm claim 1: stock controller genuinely produces SHA-1

$ kubectl get secret test-ca -o jsonpath='{.data.crt\.pem}' | base64 -d > test-ca-before.pem
$ kubectl get secret test-leaf-before -o jsonpath='{.data.crt\.pem}' | base64 -d > test-leaf-before-before.pem

$ openssl x509 -in test-ca-before.pem -text -noout | grep -A1 'Subject Key Identifier'
            X509v3 Subject Key Identifier:
                0D:2D:32:6F:DE:2A:02:0B:08:00:51:A3:46:20:AA:B1:63:96:C9:38

$ openssl x509 -in test-leaf-before-before.pem -text -noout | grep -A1 'Subject Key Identifier\|Authority Key Identifier'
            X509v3 Subject Key Identifier:
                28:1D:20:4D:AD:81:2B:E8:46:FC:A8:30:DA:44:9A:A7:0E:EC:FB:FA
            X509v3 Authority Key Identifier:
                0D:2D:32:6F:DE:2A:02:0B:08:00:51:A3:46:20:AA:B1:63:96:C9:38

$ kubectl get secret test-ca -o jsonpath='{.metadata.resourceVersion}'; kubectl get secret test-leaf-before -o jsonpath='{.metadata.resourceVersion}'
629
635

Both Subject Key Identifiers are 20 bytes — the SHA-1 length. Claim 1 confirmed: the stock controller genuinely produces SHA-1-based SubjectKeyIds, not just "presumed from source code."

5. Build our fixed branch and a custom controller image

$ git clone https://github.com/carvel-dev/secretgen-controller.git .
$ git remote add fork https://github.com/G-Gobi/secretgen-controller.git
$ git fetch fork fix/config-server-sha1-removal
$ git checkout -b fix/config-server-sha1-removal fork/fix/config-server-sha1-removal
$ git log -1 --format='%H %s'
de54e5e8b2d05c57adb401a6752e1f545c778122 fix: redirect config-server dependency to config-server-release for SHA-1 fix

$ CGO_ENABLED=0 GOOS=linux go build -mod=vendor -ldflags="-X 'main.Version=develop-sha1-fix' -buildid=" -trimpath -o controller ./cmd/controller/...
build exit: 0

$ cat Dockerfile.dev
FROM scratch
COPY controller secretgen-controller
USER 1000
ENTRYPOINT ["/secretgen-controller"]

$ docker build -f Dockerfile.dev -t secretgen-controller-sha1-fix:dev .
...
#5 writing image sha256:2cab321451836f46943592b7f26f424a3eccfa7e99d862b1a3d92976cdd1dafb done
#5 naming to docker.io/library/secretgen-controller-sha1-fix:dev done

6. Load into kind and upgrade the deployment

$ kind load docker-image secretgen-controller-sha1-fix:dev --name kind
Image: "secretgen-controller-sha1-fix:dev" ... loading... (all 3 nodes)

$ kubectl -n secretgen-controller set image deployment/secretgen-controller secretgen-controller=secretgen-controller-sha1-fix:dev
deployment.apps/secretgen-controller image updated

$ kubectl -n secretgen-controller patch deployment secretgen-controller --type=json -p='[{"op":"replace","path":"/spec/template/spec/containers/0/imagePullPolicy","value":"Never"}]'
deployment.apps/secretgen-controller patched

$ kubectl -n secretgen-controller rollout status deployment/secretgen-controller --timeout=120s
deployment "secretgen-controller" successfully rolled out

$ kubectl -n secretgen-controller get pods
NAME                                    READY   STATUS    RESTARTS   AGE
secretgen-controller-5f59dd5cb7-jrtkm   1/1     Running   0          16s

$ kubectl -n secretgen-controller get deploy secretgen-controller -o jsonpath='{.spec.template.spec.containers[0].image}'
secretgen-controller-sha1-fix:dev

7. Confirm claim 2 (part 1) — existing certs untouched, despite being reconciled

$ kubectl -n secretgen-controller logs deploy/secretgen-controller | grep -i 'test-ca\|test-leaf-before'
{"level":"info","ts":"...","logger":"sg.secret","msg":"Reconciling","request":{"name":"test-ca","namespace":"default"}}
{"level":"info","ts":"...","logger":"sg.secexp","msg":"Reconciling","request":{"name":"test-ca","namespace":"default"}}
{"level":"info","ts":"...","logger":"sg.secret","msg":"Reconciling","request":{"name":"test-leaf-before","namespace":"default"}}
{"level":"info","ts":"...","logger":"sg.secexp","msg":"Reconciling","request":{"name":"test-leaf-before","namespace":"default"}}

$ kubectl get secret test-ca -o jsonpath='{.metadata.resourceVersion}'; kubectl get secret test-leaf-before -o jsonpath='{.metadata.resourceVersion}'
629
635

Both objects were reconciled by the new (fixed) controller on startup — confirmed via logs — yet their Secret resourceVersions are exactly unchanged (629, 635 — identical to step 4). Kubernetes only bumps resourceVersion on an actual write, so this alone proves no regeneration occurred.

$ kubectl get secret test-ca -o jsonpath='{.data.crt\.pem}' | base64 -d > test-ca-after.pem
$ kubectl get secret test-leaf-before -o jsonpath='{.data.crt\.pem}' | base64 -d > test-leaf-before-after.pem

$ diff test-ca-before.pem test-ca-after.pem && echo IDENTICAL
IDENTICAL
$ diff test-leaf-before-before.pem test-leaf-before-after.pem && echo IDENTICAL
IDENTICAL

$ openssl x509 -in test-ca-after.pem -text -noout | grep -A1 'Subject Key Identifier'
            X509v3 Subject Key Identifier:
                0D:2D:32:6F:DE:2A:02:0B:08:00:51:A3:46:20:AA:B1:63:96:C9:38

Byte-for-byte identical PEM data before and after the upgrade, and the exact same SKI value (not just the same length). Claim 2 (part 1) confirmed: pre-existing certificates are completely unaffected by the upgrade.

8. Confirm claim 2 (part 2) — new certs get SHA-256, chain correctly against the old CA

$ cat test-leaf-after.yaml
apiVersion: secretgen.k14s.io/v1alpha1
kind: Certificate
metadata:
  name: test-leaf-after
spec:
  commonName: test-leaf-after.example.com
  caRef:
    name: test-ca

$ kubectl apply -f test-leaf-after.yaml
certificate.secretgen.k14s.io/test-leaf-after created

$ kubectl get certificate test-leaf-after
NAME              DESCRIPTION           AGE
test-leaf-after   Reconcile succeeded   5s

$ kubectl get secret test-leaf-after -o jsonpath='{.data.crt\.pem}' | base64 -d > test-leaf-after.pem
$ openssl x509 -in test-leaf-after.pem -text -noout | grep -A1 'Subject Key Identifier\|Authority Key Identifier'
            X509v3 Subject Key Identifier:
                10:04:70:51:FD:CE:69:CE:40:DF:DB:83:01:2F:CC:FA:DB:83:A9:64:E0:3E:58:8E:DE:1D:22:E1:8A:1E:97:AC
            X509v3 Authority Key Identifier:
                0D:2D:32:6F:DE:2A:02:0B:08:00:51:A3:46:20:AA:B1:63:96:C9:38

$ # SKI byte count
32 bytes

$ openssl verify -CAfile test-ca-after.pem test-leaf-after.pem
test-leaf-after.pem: OK

test-leaf-after's Subject Key Identifier is now 32 bytes (SHA-256), while its Authority Key Identifier still correctly matches test-ca's original 20-byte SHA-1 SubjectKeyId — confirming AKI is read directly off the real CA certificate, not recomputed. The chain validates cleanly (OK) despite the algorithm/length mismatch between CA and leaf, per RFC 5280. Claim 2 (part 2) confirmed.

9. Deletion case — does a deleted cert regenerate with SHA-256?

$ kubectl delete secret test-leaf-before
secret "test-leaf-before" deleted

$ kubectl annotate certificate test-leaf-before test-trigger=1 --overwrite
certificate.secretgen.k14s.io/test-leaf-before annotated

$ kubectl get certificate test-leaf-before
NAME               DESCRIPTION           AGE
test-leaf-before   Reconcile succeeded   4m12s

$ kubectl get secret test-leaf-before
NAME               TYPE     DATA   AGE
test-leaf-before   Opaque   2      4s

(Deleting the Secret alone doesn't trigger an immediate reconcile — CertificateReconciler.AttachWatches only watches Certificate objects, not Secrets. A trivial annotation touch on the Certificate CR was used to force it, since it's watched via EnqueueRequestForObject.)

$ kubectl get secret test-leaf-before -o jsonpath='{.data.crt\.pem}' | base64 -d > test-leaf-before-regenerated.pem

$ diff test-leaf-before-before.pem test-leaf-before-regenerated.pem > /dev/null && echo SAME || echo DIFFERENT
DIFFERENT

$ openssl x509 -in test-leaf-before-regenerated.pem -text -noout | grep -A1 'Subject Key Identifier\|Authority Key Identifier'
            X509v3 Subject Key Identifier:
                E4:F1:B2:82:65:95:51:DF:59:C8:6C:CE:04:3F:B2:B0:93:2B:42:A9:84:E1:A7:F0:32:5A:34:19:B2:79:8F:D7
            X509v3 Authority Key Identifier:
                0D:2D:32:6F:DE:2A:02:0B:08:00:51:A3:46:20:AA:B1:63:96:C9:38

$ kubectl get secret test-leaf-before -o jsonpath='{.metadata.resourceVersion}'
1073

$ openssl verify -CAfile test-ca-after.pem test-leaf-before-regenerated.pem
test-leaf-before-regenerated.pem: OK

Confirmed: once the Secret is deleted and reconciliation is re-triggered, CertificateReconciler takes the createSecret (not-found) path, generates a completely new certificate (new key pair — DIFFERENT byte-diff, not just a new SKI), and that new certificate has a 32-byte SHA-256 SubjectKeyId. Its Authority Key Identifier still correctly matches the untouched test-ca's original SKI, and the chain still validates. resourceVersion jumped to 1073, confirming a real write happened this time — in clear contrast to steps 7–8 where untouched objects kept their original resourceVersion.

Conclusion

All four things asked were directly verified on a live kind cluster, not inferred from source alone:

  1. Before upgrade: stock secretgen-controller v0.21.1 genuinely produces 20-byte SHA-1 SubjectKeyIds for both CA and leaf certs.
  2. After upgrade, existing certs untouched: test-ca and test-leaf-before are reconciled (per logs) but their Secrets are byte-for-byte identical and resourceVersion-unchanged — the dependency bump does not force regeneration.
  3. After upgrade, new certs use the fix: a freshly created leaf (test-leaf-after) against the same pre-existing CA gets a 32-byte SHA-256 SubjectKeyId, correctly chains against the CA's old SHA-1 SKI via AuthorityKeyId, and openssl verify passes.
  4. Deletion triggers regeneration with the new algorithm: deleting an existing cert's Secret (with a manual reconcile trigger, since Secret deletion isn't itself watched) causes a full regeneration — new key pair, new serial, and a 32-byte SHA-256 SKI — still correctly chaining against the unchanged CA.

No regression, and the fix behaves correctly in every tested scenario, including mixed old-CA/new-leaf interoperability.

…HA-1 fix

github.com/cloudfoundry/config-server is archived (read-only) and can
never receive new tags. Its actual source of truth is now
cloudfoundry/config-server-release's src/config-server directory,
which merged a fix replacing SHA-1 with SHA-256 (RFC 7093 Method 4)
for X.509 SubjectKeyIdentifier computation in certificate_generator.go
(cloudfoundry/config-server-release#21).

Adds a replace directive pointing the existing
github.com/cloudfoundry/config-server import path at
github.com/cloudfoundry/config-server-release/src/config-server,
pinned to the merge commit (132a0f6). A plain version bump isn't
possible: no v0.2.0-equivalent tag exists for the nested Go module
(the repo's bare v0.2.0/v0.1.x tags are BOSH release tags, not
path-prefixed Go module tags), and the nested go.mod's declared
module path doesn't match the config-server-release import path
anyway, which is why replace (not require) is needed here regardless.

CertificateReconciler unconditionally calls Generate() ->
generateCertificate() for every Certificate CRD, which previously
computed SubjectKeyId via SHA-1 unconditionally. Verified directly
against the vendored dependency that certificates now get a 32-byte
SHA-256-based SubjectKeyId instead of the old 20-byte SHA-1 one.
No public API surface (Generate, CertResponse) changed, and the
existing param-based drift detection in the reconciler is unaffected,
so this does not force regeneration of already-existing certificates.

Bumping the dependency also pulled in several transitive dependency
updates via Go's minimal version selection (bosh-utils, go-logr,
golang.org/x/{crypto,mod,net,sync,sys,term,text,tools}) since
config-server-release's go.mod requires newer versions of those than
this module currently did.

Signed-off-by: Gobi Ganesan <gobi.ganesan1509@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

2 participants