iam

module
v1.42.6 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 21, 2026 License: MIT

README

GitLab IAM Service

This service provides Identity and Access Management (IAM) capabilities specifically tailored for GitLab. Designed with modularity in mind, it offers a unified codebase that can be compiled to serve distinct purposes, ensuring flexibility and optimized deployments.

It is the main codebase to implement GitLab Adaptive Trust Environment.

Capabilities

  • Auth Provider: Manages OAuth 2.0 and OIDC flows, enabling secure authentication and authorization for applications integrating with GitLab.
  • Lookup API: Reads identity and access data from the IAM database, served by the iam-data-access binary.
  • Update API: Writes identity and access data to the IAM database, served by the iam-data-access binary.

Design

The repo ships two service binaries, each with its own main package under cmd/, plus the iam-migrate migration runner that every image bundles.

  • iam-auth — the OAuth 2.0 / OIDC provider: gRPC (:5004 in development) plus the OAuth HTTP surface (:8084). Built via make iam-auth.
  • iam-data-access — runs Lookup and Update on a shared gRPC port (:5005 in development). Used as the Runway target for the iam-data-gke-grpc deployment. Built via make iam-data-access.

Each binary is one Runway deployment unit; services communicate with their consumers over their standard gRPC/HTTP interfaces, so a local run matches the distributed deployment shape.

Development

Run scripts/prepare-dev-env.sh to install build tools first.

Building

The Makefile provides targets for all build scenarios:

  • make: Build all targets.
  • make iam-auth: Build the auth service binary.
  • make iam-data-access: Build the composite Lookup + Update binary used by Runway.
  • make up, make down: Start and stop dependent services like the database.
Linting

While we catch linter and formatting errors in CI, it's best to catch these pre-CI.

Run linters once:

make lint
Database migration linting

Migrations under db/migrations/ are linted with squawk for unsafe online-migration patterns (rule configuration lives in .squawk.postgres.toml and .squawk.yugabyte.toml):

make lint-migrations

CI runs this automatically. squawk is pinned in .mise.toml, so mise install (run by scripts/prepare-dev-env.sh) installs it like any other tool. CI uses a separate pin (GL_SQUAWK_VERSION in .gitlab-ci-other-versions.yml); Renovate bumps both in the same MR.

Note: the pg_version in the squawk configs tracks the Postgres major and must be bumped manually on a major upgrade. It's a separate copy from the postgres:18.x image tags (which renovate bumps) and isn't covered by scripts/update-asdf-version-variables.sh.

Database migrations

Create a new schema migration with:

make migration name=<migration_name>

This creates two migration files, one per database backend, sharing one version (a UTC timestamp):

  • db/migrations/postgres/<timestamp>_<migration_name>.sql
  • db/migrations/yugabyte/<timestamp>_<migration_name>.sql

Edit both: they must make the same schema change. They may differ only where the backends do; for example, YugabyteDB needs the -- +goose NO TRANSACTION header to run DDL.

Rules:

  • Migrations are applied strictly in version order. If another MR merges a newer timestamp before yours, the lint:migration-ordering CI job fails your MR; rename your migration files to a fresh timestamp. That rename is safe because an unmerged migration has not been applied anywhere.
  • Never rename or renumber a merged migration. Databases track applied migrations by version, so a renamed migration would run again.
  • No -- +goose Down sections. Schemas are never rolled back; to undo a change, write a new forward migration.

The older migrations keep their sequential numbers (00001, 00002, ...); only new migrations use timestamps.

db/migrations_test.go checks the version format and that both directories contain the same migrations. It runs in make test-unit.

Storage backends

We support multiple storage backends:

  • Postgres
  • YugabyteDB

Before running the service or the tests, the dependent services need to be started using the command:

make up
Testing
  • make test-unit: Runs the unit test suite.
  • make test-l2-integration: Runs the integration test suite using the development-l2 environment (Postgres).
  • make test-l1-integration: Runs the integration test suite using the development-l1 environment (YugabyteDB).
Continuous Integration (.gitlab-ci.yml)

The pipeline is defined with three stages (lint, verify, test) and uses pinned, compatible versions of Go (golang:1.24-alpine) and golangci-lint to ensure stable, reproducible builds. This specific Go version is required by the go.mod file.

Service access

If you're hitting Unauthenticated while accessing service endpoint, you're almost certainly missing either a service token, an end-user JWT, or both. See docs/service-access.md for the full mechanism, header names, and rotation behavior.

Invoking RPCs

To invoke RPCs from any of the gRPC servers, use grpcurl. The Health examples below work without credentials because they're public methods; other RPCs may require both a service token and an end-user JWT — see Service access.

In development, grpcurl can automatically discover RPCs via a reflection server. However, this server only works in development builds (disabled elsewhere for security reasons), so grpcurl cannot always discover the schema over the wire. Passing the .proto files directly with -proto does not work either: our protos import the protovalidate definitions (buf/validate/validate.proto) from the Buf Schema Registry (BSR), which grpcurl can't resolve on its own.

Instead, let buf compile a self-contained descriptor set (buf resolves the BSR deps declared in buf.yaml) and pipe it straight into grpcurl via -protoset:

buf build -o - | grpcurl -protoset /dev/stdin -plaintext localhost:5005 gitlab.iam.lookup.v1.LookupService/Health
{
  "status": "ok"
}

The same descriptor set works for discovery and to reach deployed environments. For the sandbox, drop -plaintext (it's TLS on :443) and add the x-gitlab-svc routing header:

# List the services exposed by a server
buf build -o - | grpcurl -protoset /dev/stdin \
  -H "x-gitlab-svc: iam-data-access-grpc" \
  gate-sandbox.com:443 list

# List the methods on one service
buf build -o - | grpcurl -protoset /dev/stdin \
  -H "x-gitlab-svc: iam-data-access-grpc" \
  gate-sandbox.com:443 list gitlab.iam.lookup.v1.LookupService

# Invoke a method
buf build -o - | grpcurl -protoset /dev/stdin \
  -H "x-gitlab-svc: iam-data-access-grpc" \
  -d '{}' gate-sandbox.com:443 gitlab.iam.lookup.v1.LookupService/Health

# For AuthService
buf build -o - | grpcurl -protoset /dev/stdin \
  -H "x-gitlab-svc: iam-auth-grpc" \
  -d '{}' gate-sandbox.com:443 gitlab.iam.auth.v1.AuthService/Health

To make many calls, build the descriptor set once (buf build -o /tmp/iam.binpb) and reuse it with -protoset /tmp/iam.binpb to avoid rebuild on every invocation.

Making an authenticated request

The examples above only call Health, which is exempt from both auth layers (see Service access). Every other RPC needs both a service token and an end-user JWT — missing either gets you Unauthenticated (see docs/service-access.md).

The service token is a static shared secret. Locally it's whatever service_token is set to in configs/environments/secrets.toml (for development and the GATE sandbox); on staging/production it's the Vault-managed value (see Configuration below).

The JWT is minted by GitLab Rails' token-exchange endpoint, authenticated with a personal access token or CI job token. Every minted token is scoped to gitlab-iam-data-access regardless of the requested audience — see docs/relationships-api.md#authentication — so any of Rails' SUPPORTED_AUDIENCES values works for calling this service:

SVC_TOKEN=...
IAM_JWT=$(curl -s -X POST \
  -H "Authorization: Bearer $USER_PAT" \
  -d 'audience=gitlab-artifact-registry' \
  http://<gitlab-host:port>/api/v4/token_exchange/token_exchange | jq -r '.token')

buf build -o - | grpcurl -protoset /dev/stdin \
  -H "gitlab-iam-data-access-token: $SVC_TOKEN" \
  -H "authorization: Bearer $IAM_JWT" \
  -d '{}' <service-host:port> gitlab.iam.lookup.v1.LookupService/LookupRelationships
Configuration

The IAM service uses a layered TOML configuration system. On startup, three files are merged in order — each layer overrides values from the previous one:

  1. base.toml — shared defaults for all environments (HTTP timeouts, CORS defaults, service addresses)
  2. Environment Config file — environment-specific overrides (database host/port, CORS origins, service URLs)
  3. Secrets file — credentials that must never be committed (database password, HMAC secrets, OAuth client secrets)

These three files use a shared structure — platform-wide settings live under [platform] and per-service settings under [services.<name>], so all services draw from the same merged config, all resolved from configs/ at the repo root with environment-specific files under configs/environments/.

Environment variables
Variable Description Default
ENV Selects the environment config file as ./configs/environments/$ENV.toml development-l2
CONFIG_FILE_PATH Absolute path to the environment config file. Overrides the default ./configs/environments/$ENV.toml when set.
SECRETS_FILE_PATH Absolute path to the secrets file. Overrides the default ./configs/environments/secrets.toml when set.
CONFIG_TOML Full environment config TOML passed inline (not a path). The container entrypoint writes it to a file and sets CONFIG_FILE_PATH for you. Used on Runway.
PLATFORM_DATABASE_PASSWORD, … Any config key can be overridden by an env var named after its dotted path, uppercased with ._ (e.g. platform.database.passwordPLATFORM_DATABASE_PASSWORD). Used to inject secrets from Vault.
Deployed Environments
  1. Set the ENV variable for the environment that is being deployed. Possible values are: sandbox-l1, sandbox-l2, staging-l1, staging-l2, production-l1, production-l2
  2. Mount the env config file with the name ENV.toml and the secrets file with the name secrets.toml under /app/configs/environments. e.g., if ENV is staging-l1, the config file should be named staging-l1.toml
File Resolution
/app/configs/
├── base.toml                  # always loaded first
└── environments/
    ├── staging-l1.toml        # ENV=staging-l1
    └── secrets.toml           # loaded last

Special Case - Runway:
Runway does not expose configMap volumes, and a Kubernetes volume mount replaces the whole target directory — so the env config and the secrets file cannot be injected as separate files into the same /app/configs/environments directory. Instead of mounting files, Runway deployments inject config without touching the filesystem layout:

  1. Env (non-secret) config is passed inline as a single CONFIG_TOML env var. The container entrypoint (scripts/entrypoint.sh) writes it to /tmp/iam-config/config.toml and exports CONFIG_FILE_PATH to point at it; base.toml stays baked in at /app/configs/base.toml.
  2. Secrets are injected as individual env vars from Vault (via Runway's secretEnvFrom). Viper's AutomaticEnv overrides the matching config key — e.g. PLATFORM_DATABASE_PASSWORD overrides [platform.database] password. No secrets.toml is shipped, so the loader treats a missing secrets file as non-fatal.

Because AutomaticEnv only consults env vars for keys that already exist in a loaded config file, each Vault-managed secret must have a placeholder entry in the env config to register its key. The convention is the VAULT_MANAGED sentinel value — see configs/environments/staging-l1.toml.

Example: inline config (as Runway runs it)
CONFIG_TOML="$(cat configs/environments/staging-l1.toml)" \
PLATFORM_DATABASE_USERNAME=iam PLATFORM_DATABASE_PASSWORD=… \
./iam-auth

CONFIG_FILE_PATH / SECRETS_FILE_PATH (absolute paths to each file) remain supported for any setup that can mount the files directly. CONFIG_TOML and CONFIG_FILE_PATH are mutually exclusive — they supply the same env config two different ways — and the entrypoint exits with an error if both are set.

Local Development and CI

For integration with a local GitLab Rails instance, see Rails development setup.

  1. Set ENV to the environment you want to work with (development-l1 or development-l2, default is development-l2): e.g. ENV=development-l1 make run-auth
  2. The repo ships these config files ready to use:
./configs/
├── base.toml                  # always loaded first
└── environments/
    ├── development-l1.toml    # ENV=development-l1
    ├── development-l2.toml    # ENV=development-l2 (default)
    ├── ci-l1.toml             # ENV=ci-l1
    ├── ci-l2.toml             # ENV=ci-l2
    └── secrets.toml           # shipped with placeholder values for local development; not baked into docker images

Tests that load config call testhelper.ChdirRepoRoot(t) in their setup, which changes the working directory to the repo root for the duration of the test. This lets config.Load resolve ./configs correctly regardless of which package directory the test runs from.

Commit messages

This project uses Conventional Commits to drive automated releases via semantic-release. Releases produce a git tag and a GitLab Release, which downstream tooling (e.g., Runway) uses to deploy.

Since we generally squash on merge, the MR title is the message that gets analyzed.

See also docs/release.

Format
<type>(<optional scope>): <description>
Common types
Type Triggers release Example
feat minor (1.2.0) feat: add OAuth scope validation
fix patch (1.2.3) fix: handle expired tokens correctly
perf patch (1.2.3) perf: cache JWKS responses
chore none chore: bump go-jose to v4
docs none docs: clarify token rotation
refactor none refactor: extract token helper
test none test: add coverage for JWKS endpoint
ci none ci: pin runner image version
Breaking changes

Append ! after the type, or include a BREAKING CHANGE: footer:

feat!: drop support for v1 tokens
feat: redesign token refresh flow

BREAKING CHANGE: clients must re-authenticate after upgrade
Notes
  • Non-conventional MR titles won't break the build, they're silently skipped by the analyzer and no release happens.
  • Use fix: for any change that should produce a deployable artifact, even if "fix" feels semantically loose. chore:, docs:, etc. will not produce a release.

Environments and releasing changes

Release management

See docs/release.

Sandbox environment

IAM services are deployed to a sandbox environment. The sandbox environment is a Kubernetes cluster under a GCP project maintained by the team. Much of this config lives in this sandbox-config repo. Please see that repo's README for details.

Deployments to the sandbox environment are automatically triggered on a green main pipeline.

Staging environment

We're deploying just the iam-auth service via Runway GKE. We have two workloads: one serving HTTP, and one serving gRPC.

Runway

NOTE: This section only covers the iam-auth Runway deployment and is out of date. We will eventually replace this with a Runway deployment handled by the Release Platform.

  • HTTP workload
    • Provisioned in this MR
    • Runway service ID: iam-auth-ext-gke
    • Runway project ID: 74293132
    • Access control: group-level (gitlab-org/software-supply-chain-security/authentication/authentication-runway-access)
  • gRPC workload
    • Access control: group-level (gitlab-org/software-supply-chain-security/authentication/authentication-runway-access)
    • Provisioning TBD
Project configuration and Vault

To support Runway deploys, the iam GitLab project was onboarded to GitLab's infra-mgmt repository. infra-mgmt is a central location to manage project configs via Terraform. The iam project was onboarded in this MR.

Communication between Runway infra and the iam project's build pipeline is enabled by an access token stored in Vault. We also have a Vault path configured for the iam-auth-ext-gke workload where we store application secrets:

Directories

Path Synopsis
oauth/server/accesstokens
Package accesstokens is the public API for access-token persistence.
Package accesstokens is the public API for access-token persistence.
oauth/server/authcodes
Package authcodes is the public API for authorization-code persistence.
Package authcodes is the public API for authorization-code persistence.
oauth/server/consent
Package consent persists consent challenges and verifiers.
Package consent persists consent challenges and verifiers.
oauth/server/internal/csrf
Package csrf provides the CSRF cookie and token helpers shared by the OAuth login and consent flows.
Package csrf provides the CSRF cookie and token helpers shared by the OAuth login and consent flows.
oauth/server/login
Package login persists login challenges and verifiers.
Package login persists login challenges and verifiers.
oauth/server/oidc
Package oidc is the public API for OpenID Connect session persistence.
Package oidc is the public API for OpenID Connect session persistence.
oauth/server/pkce
Package pkce is the public API for PKCE-request persistence.
Package pkce is the public API for PKCE-request persistence.
oauth/server/refreshtokens
Package refreshtokens is the public API for refresh-token persistence.
Package refreshtokens is the public API for refresh-token persistence.
oauth/server/revocation
Package revocation implements the OAuth 2.0 token revocation endpoint per RFC 7009.
Package revocation implements the OAuth 2.0 token revocation endpoint per RFC 7009.
oauth/server/revokedtokens
Package revokedtokens is the public API for the revoked-token denylist.
Package revokedtokens is the public API for the revoked-token denylist.
cmd
auth command
buildinfo
Package buildinfo holds version metadata baked into the binary at build time via linker -X flags (see .goreleaser.yml and scripts/build.sh).
Package buildinfo holds version metadata baked into the binary at build time via linker -X flags (see .goreleaser.yml and scripts/build.sh).
data-access command
migrate command
Command migrate applies pending database migrations and exits.
Command migrate applies pending database migrations and exits.
Package dataaccess wires the Lookup and Update gRPC services into a single process listening on one port.
Package dataaccess wires the Lookup and Update gRPC services into a single process listening on one port.
internal/dbmetrics
Package dbmetrics defines the data-access-wide repository-operation- duration instrument (used by store.go/resolver.go): a metric name plus a thin re-export of pkg/telemetry/db's generic OperationRecorder.
Package dbmetrics defines the data-access-wide repository-operation- duration instrument (used by store.go/resolver.go): a metric name plus a thin re-export of pkg/telemetry/db's generic OperationRecorder.
internal/relationships/roles
Package roles parses the glaz role definitions vendored under vendored/ (synced by vendir), the single source of truth for role ids so they are not hardcoded here.
Package roles parses the glaz role definitions vendored under vendored/ (synced by vendir), the single source of truth for role ids so they are not hardcoded here.
internal
prototest
Package prototest provides shared builders for valid relationships proto messages, used by the buf.validate rule tests co-located with each proto package (relationships / lookup / update).
Package prototest provides shared builders for valid relationships proto messages, used by the buf.validate rule tests co-located with each proto package (relationships / lookup / update).
testutil/storagefake
Package storagefake provides a configurable double for storage.Database, storage.Transaction, and storage.DBTX, for tests that exercise real service/server composition or drive sqlc-generated queries without a live DB connection.
Package storagefake provides a configurable double for storage.Database, storage.Transaction, and storage.DBTX, for tests that exercise real service/server composition or drive sqlc-generated queries without a live DB connection.
validation
Package validation builds the protovalidate validator and the gRPC interceptor that enforces the buf.validate rules declared in the .proto files.
Package validation builds the protovalidate validator and the gRPC interceptor that enforces the buf.validate rules declared in the .proto files.
pkg
telemetry
Package telemetry provides the common, service-agnostic metrics setup shared by every Go service in this repo: one Prometheus registry (via LabKit v2), an HTTP surface to expose it, and generic helpers for timing an operation into a histogram.
Package telemetry provides the common, service-agnostic metrics setup shared by every Go service in this repo: one Prometheus registry (via LabKit v2), an HTTP surface to expose it, and generic helpers for timing an operation into a histogram.
telemetry/grpc
Package grpctelemetry provides generic, Prometheus-native gRPC server metrics for any service in this repo.
Package grpctelemetry provides generic, Prometheus-native gRPC server metrics for any service in this repo.
serviceauth module
tokenauth module
userauth module
proto
smoketest
bootstrap-ancestor-grant command
Command bootstrap-ancestor-grant is a manual admin tool, run once per environment, that grants the smoke-test service account its relationships-admin role for smoketest/dataaccess.
Command bootstrap-ancestor-grant is a manual admin tool, run once per environment, that grants the smoke-test service account its relationships-admin role for smoketest/dataaccess.
internal/dataaccessclient
Package dataaccessclient holds client-side plumbing shared by the iam-data-access smoke test and its bootstrap-ancestor-grant tool.
Package dataaccessclient holds client-side plumbing shared by the iam-data-access smoke test and its bootstrap-ancestor-grant tool.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL