Registry notification_secret is interpolated unquoted into gitlab.yml, so a numeric secret becomes an Integer
Summary
charts/gitlab/templates/_registry.tpl:81 writes the registry notification secret into the Rails gitlab.yml ERB fragment without quotes:
notification_secret: <%= YAML.load_file("/etc/gitlab/registry/notificationSecret").flatten.first %>This breaks the rule the chart already documents. doc/development/style_guide.md, "Templating sensitive content", lines 477-479:
- Quote in the ERB / Gomplate output, not surrounding it.
- Use a format-native encoder whenever possible.
- For rendered YAML, use JSON strings because YAML is a superset of JSON.
Twenty other interpolations in the chart follow it. This one was missed rather than regressed: the line landed in !1948 (merged) on 2021-04-22, the .to_json sweep in #2999 (closed) closed on 2021-11-30 in milestone 14.6, and no commit touched this file in that window. It is still bare on master at the same line.
Current behavior
The rendered value is a bare YAML scalar, so Psych infers its type. Measured on Psych 5.3.1:
| stored secret | parsed as |
|---|---|
["12345678901234567890123456789012"] |
Integer |
["0x1234abcd"] |
Integer 305441741 |
["12_34"] |
Integer 1234 |
["-12345"] |
Integer -12345 |
["yes"] / ["no"] |
Boolean |
lib/api/container_registry_event.rb:14-17 then passes that value to Devise.secure_compare:
secret_token = Gitlab.config.registry.notification_secret
unauthorized! unless Devise.secure_compare(secret_token, headers['Authorization'])Devise.secure_compare (devise 4.9.4, lib/devise.rb:516) is return false if a.blank? || b.blank? || a.bytesize != b.bytesize. An Integer and true are both not blank?, so execution reaches a.bytesize:
NoMethodError: undefined method 'bytesize' for an instance of IntegerEvery image push and delete event is then dropped. Container Registry size and cleanup policies stop working, with no configuration error anywhere. A nil value, by contrast, returns false early and gives a clean 401.
INFERRED: we expect the raise to surface as HTTP 500 rather than 401, from standard Grape handling of an unrescued exception. We verified the raise directly but did not drive a live request over the wire.
Who is affected
Only an operator who supplies their own global.registry.notificationSecret.secret. The bundled generator (templates/shared-secrets/_generate_secrets.sh.tpl:215) emits ["<32 random alphanumerics>"], which is effectively never all-digit.
generate_secret_if_needed skips a secret that already exists, so a pre-created secret, or shared-secrets.enabled: false, reaches the ERB with nothing in between. The whole chart has exactly two references to the mounted file — the mount at templates/_registry.tpl:53 and the read at charts/gitlab/templates/_registry.tpl:81. Structurally nothing can sit between them: Helm never sees the Secret's contents, so no values.schema.json could reach it.
doc/installation/secrets.md:320-329 shows the required [\"…\"] array shape but says nothing about the value's charset. Neither Rails nor the registry constrains it.
Steps to reproduce
kubectl create secret generic my-registry-notification \
--from-literal=secret='["12345678901234567890123456789012"]'
helm install test gitlab/gitlab --version 10.3.0 \
--set global.registry.notificationSecret.secret=my-registry-notification ...Or reproduce the mechanism with no cluster:
mkdir -p /etc/gitlab/registry
printf '["12345678901234567890123456789012"]' > /etc/gitlab/registry/notificationSecret
ruby -rerb -ryaml -e '
out = ERB.new(%q{notification_secret: <%= YAML.load_file("/etc/gitlab/registry/notificationSecret").flatten.first %>}).result
puts out
v = YAML.safe_load(out)["notification_secret"]
puts "#{v.class}: #{v.inspect}"
v.bytesize'notification_secret: 12345678901234567890123456789012
Integer: 12345678901234567890123456789012
undefined method 'bytesize' for an instance of Integer (NoMethodError)The same line renders into three ConfigMaps: webservice, sidekiq and toolbox.
Proposed fix
notification_secret: <%= YAML.load_file("/etc/gitlab/registry/notificationSecret").flatten.first.to_s.to_json %>.to_json is the documented idiom. .to_s is needed here because, unlike the File.read(...).strip sites, this read has no type guarantee: a secret stored as [12345…] with no inner quotes is already an Integer before to_json runs, and Integer#to_json emits a bare number again. Measured:
| stored secret | .to_json |
.to_s.to_json |
|---|---|---|
["Ab3xY9zQ"] |
String |
String |
["12345678901234567890123456789012"] |
String |
String |
[12345678901234567890123456789012] |
Integer — still broken |
String |
Please do not use .to_s.inspect. Ruby's String#inspect escapes #{ as \#{, and \# is not a valid YAML escape, so a secret containing #{ would render a gitlab.yml that Psych rejects and the pod would not start. JSON escapes are a subset of YAML's, so .to_json is always safe.
The fix is transparent for the generator's own output: ["Ab3x…"] renders as "Ab3x…", which parses to the same String as before. No rotation and no restart coupling.
What this does not cover
-
It does not add validation that the secret is a one-element array. A bare string still raises
NoMethodError: undefined method 'flatten' for an instance of String. -
It does not change the registry side of the same secret.
-
Two other sites break the same style-guide rule and are not fixed by this change. Both quote outside the ERB rather than inside it:
charts/gitlab/templates/_rails.redis.tpl:48—sentinel_password: "{{ … }}"over the bare_redis.tpl:100. A Sentinel password containing"or\renders invalid YAML.charts/gitlab/charts/gitlab-exporter/templates/configmap.yaml:29— apassword='…'connection string whosegsubescapes only'and\. A password containing#is silently truncated at the#and the pod starts with a wrong password; one containing:raisesPsych::SyntaxError.
Both are still present on
master. Happy to split them into their own issue if you prefer.
INFERRED: chart RSpec fixtures that assert on the rendered gitlab.yml string may need the expected value re-quoted. We did not run the chart's test suite.
Versions
- Chart 10.3.0 (
appVersion: v19.3.0), read from the packaged chart on disk;masteris identical atcharts/gitlab/templates/_registry.tpl:81 - GitLab 19.3.0-ee, Psych 5.3.1, Devise 4.9.4
Investigated and filed with Claude Code on behalf of @WarheadsSE. Claims that were reasoned rather than observed are labelled INFERRED.