AR can call an IAM RPC the deployed IAM does not serve, and no pipeline compares the two versions
Context
An Artifact Registry (AR) release can call an IAM RPC that the deployed IAM does not serve. Nothing before release compares the two versions. The RPC in the report, LookupResources, has one call site, the repository listing, which drains it for a caller without a namespace-level role (S09, Repository listing). IAM first serves it in v1.49.0. An older IAM answers Unimplemented, and the listing then answers the 503 that S09 specifies for any evaluation failure. The 2026-09-30 comment sets the constraint. Self-Managed and Dedicated take GitLab monthly, and the zero-downtime upgrade policy requires consecutive GitLab minors to work together, so calls between modules have to stay compatible along the monolith's release cycle. This issue serves the Dedicated deployment row (P0) of the GA epic Artifact Central: GA (.com, SM and Dedicated Co... (gitlab-org&22707) • Tim Rizzi. Its tracking issue rules out a behavior change reaching Dedicated without a communication path, because Dedicated cannot toggle feature flags: AR P0: Dedicated deployment (gitlab-org/gitlab#605684) • Unassigned. The GitLab-to-AR half of the same question is Publish and enforce a version compatibility mat... (#1468) • Unassigned.
What exists on main
Each line holds at origin/main eca9f1bab (2026-10-01) unless it names another source.
internal/iam/client.go: issuesReadRelationships(:130),LookupResources(:145), andgrpc.health.v1.Health/Check(:159), the closed set inrpcMethods(internal/iam/metrics.go:115). AnyReadRelationshipserror answers 503 (internal/authz/decision.go:238). The listing's drain is the oneLookupResourcescall site.internal/iam/errors.go,mapStatus(:30): maps five codes to sentinels.Unimplementedtakes the default arm (:53), a bareiam: rpc:wrap, as S25### Errorsspecifies for any other code. The GLAZ client maps it toErrUnimplemented(internal/authz/glaz/errors.go:20), whose comment says the log line then tells a missing RPC from an outage.internal/managementapi/list_authz.go: a refused drain logs a WARN (:467) and an ERROR (:243), answers 503 (:245), and bookslisting_failure_reason="transport"unless its own budget ran out (:474).docs/dev/observability.md:145definestransportas aLookupResourcescall IAM refused or could not answer, soUnimplementedbooks the same value as a network fault.internal/grpcutil/metrics.go:84: each IAM RPC recordsgitlab_artifact_registry_iam_data_access_requests_total{rpc, grpc_code}, so the reported failure counts asgrpc_code="Unimplemented".docs/dev/alerting.mdhas no entry on that series.cmd/artifact-registry/wire_iam.go:40: registers no readiness check on IAM, as S25## Dependenciesrequires. The oneClient.Healthcall site isscripts/iam-rig-wait-healthy.vendir.yml:9: vendors the IAM proto at v1.24.0 (2026-07-02), which declaresLookupResources(third_party/iam-proto/proto/lookup/lookup.proto:33) and aHealthResponsecarrying onlystatus(:49).buf:generate-check(.gitlab-ci.yml:474) fails only when the committed stubs differ from these protos (docs/dev/vendored-iam-protos.md,## CI gate).gitlab-org/auth/iam, read 2026-10-01: v1.48.0'sLookupServicedefines noLookupResourcesand embedsUnimplementedLookupServiceServer, so the call answersUnimplemented. v1.49.0 (2026-09-04) implements it. From v1.38.0 (2026-08-07),HealthResponsecarriesversionandcommitfrom the build, with the comment "Lets callers confirm a new deploy has actually rolled out before exercising it".1cddbec14(2026-09-10, first in AR v2.30.0) wired the drain into the listing, so AR's floor is IAM v1.49.0. No file states it. Its plan (docs/plans/2026-09-04-lookup-resources-list-visibility.md) found that the v1.24.0 proto already carried the messages, "so novendirbump is needed" (:60). The plan re-confirmed IAM's behavior by hand (:274)..gitlab-ci.yml,test:integration:iam-rig(:1295): runsinternal/iam's integration suite,LookupResourcesincluded, againstauth/iam:v1.60.2(:1303), the tag Renovate last moved on 2026-09-29 (f8bdb4977). It runs onmainand on canonical MRs that touch its listed paths, never on the security mirror that cuts releases..gitlab/ci/e2e-caproni-functional.gitlab-ci.yml:24:test:e2e:caproni:functionalruns the e2e suite on the "Caproni e2e daily" schedule and as a manual,allow_failurejob on MRs and branches. Its IAM release comes from a vendored fragment that names the chart with noversion(test/caproni/vendor/gitlab-caproni/fragments/caproni.iam-data-access.yaml:75), and nothing undere2e/reads an IAM version. The 1.44.0 pin that the original description cites (test/caproni/fragments/caproni.authz.yaml) is not onmain. The authz arm landed on 2026-10-01 without it, through test(e2e): the end-to-end suite in three arms o... (!2052 - merged) • Suleimi Ahmed • 19.5..runway/fairway.yaml:135andscripts/chart-gen.sh: production and the e2e rig take the GLAZ sidecar from one pin, v10.1.0.vendir.ymlholds two more GLAZ pins: the proto at v6.0.1 (:64), whose comment says it tracks the sidecar, and the role catalog at v2.0.0 (:36), whose comment says it matches the proto. Only comments tie the three together, and no two pins match.docs/dev/releases.md,## Deployment is triggered from the mirror's main pipeline: each release uploads manifests to staging automatically and to production on a manual play. No step reads an IAM version.docs/dev/self-managed-install.md: the header records the one set walked together (GitLab 19.4.0, AR chart 2.82.1, IAM chart 1.55.3, 2026-09-22).## Scoperecords that no matrix exists.## Upgradingupgrades AR before IAM, names no required order, and reads IAM's version fromgitlab.iam.update.v1.UpdateService/Health.docs/dev/self-managed-limitations.md, row 9 of## What should be lifted: asks for a published matrix and names this issue for the AR-to-IAM half.- S25 (
docs/specs/S25-iam-relationships-client.md):### Proto schema sourcingmakes each proto bump an explicit commit and names no minimum IAM version.### Connectionpoints the probe atgrpc.health.v1.Health/Check, "not theLookupService.HealthRPC". Acceptance criterion 14's integration test namesReadRelationshipsandHealthonly. - S09 (
docs/specs/S09-authorization.md),## Dependencies(:38): states one version requirement, a GLAZ sidecar servingBatchCheck(auth/glaz v2.0.0) "confirmed before the consuming changes ship", and none for IAM. - The release manifest design records one compatible version per component for each GitLab release, in a private catalog until that release publishes. Its schema allows only
version,sha,ref, andsourceper entry (additionalProperties: false), so no entry can state a requirement on another component. The public record holds only19/4/0.json, which listsrelease-platform-canaryalone. IAM's.gitlab-ci.ymlenables the samerelease-platformcomponent as AR's.
Change
AR records, in one file, its floor: the oldest IAM release that serves every RPC AR calls. A check fails before an AR release reaches an IAM below the floor. At eca9f1bab the floor is v1.49.0, set by LookupResources. test:integration:iam-rig runs at the floor, so an MR that adopts an RPC above it fails until the MR raises the floor in review. An Unimplemented reply returns a named error, as it does on the GLAZ client. An alert names the RPC. The listing keeps its 503, which the original description calls correct. Product decides where Self-Managed and Dedicated operators read the floor and which upgrade order they follow. The GitLab-to-AR half stays out of scope, and so does the GLAZ pin drift, which no issue tracks.
Acceptance criteria
- Decision for product, since no spec covers it: where Self-Managed and Dedicated operators read AR's IAM floor. The 2026-09-29 comment points to the release manifest, which pairs co-released versions and has no field for a floor. The options are a floor field in the manifest (a schema change by its owners), the AR and IAM versions the manifest pairs per GitLab release (which criterion 5's check reads), or a document AR publishes. This issue's description records the choice and its date.
- Decision for product, since no spec covers it: the upgrade rule that the zero-downtime policy, as the 2026-09-30 comment states it, implies. Either the AR release bundled with a GitLab minor works with the IAM release bundled with the previous minor, so AR adopts an IAM RPC one GitLab release after IAM ships it, or the documented upgrade moves IAM before AR.
## Upgradingindocs/dev/self-managed-install.mdstates the rule. - Decision for the S25 owner, since S25 states no minimum IAM version: the file holding AR's IAM floor, which criteria 4 and 5 read, and the rule that sets it. At
eca9f1babthe floor is v1.49.0, the first IAM release serving every RPC inrpcMethods. The vendored proto version cannot stand in, because v1.24.0 declaresLookupResourcesand no IAM served it before v1.49.0. The amended### Proto schema sourcingnames the file and the rule. test:integration:iam-rigruns theinternal/iamintegration suite at the floor, read from criterion 3's file, and at the newest tag it tracks today, which catches an IAM release that drops an RPC AR calls. Itschanges:list includes the floor file. The MR that lands this records a run against v1.48.0 in whichTestClient_IntegrationLookupResourcesfails withUnimplemented. S25 acceptance criterion 14 addsLookupResources.- Decision for the AR maintainers, since no spec covers it: the check that fails before an AR release reaches an IAM below its floor, and where it reads each target's IAM version. On GitLab.com, production waits on a manual play (
docs/dev/releases.md). IAM's deployed version there sits in a private Runway deployment project that no AR job reads. On Self-Managed and Dedicated, the target is the IAM version that criterion 1's document pairs with the AR release. This issue's description names the check and its targets. The MR that adds the check records a failing run with the floor set above a target's version. - Decision for the S25 owner, since
### Connectionpoints the probe atgrpc.health.v1.Health/Check: whether the client also reads IAM's version fromLookupService.Healthand compares it with the floor. IAM reports that version from v1.38.0. AR's v1.24.0 stubs have no field for it, so adopting this bumps the vendored proto. S25## Dependenciesrules out a readiness gate, so a version below the floor yields only a log line and a metric. An empty version, which IAM documents for non-release builds, counts as unknown. With the comparison in place, a unit test against an in-process server that reports v1.48.0 asserts both signals. - Decision for the S25 owner, since
### ErrorsroutesUNIMPLEMENTEDthrough its any-other-code row: a row mapping it toiam.ErrUnimplemented, matchingglaz.ErrUnimplemented. With the row in place, acceptance criterion 4's table test covers it: an in-process server answeringcodes.Unimplementedyields an error that matches the sentinel througherrors.Isand keeps the status forerrors.As. - Decision for the S09 owner, since
docs/dev/observability.mddefinestransportas anyLookupResourcescall IAM refused or could not answer: whether anUnimplementeddrain books its ownlisting_failure_reason. A new value changes that row,drainLookupFailureReason, and the closed set with its budget of 5 (internal/metrics/cardinality.go:319). Keepingtransportleaves the IAM client series'grpc_codeto tell the two apart. The response stays the 503 S09 specifies either way. docs/dev/alerting.mdgains an IAM client entry, in the catalog's form (series, condition, and the readings that are not a fault), forgitlab_artifact_registry_iam_data_access_requests_total{grpc_code="Unimplemented"}rising at all. The entry names the RPC from therpclabel.- Once product rules on criterion 1, row 9 of
docs/dev/self-managed-limitations.mdand the no-matrix bullet in## Scopeofdocs/dev/self-managed-install.mdlink the published floor.
Specs and ADRs
- S25, Errors: the status-to-sentinel table criterion 7 amends.
- S25, Proto schema sourcing: the vendoring rule criterion 3 extends with a floor.
- S25, Connection: the health probe criterion 6 extends.
- S25, Dependencies: the IAM health signal never gates readiness.
- S25, Acceptance Criteria: criterion 4 (error mapping) and criterion 14 (integration test).
- S09, Repository listing: the drain, and the 503 on any evaluation failure.
- S09, Observability: the listing filter's failure series.
- S09, Dependencies: the GLAZ
BatchCheckrequirement, the one version requirement a spec states. - ADR-021, Constraints: authorization fails closed when a dependency it needs is unavailable.
- ADR-021, Grant-bounded repository-list evaluation: the
LookupResourcesadoption. - ADR-024: AR and IAM as separately built and deployed components.
- Release manifest design: one compatible version per component for each GitLab release.
Dependencies
- The GitLab-to-AR half asks product to choose between a per-pair matrix and the release manifest's compatibility contract, which criterion 1 also weighs, so one ruling can settle both. It has no code ordering: Publish and enforce a version compatibility mat... (#1468) • Unassigned
- The zero-downtime upgrade procedure carries criterion 2's rule, so it follows criterion 2: Write the operational lifecycle procedures: bac... (#1329) • Unassigned
- If criterion 6 adopts runtime detection, its metric reaches alerting through the signal registry that S25
## Dependenciesassigns to S03 spec: observability readiness model and int... (#58) • Unassigned. - Under criterion 1's manifest-field option, the schema change belongs to the manifest's owners. The schema project had no issue for it on 2026-10-01, so file one there before criterion 5's check depends on the field.
- The GLAZ action-catalogue parity test covers the same drift on the GLAZ edge, with no ordering: CI parity test: every action AR sends must exis... (#1301) • Unassigned
- The e2e work that the original description's authz arm belongs to has no ordering, because criterion 4 uses
test:integration:iam-rig, whichmaincarries ateca9f1bab: Extend automated e2e coverage ahead of closed b... (#949 - closed) • Suleimi Ahmed
First step
An S25 amendment MR comes first, since criteria 3, 6, and 7 are S25 rulings that wait on no product decision: the floor's file and rule at v1.49.0, the UNIMPLEMENTED row, the ruling on reading IAM's version, and LookupResources in acceptance criterion 14. A plan MR then sequences criteria 4, 5, 8, and 9, starting with the floor file, iam.ErrUnimplemented, and the rig at the floor. Product's rulings on criteria 1 and 2 gate only criterion 10, the install guide's upgrade rule, and the Self-Managed and Dedicated half of criterion 5.
Original description
Summary
AR can release a call to an IAM RPC that the deployed IAM does not implement. Nothing in the build, CI, or deploy path compares AR's requirements against the IAM version it will run against. Reported from production:
The namespace itself was provisioned, the problem is that IAM replied with an error (the version deployed does not have the new LookupResources operation)
AR failed closed, which is correct behavior. The gap is that nothing caught it earlier, and nothing named it when it happened.
Where the versions sit
AR's code needs >= 1.49.0
|
v
IAM ---- 1.44.0 ---------- 1.49.0 ---------- 1.53.1 ---- 1.54.1 ---->
| | | |
e2e authz arm LookupResources CI iam-rig production
(!2052) implemented .gitlab-ci (Runway)
|
below the floor: cannot serve the RPC AR calls
contract: AR's vendored IAM proto is pinned at 1.24.0 (2026-07-02). It
declares LookupResources nine weeks before any server
implemented it.Measured 2026-09-11; AR at origin/main d30ace636.
| Pinned where | Version | |
|---|---|---|
| AR's vendored proto | vendir.yml |
1.24.0 |
| e2e authz arm | test/caproni/fragments/caproni.authz.yaml:32 |
1.44.0 |
CI test:integration:iam-rig |
.gitlab-ci.yml:1241 |
v1.53.1, Renovate chasing v1.53.4 |
| Production | Runway deployment project, Flux manifest | 1.54.1 |
| AR's code requires | nowhere | >= 1.49.0 |
Four independent pins. None of them references another, and the requirement in the last row is not written down anywhere.
Why nothing catches it
- The proto declares before the server implements.
lookup.proto:33declaresrpc LookupResourcesat the pinned v1.24.0; IAM implemented it in v1.49.0.buf:generate-checkonly proves stubs match protos. - AR and IAM deploy independently, several times a day. Neither pipeline reads the other's version. On 2026-09-11 AR moved 2.38.3 to 2.42.0; IAM moved 1.49.0 to 1.54.1 over five business days, deployed minutes after each upstream tag.
- No test runs a version that would fail. Unit tests use a fake, which the
plan states plainly: "the fake returns the empty page, so the suite cannot
catch a pre-fix build; only the re-confirmation can"
(
docs/plans/2026-09-04-lookup-resources-list-visibility.md). The two environments that run a real IAM pin versions chosen by hand. - The failure has no name.
internal/iam/errors.go:45-55maps five gRPC codes.Unimplementedis not one of them, so it arrives as an opaqueiam: rpc:error and a 503 indistinguishable from any other RPC failure. - Runtime detection is not available. IAM's
HealthResponsecarries onlystring status, and gRPC reflection is disabled in staging and production (enable_reflection = false). AR's health check reportsSERVINGagainst an IAM that cannot serve the call.
For contrast, GLAZ does not have this problem
scripts/chart-gen.sh generates the chart from .runway/fairway.yaml, so
production and the e2e authz arm take the GLAZ sidecar from the same pin
(.runway/fairway.yaml:137, v6.0.1), and vendir.yml states that the vendored
GLAZ proto tracks that sidecar version. One pin, one contract, both
environments.
Notes
- While !2052 (merged) is open,
main's e2e environment has no authz arm. Once it merges, the arm deploys a real GLAZ sidecar and a realiam-data-accessat the version above. - The
iam-data-gke-grpcdeployment record shows production at v1.49.0 or later since 2026-09-04 09:23, and AR wiredLookupResourcesto a live route on 2026-09-10 (1cddbec14). The reported instance therefore needs its environment and timestamp to reproduce. The version-comparison gap holds either way.
Related, for context
- #1301, #1297 (closed) - the same class of drift between AR and GLAZ, on action names
- #949 (closed) - the e2e work the authz arm belongs to
- #969 - the
LookupResourcesadoption