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:

  1. Quote in the ERB / Gomplate output, not surrounding it.
  2. 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 Integer

Every 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 — a password='…' connection string whose gsub escapes only ' and \. A password containing # is silently truncated at the # and the pod starts with a wrong password; one containing : raises Psych::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; master is identical at charts/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.