Skip to content
This repository has been archived by the owner on Jun 11, 2020. It is now read-only.

Commit

Permalink
Handling expiration of secrets
Browse files Browse the repository at this point in the history
* Add DynamoDB stream for AUTHORIZATION table
* Set expire TTL on AUTHORIZATION table
* Code to reset TTL and last_accessed
* After EXPIRES_IN_DAYS (default 14), clean up the secret (if it still exists)
  • Loading branch information
cuppett committed Jan 9, 2020
1 parent 2f935d3 commit 06b0c26
Show file tree
Hide file tree
Showing 5 changed files with 107 additions and 27 deletions.
43 changes: 38 additions & 5 deletions assets/broker-webhook/cloudformation/deployment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,18 @@ Resources:
AttributeType: S
SSESpecification:
SSEEnabled: true
TimeToLiveSpecification:
AttributeName: expires
Enabled: true
StreamSpecification:
StreamViewType: OLD_IMAGE
DeletedRecordSource:
Type: AWS::Lambda::EventSourceMapping
Properties:
Enabled: true
EventSourceArn: !GetAtt Authorizations.StreamArn
FunctionName: !GetAtt OcpBrokerWebhook.Arn
StartingPosition: TRIM_HORIZON
OcpBrokerWebhook:
Type: 'AWS::Serverless::Function'
Properties:
Expand Down Expand Up @@ -186,20 +198,41 @@ Resources:
Action:
- 'ssm:GetParameter'
Resource: !Sub 'arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:parameter/${KubeConfig}'
- PolicyName: UseDynamoDbTables
- PolicyName: ReadAllowancesTable
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- 'dynamodb:PutItem'
- 'dynamodb:GetItem'
- 'dynamodb:DeleteItem'
- 'dynamodb:DescribeTable'
- 'dynamodb:GetItem'
- 'dynamodb:Query'
- 'dynamodb:UpdateTable'
Resource:
- !GetAtt Allowances.Arn
- PolicyName: UseAuthorizationTable
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- 'dynamodb:DescribeTable'
- 'dynamodb:GetItem'
- 'dynamodb:ListStreams'
- 'dynamodb:PutItem'
- 'dynamodb:Query'
- 'dynamodb:UpdateItem'
Resource:
- !GetAtt Authorizations.Arn
- PolicyName: UseDynamoDbAuthorizationStream
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- 'dynamodb:DescribeStream'
- 'dynamodb:GetRecords'
- 'dynamodb:GetShardIterator'
Resource:
- !GetAtt Authorizations.StreamArn
ManagedPolicyArns:
- !Sub arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
25 changes: 25 additions & 0 deletions broker.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import datetime
import json
import logging
import math
import os

from botocore.exceptions import ClientError
Expand All @@ -30,7 +31,31 @@ def _get_arn(lookup_token):
row = client.get_item(TableName=os.getenv('AUTH_TABLE', 'role_perms'),
Key={'auth_token': {'S': lookup_token}})
if row is not None:
# Resetting/refreshing the last_accessed/expires TTLs
try:
now = datetime.datetime.now()
expires_days_in_seconds = os.getenv('EXPIRES_IN_DAYS', 14) * 86400
expires_ts = now + datetime.timedelta(seconds=expires_days_in_seconds)
expires_ttl = math.floor(expires_ts.timestamp())
last_accessed = math.floor(now.timestamp())

client.update_item(
TableName=os.getenv('AUTH_TABLE', 'role_perms'),
Key={
'auth_token': { 'S': lookup_token }
},
UpdateExpression="set expires = :e, last_accessed=:l",
ExpressionAttributeValues={
':e': { 'N': str(expires_ttl) },
':l': { 'N': str(last_accessed) }
},
ReturnValues='NONE'
)
except Exception as e:
_logger.error("Unexpected error: %s" % e)

return row['Item']['role_arn']['S']

else:
return None
else:
Expand Down
33 changes: 22 additions & 11 deletions index.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,28 @@ def handler(event, context):
_logger.debug(event)
to_return = None

if event['httpMethod'] == 'GET':
to_return = broker.handler(event, context)
elif event['httpMethod'] == 'POST':
to_return = webhook.handler(event, context)

if to_return is None:
to_return = {
'headers': {'Content-Type': 'application/json'},
'statusCode': 503,
'body': {'data': {'output': 'Invalid invocation'}}
}
# Processing the API Gateway forwarded requests.
if 'httpMethod' in event:
if event['httpMethod'] == 'GET':
to_return = broker.handler(event, context)
elif event['httpMethod'] == 'POST':
to_return = webhook.handler(event, context)

if to_return is None:
to_return = {
'headers': {'Content-Type': 'application/json'},
'statusCode': 503,
'body': {'data': {'output': 'Invalid invocation'}}
}

# Processing DynamoDB record removals
elif 'Records' in event:
for record in event['Records']:
if record['eventName'] == 'REMOVE':
webhook.remove_secret(
record['dynamodb']['OldImage']['namespace']['S'],
record['dynamodb']['OldImage']['secret_name']['S']
)

return to_return

Expand Down
7 changes: 2 additions & 5 deletions user_guide.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ The webhook is responsible for identifying of pods created those permitted to AW

This webhook will:

. Watch for Pod CREATE/DELETE
. Watch for Pod CREATE
. On CREATE:
.. Identify if the Pod is in a project and service account we know a role can attached to (query to DynamoDB)
.. Query the OpenShift API server for the desired role on the serviceaccount annotation “eks.amazonaws.com/role-arn”
Expand All @@ -57,9 +57,6 @@ This webhook will:
... Add environment variables to each container:
.... AWS_CONTAINER_CREDENTIALS_FULL_URI set to the endpoint of localhost for proxy
.... AWS_CONTAINER_AUTHORIZATION_TOKEN set to the secret from above
. On DELETE:
.. Identify if the Pod previously was given an authorization token
.. Clean it up if so

== Installation

Expand Down Expand Up @@ -214,7 +211,7 @@ webhooks:
clientConfig:
url: https://RESTAPI.execute-api.REGION.amazonaws.com/Prod
rules:
- operations: [ "CREATE", "DELETE" ]
- operations: [ "CREATE" ]
apiGroups: [""]
apiVersions: ["v1"]
resources: ["pods"]
Expand Down
26 changes: 20 additions & 6 deletions webhook.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@
import base64
import boto3
import copy
import datetime
import json
import jsonpatch
import logging
import math
import os
import random
import string
Expand Down Expand Up @@ -121,6 +123,11 @@ def _get_allowed_arns(namespace: string, service_account: string) -> []:
def _insert_auth_row(auth_token: string, role_arn: string, secret_name: string, namespace: string,
service_account: string) -> None:
dynamo = boto3.client('dynamodb')

expires_days_in_seconds = os.getenv('EXPIRES_IN_DAYS', 14) * 86400
expires_ts = datetime.datetime.now() + datetime.timedelta(seconds=expires_days_in_seconds)
expires_ttl = math.floor(expires_ts.timestamp())

item = {
'auth_token': {
'S': auth_token
Expand All @@ -136,8 +143,12 @@ def _insert_auth_row(auth_token: string, role_arn: string, secret_name: string,
},
'service_account': {
'S': service_account
},
'expires': {
'N': str(expires_ttl)
}
}

dynamo.put_item(TableName=os.getenv('AUTH_TABLE', 'role_perms'), Item=item)


Expand Down Expand Up @@ -169,9 +180,14 @@ def _get_auth_secret(namespace: string, service_account: string) -> string:
return None


def _remove_auth_row(namespace: string, pod_name: string) -> None:
"""TODO: Need a way to identify this row, pod_name is not guaranteed to exist when using generated name."""
pass
def remove_secret(namespace: string, secret_name: string) -> None:
_get_kube_config()
try:
v1 = client.CoreV1Api()
resp = v1.delete_namespaced_secret(secret_name, namespace)
_logger.debug('Result of the call: %s', resp)
except ApiException as e:
_logger.error("Unknown error deleting secret: %s" % e)


def _update_pod_spec(original: [], secret_name: string) -> []:
Expand Down Expand Up @@ -245,9 +261,7 @@ def handler(event, context):

if operation == 'CREATE':
patchset = _generate_patchset(body['request'])
elif operation == 'DELETE':
pod_name = body['request']['name']
_remove_auth_row(namespace, pod_name)

except Exception as e:
_logger.error('Unhandled exception in webhook: %s', e)

Expand Down

0 comments on commit 06b0c26

Please sign in to comment.