Skip to content

Commit aacd8fb

Browse files
committed
initial commit
0 parents  commit aacd8fb

File tree

6 files changed

+265
-0
lines changed

6 files changed

+265
-0
lines changed

Dockerfile

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
FROM python:3.8
2+
3+
WORKDIR /app
4+
5+
COPY requirements.txt .
6+
COPY rotate_keys.py .
7+
8+
RUN pip3 install -r requirements.txt
9+
10+
CMD [ "python", "/app/rotate_keys.py" ]

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2019 Abi Noda
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# Rotate AWS Access token stored in Github Repository secrets
2+
3+
## Environment Variables
4+
#### AWS_ACCESS_KEY_ID
5+
- Required: ***True***
6+
- Description: Access Key ID of the user being rotated. You can use `${{secrets.ACCESS_KEY_ID}}`
7+
8+
#### AWS_SECRET_ACCESS_KEY
9+
- Required: ***True***
10+
- Description: Secret Access Key ID of the user being rotated. You can use `${{secrets.SECRET_ACCESS_KEY_ID}}`
11+
12+
#### IAM_USERNAME
13+
- Required: ***True***
14+
- Description: Name of IAM user being rotated
15+
16+
#### GITHUB_TOKEN
17+
- Required: ***True***
18+
- Description: Github Token with **Repo Admin** access of the target repo. If being ran in the repo being updated, you can use `${{github.token}}`
19+
20+
#### OWNER_REPOSITORY
21+
- Required: ***True***
22+
- Description: The owner and repository name. For example, octocat/Hello-World. If being ran in the repo being updated, you can use `${{github.repository}}`
23+
24+
#### GITHUB_ACCESS_KEY_NAME
25+
- Required: ***False***
26+
- Default: `access_key_id`
27+
- Description: Name of the secret for the Access Key ID. Setting this overrides the default.
28+
29+
#### GITHUB_SECRET_KEY_NAME
30+
- Required: ***False***
31+
- Default: `secret_key_id`
32+
- Description: Name of the secret for the Secret Access Key ID. Setting this overrides the default.
33+
34+
# Example
35+
## Rotation every monday at 13:00 UTC
36+
```
37+
on:
38+
schedule:
39+
- cron: '* 13 * * 1'
40+
41+
jobs:
42+
rotate:
43+
name: rotate iam user keys
44+
runs-on: ubuntu-latest
45+
steps:
46+
- uses: actions/[email protected]
47+
48+
- name: rotate aws keys
49+
uses: kneemaa/[email protected]
50+
env:
51+
AWS_ACCESS_KEY_ID: ${{ secrets.access_key_name }}
52+
AWS_SECRET_ACCESS_KEY: ${{ secrets.secret_key_name }}
53+
IAM_USERNAME: 'iam-user-name'
54+
GITHUB_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
55+
OWNER_REPOSITORY: ${{ github.repository }}
56+
```
57+
58+
## Adding Slack notification on failure only
59+
```
60+
on:
61+
schedule:
62+
- cron: '* 13 * * 1'
63+
64+
jobs:
65+
rotate:
66+
name: rotate iam user keys
67+
runs-on: ubuntu-latest
68+
steps:
69+
- uses: actions/[email protected]
70+
71+
- name: rotate aws keys
72+
uses: kneemaa/[email protected]
73+
env:
74+
AWS_ACCESS_KEY_ID: ${{ secrets.access_key_name }}
75+
AWS_SECRET_ACCESS_KEY: ${{ secrets.secret_key_name }}
76+
IAM_USERNAME: 'iam-user-name'
77+
GITHUB_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
78+
OWNER_REPOSITORY: ${{ github.repository }}
79+
80+
- name: Send Slack Status
81+
if: failure()
82+
uses: 8398a7/[email protected]
83+
with:
84+
status: ${{job.status}}
85+
author_name: kneema-aws-rotation-action
86+
username: kneema-rotation-bot
87+
text: Rotating the token had a status of ${{ job.status }}
88+
channel: alerts-test
89+
env:
90+
SLACK_WEBHOOK_URL: https://hooks.slack.com/services/.../...
91+
```
92+
## License
93+
The Dockerfile and associated scripts and documentation in this project are released under the [MIT License](LICENSE).

action.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
name: 'Rotate AWS Access Keys'
2+
author: 'Nema Darban <[email protected]>'
3+
description: Github action that rotates the iam user tokens on a schedule
4+
runs:
5+
using: 'docker'
6+
image: 'Dockerfile'
7+
branding:
8+
icon: 'check-circle'
9+
color: 'blue'

requirements.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
pynacl
2+
boto3
3+
requests

rotate_keys.py

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import boto3, requests, json, sys, os
2+
from base64 import b64encode
3+
from nacl import encoding, public
4+
5+
access_key_name = os.getenv('GITHUB_ACCESS_KEY_NAME', "access_key_id")
6+
secret_key_name = os.getenv('GITHUB_SECRET_KEY_NAME', "secret_key_id")
7+
8+
# sets creds for boto3
9+
iam = boto3.client(
10+
'iam',
11+
aws_access_key_id = os.environ['AWS_ACCESS_KEY_ID'],
12+
aws_secret_access_key = os.environ['AWS_SECRET_ACCESS_KEY']
13+
)
14+
15+
def main_function():
16+
iam_username = os.environ['IAM_USERNAME']
17+
github_token = os.environ['GITHUB_TOKEN']
18+
owner_repository = os.environ['OWNER_REPOSITORY']
19+
20+
list_ret = iam.list_access_keys(UserName=iam_username)
21+
starting_num_keys = len(list_ret["AccessKeyMetadata"])
22+
23+
# save current id for deletion later
24+
current_access_id = list_ret["AccessKeyMetadata"][0]["AccessKeyId"]
25+
26+
# Check if two keys already exist, if so, exit 1
27+
if starting_num_keys != 1:
28+
print("There are already 2 keys for this user, Cannot rotate tokens")
29+
sys.exit(1)
30+
else:
31+
print(f"I have {starting_num_keys} token, proceeding.")
32+
33+
#generate new credentials
34+
(new_access_key, new_secret_key) = create_new_keys(iam_username)
35+
36+
#delete old keys
37+
delete_old_keys(iam_username, current_access_id)
38+
39+
#get repo pub key info
40+
(public_key, pub_key_id) = get_pub_key(owner_repository, github_token)
41+
42+
#encrypt the secrets
43+
encrypted_access_key = encrypt(public_key,new_access_key)
44+
encrypted_secret_key = encrypt(public_key,new_secret_key)
45+
46+
#upload secrets
47+
upload_secret(owner_repository,access_key_name,encrypted_access_key,pub_key_id,github_token)
48+
upload_secret(owner_repository,secret_key_name,encrypted_secret_key,pub_key_id,github_token)
49+
50+
sys.exit(0)
51+
52+
def create_new_keys(iam_username):
53+
# create the keys
54+
create_ret = iam.create_access_key(
55+
UserName=iam_username
56+
)
57+
58+
new_access_key = create_ret['AccessKey']['AccessKeyId']
59+
new_secret_key = create_ret['AccessKey']['SecretAccessKey']
60+
61+
# check to see if the keys were created
62+
second_list_ret = iam.list_access_keys(UserName=iam_username)
63+
second_num_keys = len(second_list_ret["AccessKeyMetadata"])
64+
65+
if second_num_keys != 2:
66+
print("new keys failed to generate.")
67+
sys.exit(1)
68+
else:
69+
print("new keys generated, proceeding")
70+
return (new_access_key,new_secret_key)
71+
72+
def delete_old_keys(iam_username,current_access_id):
73+
delete_ret = iam.delete_access_key(
74+
UserName=iam_username,
75+
AccessKeyId=current_access_id
76+
)
77+
78+
if delete_ret['ResponseMetadata']['HTTPStatusCode'] != 200:
79+
print("deletion of original key failed")
80+
sys.exit(1)
81+
82+
## Update Actions Secret
83+
# https://developer.github.com/v3/actions/secrets/#create-or-update-a-secret-for-a-repository
84+
def encrypt(public_key: str, secret_value: str) -> str:
85+
"""Encrypt a Unicode string using the public key."""
86+
public_key = public.PublicKey(public_key.encode("utf-8"), encoding.Base64Encoder())
87+
sealed_box = public.SealedBox(public_key)
88+
encrypted = sealed_box.encrypt(secret_value.encode("utf-8"))
89+
return b64encode(encrypted).decode("utf-8")
90+
91+
def get_pub_key(owner_repo, github_token):
92+
# get public key for encrypting
93+
pub_key_ret = requests.get(
94+
f'https://api.github.com/repos/{owner_repo}/actions/secrets/public-key',
95+
headers={'Authorization': f"token {github_token}"}
96+
)
97+
98+
if not pub_key_ret.status_code == requests.codes.ok:
99+
raise Exception(f"github public key request failed, status code: {pub_key_ret.status_code}, body: {pub_key_ret.text}, vars: {owner_repo} {github_token}")
100+
sys.exit(1)
101+
102+
#convert to json
103+
public_key_info = pub_key_ret.json()
104+
105+
#extract values
106+
public_key = public_key_info['key']
107+
public_key_id = public_key_info['key_id']
108+
109+
return (public_key, public_key_id)
110+
111+
def upload_secret(owner_repo,key_name,encrypted_value,pub_key_id,github_token):
112+
#upload encrypted access key
113+
updated_secret = requests.put(
114+
f'https://api.github.com/repos/{owner_repo}/actions/secrets/{key_name}',
115+
json={
116+
'encrypted_value': encrypted_value,
117+
'key_id': pub_key_id
118+
},
119+
headers={'Authorization': f"token {github_token}"}
120+
)
121+
# status codes github says are valid
122+
good_status_codes = [204,201]
123+
124+
if updated_secret.status_code not in good_status_codes:
125+
print(f'Got status code: {updated_secret.status_code} on updating {key_name}')
126+
sys.exit(1)
127+
128+
# run everything
129+
main_function()

0 commit comments

Comments
 (0)