Jarrett Meyer
Posted on May 31, 2022
Objective
Deploy arbitrary projects using the AWS CDK from a GitHub action.
GitHub recommends using OIDC to allow communication between GitHub actions and AWS. But it wouldn't be an adventure if the instructions worked, would it? My company has blocked creating AWS OpenID providers, so we need a new way around this problem.
Create a Lambda function for rotating credentials
GH Personal Access Token
The first thing you're going to need is a personal access token from GitHub. We are going to save this PAT as a secret in AWS. This assumes you have saved your secret as a key-value pair that looks like
{
"token": "thisismytokentherearemanylikeitbutthisoneismine"
}
const SECRET_NAME = "my-github-pat";
export async function getPersonalAccessToken(): Promise<string> {
const secretsManager = new AWS.SecretsManager();
const value = await secretsManager.getSecretValue({ SecretId: SECRET_NAME }).promise();
const obj = JSON.parse(value || "{}");
return obj.token;
}
Assume role
Assuming a role will allow us to generate temporary credentials for that role.
async function assumeGitHubRole() {
const sts = new AWS.STS();
const assumeRoleParams: AWS.STS.AssumeRoleRequest = {
RoleArn: process.env.ROLE_ARN,
RoleSessionName: process.env.ROLE_SESSION_NAME,
};
return await sts.assumeRole(assumeRoleParams).promise();
}
Save secrets to GitHub
Secrets must be encrypted using the public key for the repository. We are using libsodium crypto_box_seal
function to encrypt the values.
interface SealFunction {
(message: Uint8Array, publicKey: Uint8Array): Uint8Array;
}
function encryptMessage(sealer: SealFunction, message: string, publicKey: Buffer): string {
const messageBuffer = Buffer.from(message);
const encryptedMessage = sealer(messageBuffer, publicKey);
return Buffer.from(encryptedMessage).toString("base64");
}
Update credentials
You now have all of the pieces necessary to update credentials in your GitHub repo.
export async function updateCredentials(): Promise<void> {
try {
const token = await getPersonalAccessToken();
const repos = await request("GET /orgs/{org}/teams/{team_slug}/repos", {
org: "my-org",
team_slug: "my-team",
headers: {
authorization: `token ${token}`,
},
});
const assumeRole = await assumeGitHubRole();
// Ensure that the libsodium library has loaded and is ready to be called.
await sodium.ready;
// Loop through each repository, updating secrets as you go.
const promises = [] as Promise<unknown>[];
for (const repo of repos.data) {
// Get the public key for the repo.
const publicKeyResponse = await request("GET /repos/{owner}/{repo}/actions/secrets/public-key", {
owner: "my-org",
repo: repo.name,
headers: {
authorization: `token ${token}`,
}
};
const publicKey = Buffer.from(publicKeyResponse.data.key, "base64");
const encryptedAccessKeyId = encryptMessage(sodium.crypto_box_seal, assumeRole.Credentials.AccessKeyId, publicKey);
const encryptedSecretAccessKey = encryptMessage(sodium.crypto_box_seal, assumeRole.Credentials.SecretAccessKey, publicKey);
const encryptedSessionToken = encryptMessage(sodium.crypto_box_seal, assumeRole.Credentials.SessionToken, publicKey);
promises.push(updateRepositorySecret(repo.name, "AWS_ACCESS_KEY_ID", getPublicKeyResponse.data.key_id, encryptedAccessKeyId));
promises.push(updateRepositorySecret(repo.name, "AWS_SECRET_ACCESS_KEY", getPublicKeyResponse.data.key_id, encryptedSecretAccessKey));
promises.push(updateRepositorySecret(repo.name, "AWS_SESSION_TOKEN", getPublicKeyResponse.data.key_id, encryptedSessionToken));
}
await Promise.all(promises);
process.exit(0);
} catch (error) {
console.error("Error updating credentials:", JSON.stringify(error));
process.exit(1);
}
}
To be continued...
Posted on May 31, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.