Skip to content

Latest commit

 

History

History
230 lines (178 loc) · 11.2 KB

0010-OIDC.md

File metadata and controls

230 lines (178 loc) · 11.2 KB
  • Feature Name: OIDC
  • Start Date: 2023-04-01
  • RFC PR: #49
  • Bundler Issue: (leave this empty)

Summary

Add the ability for users to exchange an OIDC JWT for a rubygems.org API token.

Motivation

  • Make it possible for users to have automation without hardcoding long-lived credentials
  • Add a way for users to enable MFA on push but be able to push without human intervention
  • Allow gathering cryptographically verified claims about who has pushed a version

Guide-level explanation

You are a developer with gems published to RubyGems.org, and you want a secure way to automate publishing gems.

You have a CI system that can generate OIDC tokens (for example using GitHub Actions), and you want to be able to use those tokens to publish gems, instead of storing harcoded API tokens as secrets.

To do this, you will:

  1. Ensure your OIDC Provider is configured on RubyGems.org

    (At first release of this feature, only GitHub Actions will be supported, and it will be configured by the RubyGems.org team)

  2. Create a new API key role on RubyGems.org, configured with the desired permissions and access policy.

    (At first release of this feature, API keys roles will be configured by the RubyGems.org team, via a support request)

    API Key Roles are configured with a set of permissions (the same permissions API keys can be configured with today), an optional gem the granted API key will be scoped to, and an Access Policy. This is done via a JSON document.

    Example permissions:

    {
      "scopes": ["push_rubygem", "index_rubygems"],
      "valid_for": "PT30M",
      "gems": ["oidc-test"]
    }

    These permissions:

    • Allow the API key to push gems to RubyGems.org
    • Allow the API key to index gems on RubyGems.org
    • Allow the API key to push gems only for the oidc-test gem
    • Allow the API key to be valid for 30 minutes

    API Key Roles are also configured with Access Policies, which are JSON documents heavily inspired by AWS IAM policies, that describe the restrictions on OIDC token claims needed to redeem the token for an API key. For example, an access policy could require that the OIDC token be issued by a specific provider, and that the aud claim be set to https://rubygems.org.

    Example policy:

    {
      "statements": [
        {
          "effect": "allow",
          "principal": {
            "oidc": "https://token.actions.githubusercontent.com"
          },
          "conditions": [
            {
              "operator": "string_equals",
              "claim": "aud",
              "value": "rubygems.org"
            },
            {
              "operator": "string_matches",
              "claim": "sub",
              "value": "repo:segiddins/oidc-test:ref:refs/heads/.*"
            }
          ]
        }
      ]
    }

    This policy states:

    • It allows OIDC tokens issued by https://token.actions.githubusercontent.com
    • It requires the aud claim (which is user-configurable when getting an OIDC token in an action) to be set to rubygems.org
    • It requires the sub claim (which is the GitHub repository the token was issued for) to match the regular expression repo:segiddins/oidc-test:ref:refs/heads/.*, which states that the repository must be segiddins/oidc-test, and the ref must be a branch.
  3. Update your GitHub Actions workflow to use this feature!

    We've built a GitHub Action that will take an OIDC token, and exchange it for an API key. You can use it like this:

    on:
      - push
    
    jobs:
      job:
        runs-on: ubuntu-latest
        permissions:
          contents: write # This is required to push the release commit to the repository
          id-token: write # This is required to get the OIDC token
        steps:
          # Set up a RubyGems.org API token via OIDC
          - uses: rubygems/configure-rubygems-credentials@main
            with:
              role-to-assume: rg_oidc_akr_01234abcd # The role token of the API key role you created in step 2
              gem-server: "https://rubygems.org" # this is the default value
              audience: "https://rubygems.org" # this is the default value
    
          # Check out the repository
          - uses: actions/checkout@v3
    
          # Configure git so that the release commit is attributed to the user who pushed it
          # and the commit created by `bundle exec rake release` will succeed & can be pushed
          - name: Set remote URL
            run: |
              git config --global user.email "$(git log -1 --pretty=format:'%ae')"
              git config --global user.name "$(git log -1 --pretty=format:'%an')"
              git remote set-url origin "https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/$GITHUB_REPOSITORY"
    
          # Set up Ruby
          - name: Set up Ruby
            uses: ruby/setup-ruby@v1
            with:
              ruby-version: "3.2.1"
              bundler-cache: true
    
          # Release the gem!
          - name: Release
            run: bundle exec rake release

Explain the proposal as if it was already implemented and released, and you were teaching it to another developer. That generally means:

  • Introducing new named concepts.
  • Explaining the feature largely in terms of examples.
  • Explaining how users should think about the feature, and how it should impact the way they use Bundler. It should explain the impact as concretely as possible.
  • If applicable, provide sample error messages, deprecation warnings, or migration guidance.
  • If applicable, describe the differences between teaching this to existing users and new users.

For implementation-oriented RFCs, this section should focus on how contributors should think about the change, and give examples of its concrete impact. For policy RFCs, this section should provide an example-driven introduction to the policy, and explain its impact in concrete terms.

Reference-level explanation

OIDC (OpenID Connect) is a standard for proving client identity built on top of JWT/JWS (JSON Web Tokens / JSON Web Signatures).

Several compute environments, such as CI sytems, are able to grant authorized users (even automated jobs) OIDC Tokens than contain relevant claims about the requesting party, and are cryptographically attested by the provider.

For example, GitHub Actions allows jobs with the identity-token: write permission to retrieve an OIDC token with claims stating the aud (audience, potentially user-provided data describing who the token is intented to be consumed by), as well as details about the workflow run that generated the token.

Below is an example OIDC token from github actions:

{
  "iss": "https://token.actions.githubusercontent.com",
  "aud": "https://rubygems.org",
  "iat": 1617280000,
  "exp": 1617283600,
  "nbf": 1617280000,
  "jti": "1234567890",
  "github": {
    "actor": "octocat",
    "event": {
      "name": "push",
      "path": "refs/heads/main",
      "sha": "1234567890abcdef",
      "ref": "refs/heads/main",
      "workflow": "build",
      "action": "push",
      "head_ref": "",
      "base_ref": "",
      "event_name": "push",
      "workspace": "/home/runner/work/my-repo/my-repo",
      "action_repository": "octocat/my-repo",
      "action_ref": "refs/heads/main",
      "action_sha": "1234567890abcdef",
      "run_id": 1234567890,
      "run_number": 1,
      "retention_days": 90,
      "api_url": "https://api.github.com/repos/octocat/my-repo/actions/runs/1234567890"
    },
    "repository": "octocat/my-repo",
    "ref": "refs/heads/main",
    "sha": "1234567890abcdef"
  }
}

Firstly, we've added the concept of "API key roles" to the platform. These roles can be configured with access policies and preset API key permissions. This means that users can now define specific roles with specific permissions, and then assign those roles to API keys. For example, a user could create a "read-only" role that only allows a key to retrieve gem information, or a "push" role that allows a key to publish new gems.

To use this feature, users can configure their OIDC provider with the necessary scopes and claims to enforce the access policies and permissions they want. Once configured, users can and exchange the OIDC token for an ephemeral API key that has the permissions associated with the configured role. For example, if a user redeems their OIDC token against their "read-only" role, the API key they receive will only be able to retrieve gem information, but not publish new gems.

Detailed implementation notes
  • new provider table, that contains the stuff in https://token.actions.githubusercontent.com/.well-known/openid-configuration & https://token.actions.githubusercontent.com/.well-known/jwks
  • new table that’s something like a role. it binds a provider & user & token created policy
  • a JWT token table, where we can store the details of the JWT from the OIDC issuer and link it to the APIKey it creates
  • and the policy would probably be { scopes: [], rubygems: [], conditions: [] }
  • and then, to get the token, there’d be an API endpoint for the role that takes a POST with the JWT, looks up the role, decodes & validates the JWT, and then checks the claims in the JWT against the conditions, and if it all matches, return an API key
  • and we probably want to add an expires_at to the API key table as well
  • oh and we probably also want to track the API key used to push a version, and then only ever soft-delete the API keys
  • that way, if its pushed using OIDC, we can (down the road) expose some of the claims

Drawbacks

This is a large change to rubygems.org!

In particular, it puts us down the path of "soft deleting" records such as API tokens, instead of removing them from the DB entirely. That's a paradigm shift (though one we probably wanted anyway), and it requires updating the way we interact with those records to ensure we only allow using "undeleted" rows.

Rationale and Alternatives

  • Why is this design the best in the space of possible designs?
  • What other designs have been considered and what is the rationale for not choosing them?
  • What is the impact of not doing this?

The impact of not adding OIDC support to RubyGems.org would be limiting the platform's ability to integrate with modern authentication systems and identity providers. Users who rely on OIDC for authentication would not be able to easily and securely interact with RubyGems.org using their existing identity provider. This could lead to user frustration, reduced adoption of the platform, and potential security risks if users resort to less secure authentication mechanisms as workarounds. By implementing OIDC support, RubyGems.org enhances its usability, security, and interoperability, enabling seamless integration with a wide range of identity providers and aligning with industry best practices.

Unresolved questions

  • Is the schema for Access Policies extensible enough to support arbitrary OIDC providers / the claims they will sign?
  • What does the design for this look like when we allow onboarding Providers / API Key Roles through the web UI?
  • How should we protect a future API that allows for creating & updating API key roles?
  • What claims, if any, should we surface in the UI / API? What should that look like?
  • How will we determine when to make this feature generally available?