Deploying AWS Credentials to GitHub Secrets
Tags: devops, aws, github
Published: 2022-05-31 16:29:44

## Objective

Deploy arbitrary projects using the [AWS CDK](https://aws.amazon.com/cdk/) from a [GitHub action](https://github.com/features/actions).

[GitHub recommends using OIDC](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services) 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](https://github.com/settings/tokens) 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

```json
{
  "token": "thisismytokentherearemanylikeitbutthisoneismine"
}
```

```ts
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.

```ts
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](https://www.npmjs.com/package/libsodium-wrappers) `crypto_box_seal` function to encrypt the values.

```ts
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.

```ts
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...