The GitLabSecrets reconciler
internal/controller/gitlabsecrets reconciles the apps.gitlab.com/v2alpha1 GitLabSecrets
resource into the Secrets the GitLab chart would otherwise generate with its shared-secrets hook
Job. For the requirements, see the GitLab secrets spec. For the
design, see ADR 35.
The reconciler is not implemented yet. The definition is implemented, as described in
The definition, and generation in internal/secretgen, as described in
Generation. This page also records the behavior of the Job it matches.
The definition
api/v2alpha1/gitlabsecrets_types.go declares the resource. task manifests regenerates
config/crd/bases/apps.gitlab.com_gitlabsecrets.yaml, and task generate the deepcopy code.
task install_v2alpha1_crds and the end-to-end harness apply the definition with the other
v2alpha1 definitions.
Server-side apply rejects a field the definition does not declare, as does a create or update with
fieldValidation=Strict. Examples are spec.policy and authority.expiry, which earlier chart
drafts rendered. A request without fieldValidation stores the resource without the field and
returns a warning.
internal/controller/gitlabsecrets/definition_test.go creates each chart render in
internal/controller/gitlabsecrets/testdata/chart/ on envtest. It also checks the rejected values
and the bounds. task slow-unit-tests runs it. To run it on its own:
KUBEBUILDER_ASSETS=$(./setup-envtest use -p path) go test ./internal/controller/gitlabsecrets/ -run TestDefinitionTo regenerate the fixtures from a chart checkout, run the script, then the test:
internal/controller/gitlabsecrets/testdata/chart/render.sh <chart-dir>The script renders each values/<case>.yaml with values/base.yaml. To add a case, add its values
file, and add what it covers to fixtureCovers in the test. The README.md next to the fixtures
records the chart commit they come from.
Generation
internal/secretgen decides what to add to one Secret, with no Kubernetes client. Plan takes the
entries of spec.secrets that name the Secret, the Secret if it exists, and its readyUnits. It
returns the keys to add, the reports, the declared data-bearing units, the next readyUnits, and a
note when the existing type differs from the declared one. The package comment shows how a
controller uses it. spec.certificates is not implemented yet.
The tests compare every generated value with the shape of the Job’s output. The fixture is in
internal/secretgen/testdata/job/:
values.yaml: chart values that render every generator type and option.gitlabsecrets.yaml: theGitLabSecretsthat the chart renders for them withshared-secrets.provider: controller.shapes.json: the shapes of the Secrets the Job created for them, recorded byhack/secretshapes, with the chart commits, the Job image, and the date. It holds no secret material.
The shape test accepts only these differences from the recorded shapes:
- An SSH host key carries no comment, in its
.pubor in its private key. secrets.ymlends with a newline.
The generated values also differ from the Job’s in ways a single recording does not show:
- A
randomvalue always haslengthcharacters. - A generated
secrets.ymlvalue that YAML reads as another type, such as all digits, is quoted. The Job writes it bare, so Rails reads a number. An existing bare value stays bare. - A rewritten
secrets.ymlis indented by 2, drops---and..., joins the lines of folded scalars, and tags merge keys as!!merge <<:. Every value is kept, and a document of comments only keeps its comments.
Behavior of the Job
The values in the field came from the Job, so the Job is the reference. These results are from
2026-09-30, in the Job’s image cng/kubectl:v19.4.1 and in the arm64 cng/cfssl-self-sign:v19.2.1,
whose script matches the CNG default branch.
| Subject | Behavior |
|---|---|
| OpenSSL | 3.5.7. openssl genrsa and openssl req -keyout write PRIVATE KEY (PKCS#8). |
x509 | openssl req -x509 writes X.509 v3, sha256WithRSAEncryption, subject and issuer CN=<commonName>, CA:TRUE (critical), and subject and authority key IDs. |
sshHostKeys | OpenSSH 10.0p2. ssh-keygen -A makes RSA 3072, ECDSA P-256, and Ed25519, as ssh_host_<type>_key and .pub. Private keys are OPENSSH PRIVATE KEY, with no cipher and no KDF, in lines of 70 characters. Public and private keys carry a <user>@<pod> comment. The Job mounts an emptyDir on /etc/ssh, because the image user cannot write there. |
random | gen_random filters 4096 random bytes with tr, so a narrow character set can return fewer than length characters. |
encoding: base64 | In chart 10.4.1, stores standard base64 with no wrapping and no trailing newline up to a length of 57, the same as base64-nowrap. Every base64 entry is 32. From 58, the create path fails and the patch path truncates. |
wrap: jsonArray | Encodes first, then wraps. |
| Existing Secret | Adds only a missing --from-literal key. Never adds a key of rsa, x509, sshHostKeys, or raw bytes, and never changes an existing host-key Secret. |
secrets.yml | Rewritten on every run, with the declared fields of one environment only. Regenerates an absent, null, or empty-string field, and keeps an empty list. Indents by 2, with a PEM in a | block and list items indented by 4. Has no trailing newline. A rerun writes the same bytes. |
| Labels | Overwrites gitlab.standardLabels and gitlab.commonLabels. Removes app.kubernetes.io/name when global.application.create is false. |
| Certificate set | Every run issues a new CA and wildcard certificate, then creates each of the three Secrets with || true. A run with one Secret missing fills it from the new CA, which the other two do not match. |
| Certificate structure | CA: subject and issuer O=<namespace>, OU=<release>, CN=<caSubject>, CA:TRUE (critical), key usage certificate sign and CRL sign (critical). Wildcard: subject CN=<domain>, SANs <domain> and *.<domain>, CA:FALSE (critical), key usage digital signature, key encipherment, and certificate sign, extended key usage server authentication. The chain is ca.pem, then wildcard.pem. |
| Certificate keys | RSA PRIVATE KEY (PKCS#1) or EC PRIVATE KEY (SEC 1). |
| Certificate sizes | RSA from 2048 to 8192, odd sizes included. ECDSA 256, 384, and 521. RSA 1024 fails with RSA key is too weak, 8194 with RSA key size too large, and ECDSA 224 with invalid curve. ed25519 depends on the CNG version, as described below. |
| Certificate signature | RSA from 4096 SHA-512, from 3072 SHA-384, below that SHA-256. ECDSA P-256 SHA-256, P-384 SHA-384, P-521 SHA-512. |
| Certificate validity | CA 43800 hours, wildcard 8760 hours. The script reads EXPIRE while the Job sets EXPIRY, so shared-secrets.selfsign.expiry never applies. |
ed25519 follows the cfssl version, not the architecture. cng/cfssl-self-sign before v19.4.0
ships cfssl 1.6.1, which fails ed25519 with invalid algorithm, so charts 10.2 and 10.3 fail
it. From v19.4.0, both architectures ship cfssl 1.6.5, which the arm64 image builds from source
and reports as dev, so chart 10.4 issues Ed25519 certificates.
Those keys are RFC 8410 PKCS#8, byte-identical to Go’s x509.MarshalPKCS8PrivateKey, under the
nonstandard label Ed25519 PRIVATE KEY instead of the PRIVATE KEY of RFC 7468
(csr/csr.go:263-272 in cfssl 1.6.5, unchanged in 1.7.0). NGINX with OpenSSL 3.5.7, Envoy with
BoringSSL, and the Envoy Gateway validation reject the label, and load the key once it is
relabeled. Go accepts both. Chrome and Firefox do not support an Ed25519 TLS server certificate,
whatever the label, which is why the Operator refuses ed25519 certificates.
Reproduce the checks
Print headers and sizes only. The keys are throwaway, but do not paste them anywhere.
Check OpenSSL and the SSH host keys in the Job’s image:
docker run --rm --user 0 --entrypoint /bin/bash \ registry.gitlab.com/gitlab-org/build/cng/kubectl:v19.4.1 -c ' openssl version openssl genrsa 2048 2>/dev/null | head -1 ssh-keygen -A for pub in /etc/ssh/ssh_host_*.pub; do ssh-keygen -lf "$pub" | cut -d" " -f1,4; done head -1 /etc/ssh/ssh_host_rsa_key'Issue a certificate set for each algorithm and size, then print the signature algorithm, the CA row, and the validity:
for spec in rsa-1024 rsa-2048 rsa-3072 rsa-4096 rsa-8192 ecdsa-256 ecdsa-384 ecdsa-521 ed25519-0; do mkdir -m 777 -p "out/$spec" docker run --rm -v "$PWD/out/$spec:/output" -e ALGORITHM="${spec%-*}" -e KEY_SIZE="${spec#*-}" \ -e CA_SUBJECT="GitLab Helm Chart" -e CERT_DOMAIN=example.com -e EXPIRY=3650d \ registry.gitlab.com/gitlab-org/build/cng/cfssl-self-sign:v19.2.1 >/dev/null 2>&1 if [ -f "out/$spec/ca.pem" ]; then echo "$spec: $(openssl x509 -in "out/$spec/wildcard.pem" -noout -text | grep -m1 'Signature Algorithm')" else echo "$spec: issues nothing" fi done openssl x509 -in out/rsa-4096/ca.pem -noout -subject -issuer -dates openssl x509 -in out/rsa-4096/ca.pem -noout -text | grep -A1 -E 'Constraints|Key Usage' openssl x509 -in out/rsa-4096/wildcard.pem -noout -dates rm -rf outRecord the shape fixture of
internal/secretgen. Copy the chart checkout that the fixture names, and run these commands from its root. SetOPERATORto the root of this repository, and make a throwaway cluster the current context:fixture="$OPERATOR/internal/secretgen/testdata/job" work=$(mktemp -d) helm dependency build helm template gl . -n sg-fixture -f "$fixture/values.yaml" > "$work/render.yaml" helm template gl . -n sg-fixture -f "$fixture/values.yaml" --set shared-secrets.provider=controller | yq 'select(.kind == "GitLabSecrets")' > "$fixture/gitlabsecrets.yaml" yq 'select(.metadata.name == "gl-shared-secrets" or (.kind == "Job" and (.metadata.name | test("^gl-shared-secrets-[0-9a-f]+$"))))' \ "$work/render.yaml" > "$work/job.yaml" kubectl create namespace sg-fixture kubectl -n sg-fixture apply -f "$work/job.yaml" kubectl -n sg-fixture wait --for=condition=complete job --all --timeout=300s kubectl -n sg-fixture get secrets -o json > "$work/secrets.json" kubectl delete namespace sg-fixture (cd "$OPERATOR" && go run ./hack/secretshapes -secrets "$work/secrets.json" \ -manifest "$fixture/gitlabsecrets.yaml" -out "$fixture/shapes.json" \ -source "chart=<chart version and commits>" \ -source "render=helm <version>, release gl, namespace sg-fixture, values.yaml" \ -source "job=<Job image>" -source "cluster=<cluster and platform>" -source "recorded=<date>" \ -source "reproduce=doc/developer/gitlabsecrets.md, Reproduce the checks") rm -rf "$work"Then run
go test ./internal/secretgen/.... A shape difference that is not listed in Generation fails the test.