Skip to content

Commit

Permalink
Support Github OIDC authentication (#13)
Browse files Browse the repository at this point in the history
Co-authored-by: Jim Page <jim.page@redmatter.com>
  • Loading branch information
SemiConscious and jim-page authored May 14, 2024
1 parent 03ac3c1 commit b81e663
Show file tree
Hide file tree
Showing 8 changed files with 185 additions and 72 deletions.
1 change: 1 addition & 0 deletions .github/workflows/aws-test-all.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ jobs:
- ami-id: "ami-0944e91aed79c721c"
ami-distro: "Amazon Linux 2023 2023.3.20240108.0"
with:
oidc: false
ami-id: ${{ matrix.ami-id }}
ami-distro: ${{ matrix.ami-distro}}
secrets:
Expand Down
21 changes: 17 additions & 4 deletions .github/workflows/aws-test-on-demand.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ name: Test Distro
on:
workflow_call:
inputs:
oidc:
description: "true if we're using OIDC auth"
required: true
type: boolean
ami-id:
description: "The id of ami to be tested"
required: true
Expand All @@ -21,10 +25,10 @@ on:
secrets:
aws-access-key-id:
description: "The AWS access key"
required: true
required: false
aws-secret-access-key:
description: "The AWS secret access key"
required: true
required: false
github-token:
description: "Github token (PAT)"
required: true
Expand All @@ -47,13 +51,22 @@ jobs:
pull-requests: read
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials
if: ${{ inputs.oidc }}
id: creds
uses: aws-actions/configure-aws-credentials@v4
with:
aws-region: ${{ vars.AWS_REGION }}
role-to-assume: ${{ vars.ASSUME_ROLE_ARN }}
output-credentials: true
- name: Start EC2 runner
id: start-ec2-runner
uses: ./
with:
github_token: ${{ secrets.github-token }}
aws_access_key_id: ${{ secrets.aws-access-key-id }}
aws_secret_access_key: ${{ secrets.aws-secret-access-key }}
aws-access-key-id: ${{ inputs.oidc && steps.creds.outputs.aws-access-key-id || secrets.aws-access-key-id }}
aws-secret-access-key: ${{ inputs.oidc && steps.creds.outputs.aws-secret-access-key || secrets.aws-secret-access-key }}
aws-session-token: ${{ inputs.oidc && steps.creds.outputs.aws-session-token || '' }}
aws_region: "us-west-2"
ec2_instance_type: t2.small
ec2_ami_id: ${{ inputs.ami-id }}
Expand Down
59 changes: 59 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,16 @@ Users can provide their own custom AMI image pre-loaded with all the necessary t

### 2. Setup GitHub Secrets for IAM credentials

#### 2a. Use IAM keys

1. Add your `IAM Access Key ID` and `Secret Access Key` to GitHub Secrets and note the secret names!
2. Modify `${{ secrets.DEPLOY_AWS_ACCESS_KEY_ID }}` and `${{ secrets.DEPLOY_AWS_SECRET_ACCESS_KEY }}` in examples below to match the names of your GH secrets

#### 2b. Use OIDC

1. Configure your EC2 backend to allow a federated connection from github
2. use [configure-aws-credentials](https://github.com/aws-actions/configure-aws-credentials) or similar to authenticate against your backend using OIDC. See the OIDC example for guidance on how this is done. The documentation in the `configure-aws-credendials` is very detailed.

*Note*: For information about required IAM permissions check **IAM role policy** [here](./docs/CrossAccountIAM.md)

### 3. Collect EC2 information:
Expand Down Expand Up @@ -164,6 +171,58 @@ jobs:
steps:
- run: env
```
### Use OIDC

- IAM policy and role setup instructions can be found [here](https://github.com/aws-actions/configure-aws-credentials)
- Modify `ec2_spot_instance_strategy` for other deployment strategies. List of all values can be found [here](action.yaml)

```yaml
jobs:
start-runner:
timeout-minutes: 5 # normally it only takes 1-2 minutes
name: Start self-hosted EC2 runner
runs-on: ubuntu-latest
permissions:
actions: write
contents: read
id-token: write
steps:
- name: Configure AWS credentials
id: creds # name of step, to allow access to outputs
uses: aws-actions/configure-aws-credentials@v4
with:
aws-region: "AWS_REGION"
role-to-assume: "arn:aws:iam::REDACTED:role/REDACTED"
output-credentials: true # output the credentials
- name: Start EC2 runner
id: start-ec2-runner
uses: NextChapterSoftware/ec2-action-builder@v1
with:
github_token: ${{ secrets.GH_PERSONAL_ACCESS_TOKEN }}
aws_access_key_id: ${{ steps.creds.outputs.aws-access-key-id }} # generated by configure-aws-credentials
aws_secret_access_key: ${{ steps.creds.outputs.aws-secret-access-key }} # generated by configure-aws-credentials
aws_session_token: ${{ steps.creds.outputs.aws-session-token }} # generated by configure-aws-credentials
aws_region: "AWS_REGION"
ec2_subnet_id: "SUBNET_ID_REDACTED"
ec2_security_group_id: "SECURITY_GROUP_ID_REDACTED"
ec2_instance_type: t4g.large
ec2_ami_id: ami-0c29a2c5cf69b5a9c
ec2_instance_ttl: 40 # Optional (default is 60 minutes)
ec2_spot_instance_strategy: BestEffort # Other options are: None, SpotOnly, BestEffort, MaxPerformance
ec2_instance_tags: > # Required for IAM role resource permission scoping
[
{"Key": "Owner", "Value": "deploybot"}
]
# Job that runs on the self-hosted runner
run-build:
timeout-minutes: 1
needs:
- start-runner
runs-on: ${{ github.run_id }}
steps:
- run: env
```
## How it all works under the hood

### General instance launch flow
Expand Down
5 changes: 4 additions & 1 deletion action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ inputs:
aws_secret_access_key:
description: 'AWS secret access key'
required: true
aws_session_token:
description: 'AWS session token'
required: false
aws_region:
description: 'AWS Region'
required: true
Expand Down Expand Up @@ -63,4 +66,4 @@ inputs:
default: "none"
runs:
using: 'node20'
main: 'dist/index.js'
main: 'dist/index.js'
15 changes: 15 additions & 0 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ class ActionConfig {
// AWS account and credentials params
this.awsAccessKeyId = core.getInput("aws_access_key_id");
this.awsSecretAccessKey = core.getInput("aws_secret_access_key");
this.awsSessionToken = core.getInput("aws_session_token");
this.awsRegion = core.getInput("aws_region");
this.awsIamRoleArn = core.getInput("aws_iam_role_arn");
this.awsAssumeRole = this.awsIamRoleArn ? true : false;
Expand Down Expand Up @@ -118,6 +119,7 @@ class Ec2Instance {
this.credentials = {
accessKeyId: this.config.awsAccessKeyId,
secretAccessKey: this.config.awsSecretAccessKey,
sessionToken: this.config.awsSessionToken,
};
this.client = new client_ec2_1.EC2({
credentials: this.credentials,
Expand Down Expand Up @@ -170,6 +172,12 @@ class Ec2Instance {
}
getCrossAccountCredentials() {
return __awaiter(this, void 0, void 0, function* () {
// if we have a valid session token then we just pass the credentials through
// possibly this is due to an OIDC/OAuth flow
if (typeof this.credentials.sessionToken == "string" &&
this.credentials.sessionToken != "") {
return Object.assign(this.credentials);
}
const stsClient = new client_sts_1.STS({
credentials: this.credentials,
region: this.config.awsRegion,
Expand Down Expand Up @@ -521,6 +529,7 @@ class Ec2Pricing {
this.credentials = {
accessKeyId: this.config.awsAccessKeyId,
secretAccessKey: this.config.awsSecretAccessKey,
sessionToken: this.config.awsSessionToken,
};
this.client = new client_pricing_1.Pricing({
credentials: this.credentials,
Expand All @@ -542,6 +551,12 @@ class Ec2Pricing {
}
getCrossAccountCredentials() {
return __awaiter(this, void 0, void 0, function* () {
// if we have a valid session token then we just pass the credentials through
// possibly this is due to an OIDC/OAuth flow
if (typeof this.credentials.sessionToken == "string" &&
this.credentials.sessionToken != "") {
return Object.assign(this.credentials);
}
const stsClient = new client_sts_1.STS({
credentials: this.credentials,
region: this.config.awsRegion,
Expand Down
135 changes: 69 additions & 66 deletions src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,80 +2,83 @@ import * as core from "@actions/core";
import * as github from "@actions/github";

export interface ConfigInterface {
awsAccessKeyId: string;
awsSecretAccessKey: string;
awsRegion: string;
awsIamRoleArn: string;
awsAssumeRole: boolean;
awsAccessKeyId: string;
awsSecretAccessKey: string;
awsSessionToken: string;
awsRegion: string;
awsIamRoleArn: string;
awsAssumeRole: boolean;

githubToken: string;
githubJobId: string;
githubRef: string;
githubRepo: string;
githubActionRunnerVersion: string;
githubActionRunnerLabel: string;
githubToken: string;
githubJobId: string;
githubRef: string;
githubRepo: string;
githubActionRunnerVersion: string;
githubActionRunnerLabel: string;

ec2InstanceType: string;
ec2AmiId: string;
ec2InstanceIamRole: string;
ec2InstanceTags: string;
ec2InstanceTtl: string;
ec2SecurityGroupId: string;
ec2SubnetId: string;
ec2SpotInstanceStrategy: string;
ec2InstanceType: string;
ec2AmiId: string;
ec2InstanceIamRole: string;
ec2InstanceTags: string;
ec2InstanceTtl: string;
ec2SecurityGroupId: string;
ec2SubnetId: string;
ec2SpotInstanceStrategy: string;
}

export class ActionConfig implements ConfigInterface {
awsAccessKeyId: string;
awsSecretAccessKey: string;
awsRegion: string;
awsIamRoleArn: string;
awsAssumeRole: boolean;
awsAccessKeyId: string;
awsSecretAccessKey: string;
awsSessionToken: string;
awsRegion: string;
awsIamRoleArn: string;
awsAssumeRole: boolean;

githubToken: string;
githubJobId: string;
githubRef: string;
githubRepo: string;
githubActionRunnerVersion: string;
githubActionRunnerLabel: string;
githubToken: string;
githubJobId: string;
githubRef: string;
githubRepo: string;
githubActionRunnerVersion: string;
githubActionRunnerLabel: string;

ec2InstanceType: string;
ec2AmiId: string;
ec2InstanceIamRole: string;
ec2InstanceTags: string;
ec2InstanceTtl: string;
ec2SecurityGroupId: string;
ec2SubnetId: string;
ec2SpotInstanceStrategy: string;
ec2InstanceType: string;
ec2AmiId: string;
ec2InstanceIamRole: string;
ec2InstanceTags: string;
ec2InstanceTtl: string;
ec2SecurityGroupId: string;
ec2SubnetId: string;
ec2SpotInstanceStrategy: string;

constructor() {
// AWS account and credentials params
this.awsAccessKeyId = core.getInput("aws_access_key_id");
this.awsSecretAccessKey = core.getInput("aws_secret_access_key");
this.awsRegion = core.getInput("aws_region");
this.awsIamRoleArn = core.getInput("aws_iam_role_arn");
this.awsAssumeRole = this.awsIamRoleArn ? true : false;
constructor() {
// AWS account and credentials params
this.awsAccessKeyId = core.getInput("aws_access_key_id");
this.awsSecretAccessKey = core.getInput("aws_secret_access_key");
this.awsSessionToken = core.getInput("aws_session_token");
this.awsRegion = core.getInput("aws_region");
this.awsIamRoleArn = core.getInput("aws_iam_role_arn");
this.awsAssumeRole = this.awsIamRoleArn ? true : false;

// Github params
this.githubToken = core.getInput("github_token");
this.githubJobId = github.context.runId.toString();
this.githubRef = github.context.ref;
this.githubRepo = github.context.repo.repo;
this.githubActionRunnerVersion = core.getInput(
"github_action_runner_version"
);
this.githubActionRunnerLabel = this.githubJobId;
// Github params
this.githubToken = core.getInput("github_token");
this.githubJobId = github.context.runId.toString();
this.githubRef = github.context.ref;
this.githubRepo = github.context.repo.repo;
this.githubActionRunnerVersion = core.getInput(
"github_action_runner_version"
);
this.githubActionRunnerLabel = this.githubJobId;

// Ec2 params
this.ec2InstanceType = core.getInput("ec2_instance_type");
this.ec2AmiId = core.getInput("ec2_ami_id");
this.ec2InstanceIamRole = core.getInput("ec2_instance_iam_role");
this.ec2InstanceTags = core.getInput("ec2_instance_tags");
this.ec2InstanceTtl = core.getInput("ec2_instance_ttl");
this.ec2SubnetId = core.getInput("ec2_subnet_id");
this.ec2SecurityGroupId = core.getInput("ec2_security_group_id");
this.ec2SpotInstanceStrategy = core
.getInput("ec2_spot_instance_strategy")
.toLowerCase();
}
// Ec2 params
this.ec2InstanceType = core.getInput("ec2_instance_type");
this.ec2AmiId = core.getInput("ec2_ami_id");
this.ec2InstanceIamRole = core.getInput("ec2_instance_iam_role");
this.ec2InstanceTags = core.getInput("ec2_instance_tags");
this.ec2InstanceTtl = core.getInput("ec2_instance_ttl");
this.ec2SubnetId = core.getInput("ec2_subnet_id");
this.ec2SecurityGroupId = core.getInput("ec2_security_group_id");
this.ec2SpotInstanceStrategy = core
.getInput("ec2_spot_instance_strategy")
.toLowerCase();
}
}
10 changes: 10 additions & 0 deletions src/ec2/ec2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export class Ec2Instance {
this.credentials = {
accessKeyId: this.config.awsAccessKeyId,
secretAccessKey: this.config.awsSecretAccessKey,
sessionToken: this.config.awsSessionToken,
};

this.client = new EC2({
Expand Down Expand Up @@ -89,6 +90,15 @@ export class Ec2Instance {
}

async getCrossAccountCredentials(): Promise<AwsCredentialIdentity> {
// if we have a valid session token then we just pass the credentials through
// possibly this is due to an OIDC/OAuth flow
if (
typeof this.credentials.sessionToken == "string" &&
this.credentials.sessionToken != ""
) {
return Object.assign(this.credentials);
}

const stsClient = new STS({
credentials: this.credentials,
region: this.config.awsRegion,
Expand Down
Loading

0 comments on commit b81e663

Please sign in to comment.