Skip to content

feat: oidc push authorization#925

Draft
avidal wants to merge 1 commit intokalbasit:mainfrom
avidal:avidal/oidc-authz
Draft

feat: oidc push authorization#925
avidal wants to merge 1 commit intokalbasit:mainfrom
avidal:avidal/oidc-authz

Conversation

@avidal
Copy link

@avidal avidal commented Feb 17, 2026

See here for the relevant discussion.

This patch implements OIDC (kind of) for push authorization. It allows an ncps server to require and match OIDC identity tokens (such as those minted by CI systems including GitHub and Gitlab) against preconfigured sets of claims before accepting a PUT or DELETE.

The existing "allow push" and "allow delete" configuration is still required and used as a coarse check. That is, a configuration with OIDC policies that does not also allow PUT and DELETE verbs will be rejected just as they are today without OIDC policies.

If a server is not configured with any OIDC policies, this feature is a no-op for backwards compatibility.

Identity tokens are extracted from Authorization headers. For compatibility with "nix store --to" (which typically uses .netrc files), the token can be read from "Basic" authorization.

For example, a .netrc of:

machine ncps.mydomain.tld
password <identity token>

Will produce a header like so:

Authorization: Basic <base64(username:password)>

In this case, the username would be empty and the password would be the token. This patch does not care what the username is.

If "Basic" authorization is not used, this patch requires the token to be provided via "Bearer" authorization.

NOTE: On startup ncps will reach out to all configured OIDC issuers to fetch their signing keys. If ncps cannot reach an issuer, or if the issuer is misconfigured, ncps will refuse to start up. While this places a startup time check on an ncps server, this is preferable to a runtime check that may not fail until a client attempts to write to the store.

Because OIDC policy configuration is a list of maps it isn't configurable by CLI flags (urfave/cli-altsrc does not support this) and can only be specified in the configuration file.

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @avidal, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a significant security enhancement by integrating OIDC push authorization into the ncps server. It allows administrators to define policies that validate incoming OIDC tokens for PUT and DELETE operations, ensuring that only authenticated and authorized CI/CD systems or users can modify the cache. This feature provides a modern, secure, and flexible mechanism for managing write access, moving beyond simple allow/deny flags to a more robust identity-based authorization model.

Highlights

  • OIDC Push Authorization: Implemented OpenID Connect (OIDC) based push authorization for PUT and DELETE requests, allowing ncps to verify OIDC identity tokens against preconfigured policies before accepting writes. This enables secure push access from CI/CD systems like GitHub Actions and GitLab CI without needing stored secrets.
  • Flexible Token Handling: The system supports both standard 'Bearer' authorization headers and 'Basic' authorization, where the JWT is provided as the password, ensuring compatibility with tools like nix copy that use .netrc files.
  • Configurable Policies with Claim Matching: OIDC policies can be configured with multiple issuers and audiences. Additionally, policies support optional claim matching using glob patterns, allowing fine-grained control over which tokens are authorized based on their embedded claims.
  • Helm Chart and Configuration Updates: The Helm chart (charts/ncps/values.yaml and charts/ncps/templates/configmap.yaml) and example configuration (config.example.yaml) have been updated to expose the new OIDC policy configuration options. Comprehensive documentation for OIDC Push Authorization has also been added to the user guide.
  • New OIDC Core Modules: Introduced new Go modules (pkg/oidc) to handle OIDC configuration parsing, token verification, claim matching, and HTTP middleware integration. This includes robust error handling for missing/invalid tokens and claim mismatches.
Changelog
  • charts/ncps/templates/configmap.yaml
    • Added OIDC policy configuration block, conditionally rendered based on .Values.config.oidc.policies.
  • charts/ncps/tests/configmap_test.yaml
    • Added tests to verify OIDC section rendering in the configmap, including cases for no policies, single policy, and multiple policies with claims.
  • charts/ncps/values.yaml
    • Added oidc section to config with policies array, including detailed comments and an example configuration for GitHub Actions and GitLab.
  • config.example.yaml
    • Added commented-out oidc configuration example to the cache section, demonstrating issuer, audience, and claim matching.
  • docs/docs/User Guide.md
    • Added a link to the new 'OIDC Push Authorization' feature documentation in the 'Features' section.
  • docs/docs/User Guide/Configuration/Reference.md
    • Added a new section 'OIDC Push Authorization' detailing configuration parameters, policy requirements, and a link to the dedicated feature guide.
    • Added a reference link to the new OIDC Push Authorization feature documentation.
  • docs/docs/User Guide/Features/OIDC Push Authorization.md
    • Added a new documentation file providing a comprehensive guide to OIDC Push Authorization, covering overview, how it works, configuration, claims matching, glob patterns, multiple policies, GitHub Actions example, authentication methods, Helm chart configuration, HTTP response codes, and troubleshooting.
  • go.mod
    • Added new direct dependencies: github.com/BurntSushi/toml, github.com/coreos/go-oidc/v3, github.com/go-jose/go-jose/v4, and golang.org/x/oauth2.
    • Updated go.yaml.in/yaml/v3 to be a direct dependency.
  • go.sum
    • Updated go.sum to reflect new and updated Go module dependencies for OIDC functionality.
  • pkg/ncps/serve.go
    • Imported strings and github.com/kalbasit/ncps/pkg/oidc packages.
    • Integrated OIDC configuration parsing from the main config file during server startup.
    • Initialized an OIDC verifier if policies are configured and passed it to the server.New constructor.
    • Added a new parseOIDCConfig helper function to read and parse the OIDC section from the configuration file, supporting YAML, TOML, and JSON formats.
  • pkg/oidc/config.go
    • Added new file defining PolicyConfig and Config structs for OIDC policies.
    • Implemented Validate method for Config to ensure policies have required fields.
    • Implemented ParseConfigData function to extract and parse the OIDC configuration from raw bytes, supporting YAML, TOML, and JSON.
  • pkg/oidc/config_test.go
    • Added new file containing unit tests for ParseConfigData covering various scenarios including empty data, missing sections, single/multiple policies, different formats, and validation errors.
  • pkg/oidc/middleware.go
    • Added new file implementing an HTTP middleware for OIDC token validation.
    • Provided ClaimsFromContext to retrieve verified OIDC claims from the request context.
    • Implemented extractToken to parse JWTs from 'Bearer' and 'Basic' authorization headers.
    • Included writeJSONError helper for consistent error responses.
  • pkg/oidc/middleware_test.go
    • Added new file with unit tests for the OIDC middleware, covering cases like missing/invalid headers, valid tokens, claim mismatches, and different authentication schemes.
  • pkg/oidc/oidc.go
    • Added new file defining the New function to create an OIDC Verifier by initializing policies and performing OIDC discovery.
  • pkg/oidc/policy.go
    • Added new file defining the policy struct which wraps an OIDC policy configuration and its go-oidc verifier.
    • Implemented newPolicy to perform OIDC discovery and create a token verifier for a given policy.
  • pkg/oidc/verifier.go
    • Added new file defining the Verifier struct and its Verify method for validating OIDC tokens against configured policies.
    • Introduced Claims struct to hold verified token information.
    • Implemented checkClaims for matching token claims against required patterns.
    • Provided claimValueToStrings and anyPatternMatches helpers for claim processing.
    • Implemented MatchGlob for simple wildcard matching of claim values.
  • pkg/oidc/verifier_test.go
    • Added new file with unit tests for the OIDC verifier, covering token validation, claim matching (exact, glob, multiple keys/patterns), missing claims, and multi-provider scenarios.
    • Included TestMatchGlob for glob pattern matching logic.
    • Provided mockOIDC helper for simulating an OIDC provider in tests.
  • pkg/server/oidc_test.go
    • Added new file with integration tests for OIDC push authorization in the ncps server.
    • Included oidcTestServer mock for OIDC provider and setupOIDCTestServer for test environment setup.
    • Tested scenarios like PUT/DELETE with and without tokens, GET requests (unaffected), and behavior when OIDC is not configured.
  • pkg/server/server.go
    • Imported github.com/kalbasit/ncps/pkg/oidc package.
    • Added oidcVerifier field to the Server struct.
    • Introduced WithOIDCVerifier option for server.New to configure the OIDC verifier.
    • Modified createRouter to apply the OIDC middleware to PUT and DELETE routes if an OIDC verifier is present, effectively gating these operations.
Activity
  • The pull request introduces a new feature for OIDC push authorization.
  • New Go modules and documentation have been added to support this feature.
  • Helm chart and example configurations were updated to reflect the new OIDC settings.
  • No specific reviewer comments or interactions are available in the provided context.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces OIDC push authorization, a significant and well-implemented feature. The code is clean, well-structured, and includes comprehensive documentation and tests, which is excellent. The use of the Option pattern for server configuration and the creation of a dedicated oidc package are great design choices. I've found one potential bug in the glob matching logic that could lead to an infinite loop, for which I've provided a suggestion.

Comment on lines 126 to 156
func MatchGlob(pattern, value string) bool {
// Fast path: no wildcards means exact match.
if !strings.Contains(pattern, "*") {
return pattern == value
}

parts := strings.Split(pattern, "*")

// Check that the value starts with the first segment and ends with the last.
if !strings.HasPrefix(value, parts[0]) {
return false
}

if !strings.HasSuffix(value, parts[len(parts)-1]) {
return false
}

// Walk through remaining segments ensuring they appear in order.
remaining := value[len(parts[0]):]

for _, part := range parts[1:] {
idx := strings.Index(remaining, part)
if idx < 0 {
return false
}

remaining = remaining[idx+len(part):]
}

return true
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The MatchGlob function has a bug where consecutive * characters (e.g., a**b) in the pattern can cause an infinite loop. This happens because strings.Split produces empty strings for consecutive delimiters, and the subsequent loop does not advance when processing an empty part, leading to an infinite loop.

A simple way to fix this is to normalize the pattern by replacing any occurrences of ** with a single * before splitting, as they are functionally equivalent for this glob implementation.

func MatchGlob(pattern, value string) bool {
	// Fast path: no wildcards means exact match.
	if !strings.Contains(pattern, "*") {
		return pattern == value
	}

	// Normalize multiple wildcards to a single one, as they are equivalent.
	// This also prevents an infinite loop in the logic below when Split produces empty parts.
	for strings.Contains(pattern, "**") {
		pattern = strings.ReplaceAll(pattern, "**", "*")
	}

	parts := strings.Split(pattern, "*")

	// Check that the value starts with the first segment and ends with the last.
	if !strings.HasPrefix(value, parts[0]) {
		return false
	}

	if !strings.HasSuffix(value, parts[len(parts)-1]) {
		return false
	}

	// Walk through remaining segments ensuring they appear in order.
	remaining := value[len(parts[0]):]

	for _, part := range parts[1:] {
		idx := strings.Index(remaining, part)
		if idx < 0 {
			return false
		}

		remaining = remaining[idx+len(part):]
	}

	return true
}

Copy link
Author

@avidal avidal Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gemini-code-assist This is incorrect. The only loop is over parts[1:] which is always bounded.

In the case of double stars you just end up with an iteration like so:

idx := strings.Index(s, "") == 0
remaining = remaining[0+0:]

remaining doesn't change that iteration, but the iteration continues

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That said, I'll add a commit to transform double star because it's a reasonable readability cleanup

This patch implements OIDC (kind of) for push authorization. It allows
an ncps server to require and match OIDC identity tokens (such as those
minted by CI systems including GitHub and Gitlab) against preconfigured
sets of claims before accepting a PUT or DELETE.

The existing "allow push" and "allow delete" configuration is still
required and used as a coarse check. That is, a configuration with OIDC
policies that does not *also* allow PUT and DELETE verbs will be
rejected just as they are today without OIDC policies.

If a server is not configured with any OIDC policies, this feature is a
no-op for backwards compatibility.

Identity tokens are extracted from Authorization headers. For
compatibility with "nix store --to" (which typically uses .netrc files),
the token can be read from "Basic" authorization.

For example, a `.netrc` of:

```
machine ncps.mydomain.tld
password <identity token>
```

Will produce a header like so:

```
Authorization: Basic <base64(username:password)>
```

In this case, the username would be empty and the password would be the
token. This patch does not care what the username is.

If "Basic" authorization is not used, this patch requires the token to
be provided via "Bearer" authorization.

NOTE: On startup ncps will reach out to all configured OIDC issuers to
fetch their signing keys. If ncps cannot reach an issuer, or if the
issuer is misconfigured, ncps will refuse to start up. While this places
a startup time check on an ncps server, this is preferable to a runtime
check that may not fail until a client attempts to write to the store.

Because OIDC policy configuration is a list of maps it isn't
configurable by CLI flags (urfave/cli-altsrc does not support this) and
can only be specified in the configuration file.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant