jwtex

package module
v0.0.0-...-7f0a898 Latest Latest
Warning

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

Go to latest
Published: Oct 27, 2023 License: MIT Imports: 19 Imported by: 0

README

jwtex

UPDATE: I'll keep this repo online for educational reasons, but a better approach is to use Cognito's native capabilities (described in this blog post) instead of the code in this repo.

*This README is a work in progress

jwtex is a serverless application that takes JSON Web Tokens (JWTs) in one format and converts them to another format. It also acts as a basic OpenID Connect (OIDC) identity provider to authenticate the emitted JWTs.

Use cases

tl;dr: Create useful AWS role session tags from JWTs issued by GitHub, GitLab, etc.

GitHub Actions can generate OIDC tokens to authenticate CI/CD jobs. This can be used to federate into many systems, including AWS. The JWT that GitHub generates contains a wealth of information about the job that created it, but most of that useful information is discarded when federating into AWS. For example, here's a GitHub JWT's claims:

{
  "actor": "octocat",
  "aud": "https://github.com/octo-org",
  "base_ref": "",
  "environment": "prod",
  "event_name": "workflow_dispatch",
  "exp": 1632493867,
  "head_ref": "",
  "iat": 1632493567,
  "iss": "https://token.actions.githubusercontent.com",
  "job_workflow_ref": "octo-org/octo-automation/.github/workflows/oidc.yml@refs/heads/main",
  "jti": "example-id",
  "nbf": 1632492967,
  "ref": "refs/heads/main",
  "ref_type": "branch",
  "repository": "octo-org/octo-repo",
  "repository_owner": "octo-org",
  "run_attempt": "2",
  "run_id": "example-run-id",
  "run_number": "10",
  "sha": "example-sha",
  "sub": "repo:octo-org/octo-repo:environment:prod",
  "workflow": "example-workflow"
}

And this is what appears in CloudTrail (I've removed irrelevant fields for brevity):

{
  "eventName": "AssumeRoleWithWebIdentity",
  "eventSource": "sts.amazonaws.com",
  "recipientAccountId": "0123456789012",
  "requestParameters": {
    "roleArn": "arn:aws:iam::0123456789012:role/ExampleGithubRole",
    "roleSessionName": "botocore-session-1631674835"
  },
  "responseElements": {
    "assumedRoleUser": {
      "arn": "arn:aws:sts::0123456789012:assumed-role/ExampleGithubRole/botocore-session-1631674835",
      "assumedRoleId": "AROAY99999AOBPS6VNUFM:botocore-session-1631674835"
    },
    "audience": "https://github.com/octo-org",
    "credentials": {
      "accessKeyId": "ASIAY29999OMG3MKNAG",
      "expiration": "Sep 15, 2021 4:00:36 AM",
      "sessionToken": "IQ[trimmed]lg=="
    },
    "provider": "arn:aws:iam::0123456789012:oidc-provider/token.actions.githubusercontent.com",
    "subjectFromWebIdentityToken": "repo:octo-org/octo-repo:environment:prod"
  },
  "sourceIPAddress": "104.211.45.236",
  "userAgent": "aws-cli/2.2.35 Python/3.8.8 Linux/5.8.0-1040-azure exe/x86_64.ubuntu.20 prompt/off command/sts.get-caller-identity",
  "userIdentity": {
    "identityProvider": "arn:aws:iam::0123456789012:oidc-provider/token.actions.githubusercontent.com",
    "principalId": "arn:aws:iam::0123456789012:oidc-provider/token.actions.githubusercontent.com:https://github.com/octo-org:repo:octo-org/octo-repo:environment:prod",
    "type": "WebIdentityUser",
    "userName": "repo:octo-org/octo-repo:environment:prod"
  }
}

The only useful information that is passed through is the sub. It would be really great if we could a) record other claims of the GitHub JWT in CloudTrail and b) use those other claims as AWS IAM role session tags.

Let's make it happen

Here's how to deploy this:

  • Create a new AWS account in your org solely for running jwtex. You should minimise who has access to it as it is a sensitive service.

  • Deploy jwtex into that account:

# jwtex.yml
Transform: AWS::Serverless-2016-10-31
Resources:
  jwtex:
    Type: AWS::Serverless::Application
    Properties:
      Location:
        ApplicationId: 'arn:aws:serverlessrepo:us-east-1:607481581596:applications/jwtex'
        SemanticVersion: '0.1.0'
    Parameters:
      Prefix: jwtex # ssm parameter prefix *without* leading slash
      CertificateArn: arn:aws:acm:us-east-1:0123456789012:certificate/ae2265dc-6397-40cf-b8e4-24f890e26d2e
      DomainName: jwtex.example.com
      HostedZoneId: Z1234YD7WANM86
      MapperFunctionArn: !GetAtt Mapper.Arn

  GithubIssuer:
    Type: AWS::SSM::Parameter
    Properties:
      Name: /jwtex/issuers/github
      Type: String
      Value: '{"issuer": "https://token.actions.githubusercontent.com"}'

  Mapper:
    Type: AWS::Serverless::Function
    Properties:
      Architectures: [arm64]
      Runtime: nodejs14.x
      Handler: mapper.handler
      CodeUri: ./mapper.js
// mapper.js
module.exports.handler = async function(input) {
    const claims = input.claims;

    if (claims.repository_owner !== "octo-org") {
        return { allow: false };
    }

    // let's extract these claims from the github jwt into
    // role session tags
    const interestingClaims = [
        "actor",
        "event_name",
        "ref",
        "repository",
        "run_attempt",
        "run_id",
        "run_number",
        "sha",
        "workflow"
    ];
    
    const tags = Object.fromEntries(interestingClaims.map(name => [
        name,
        [claims[name]]
    ]));

    claims["https://aws.amazon.com/tags"] = {
        principal_tags: tags,
        transitive_tag_keys: [],
    };

    return { allow: true, claims };
}
  • Deploy your new OIDC IdP and roles for GitHub into each AWS account in the org:
Resources:
  JwtexOidc:
    Type: AWS::IAM::OIDCProvider
    Properties:
      Url: https://jwtex.example.com
      ThumbprintList: ["TODO: insert thumbprint here"]
      ClientIdList: [https://github.com/octo-org]
      
  Role:
    Type: AWS::IAM::Role
    Properties:
      RoleName: ExampleGithubRole
      ManagedPolicyArns: [arn:aws:iam::aws:policy/ReadOnlyAccess]
      AssumeRolePolicyDocument:
        Statement:
          - Effect: Allow
            Action: sts:AssumeRoleWithWebIdentity
            Principal:
              Federated: !Ref JwtexOidc
            Condition:
              StringEquals:
                aws:RequestTag/repository: octo-org/octo-repo
                aws:RequestTag/ref: refs/heads/main
          - Effect: Allow
            Action: sts:TagSession
            Principal:
              Federated: !Ref JwtexOidc
  • Update your GHA workflows to exchange your GitHub JWT for a new JWT:
on:
  push:

permissions:
  id-token: write
  contents: read

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Configure AWS
        run: |
          export AWS_WEB_IDENTITY_TOKEN_FILE=/tmp/awscreds
          echo AWS_WEB_IDENTITY_TOKEN_FILE=$AWS_WEB_IDENTITY_TOKEN_FILE >> $GITHUB_ENV
          
          github_jwt=$(curl -s -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" "$ACTIONS_ID_TOKEN_REQUEST_URL" | jq -r '.value')
          url="https://jwtex.example.com/exchange?issuerId=github"
          curl -s --data-binary "$github_jwt" -o $AWS_WEB_IDENTITY_TOKEN_FILE $url
          
      - run: aws sts get-caller-identity --region us-east-1
        env:
          AWS_ROLE_ARN: arn:aws:iam::0123456789012:role/ExampleGithubRole

Now your role sessions have those helpful tags and your CloudTrail entries have been enriched:

diff --git cloudtrail.json cloudtrail.json
index 649867d..2784d1a 100644
--- cloudtrail.json
+++ cloudtrail.json
@@ -3,8 +3,20 @@
   "eventSource": "sts.amazonaws.com",
   "recipientAccountId": "0123456789012",
   "requestParameters": {
+    "principalTags": {
+      "actor": "octocat",
+      "event_name": "workflow_dispatch",
+      "ref": "refs/heads/main",
+      "repository": "octo-org/octo-repo",
+      "run_attempt": "2",
+      "run_id": "example-run-id",
+      "run_number": "10",
+      "sha": "example-sha",
+      "workflow": "example-workflow"
+    },
+    "transitiveTagKeys": [],
     "roleArn": "arn:aws:iam::0123456789012:role/ExampleGithubRole",
     "roleSessionName": "botocore-session-1631674835"
   },
   "responseElements": {
     "assumedRoleUser": {
@@ -17,14 +29,15 @@
       "expiration": "Sep 15, 2021 4:00:36 AM",
       "sessionToken": "IQ[trimmed]lg=="
     },
-    "provider": "arn:aws:iam::0123456789012:oidc-provider/token.actions.githubusercontent.com",
+    "packedPolicySize": 44,
+    "provider": "arn:aws:iam::0123456789012:oidc-provider/jwtex.example.com",
     "subjectFromWebIdentityToken": "repo:octo-org/octo-repo:environment:prod"
   },
   "sourceIPAddress": "104.211.45.236",
   "userAgent": "aws-cli/2.2.35 Python/3.8.8 Linux/5.8.0-1040-azure exe/x86_64.ubuntu.20 prompt/off command/sts.get-caller-identity",
   "userIdentity": {
-    "identityProvider": "arn:aws:iam::0123456789012:oidc-provider/token.actions.githubusercontent.com",
-    "principalId": "arn:aws:iam::0123456789012:oidc-provider/token.actions.githubusercontent.com:https://github.com/octo-org:repo:octo-org/octo-repo:environment:prod",
+    "identityProvider": "arn:aws:iam::0123456789012:oidc-provider/jwtex.example.com",
+    "principalId": "arn:aws:iam::0123456789012:oidc-provider/jwtex.example.com:https://github.com/octo-org:repo:octo-org/octo-repo:environment:prod",
     "type": "WebIdentityUser",
     "userName": "repo:octo-org/octo-repo:environment:prod"
   }
--

Documentation

Index

Constants

View Source
const ClaimsMapperVersion = "1.0"

Variables

This section is empty.

Functions

This section is empty.

Types

type ClaimsMapperInput

type ClaimsMapperInput struct {
	Version  string                 `json:"version"`
	IssuerId string                 `json:"issuerId"`
	Claims   map[string]interface{} `json:"claims"`
}

type ClaimsMapperOutput

type ClaimsMapperOutput struct {
	Allow  bool                   `json:"allow"`
	Claims map[string]interface{} `json:"claims"`
}

type ExchangeInput

type ExchangeInput struct {
	IssuerId string
	Jwt      string
}

type ExchangeOutput

type ExchangeOutput struct {
	Allowed bool
	Jwt     string
}

type Server

type Server struct {
	// contains filtered or unexported fields
}

func NewServer

func NewServer(issuer string, keyId string, signer crypto.Signer, verifiers map[string]*oidc.IDTokenVerifier, mapperArn string, lambda lambdaiface.LambdaAPI) *Server

func (*Server) ServeHTTP

func (srv *Server) ServeHTTP(w http.ResponseWriter, r *http.Request)

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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