pull/1/head
lance 5 years ago
commit ae892791a9

11
.gitignore vendored

@ -0,0 +1,11 @@
.venv
config.py
__pycache__
.DS_Store
.idea
zappa_settings.json
*zip
*.egg-info
*pyc
build/
dist/

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019 Lance Allen
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

@ -0,0 +1 @@
# secretshare

@ -0,0 +1,10 @@
#!/bin/bash
# Fetch all secrets
export secrets=$(aws secretsmanager list-secrets --query "SecretList[].Name[]" | jq -r ".[]")
# Loop through and delete each one
while read -r line; do
echo "[+] Deleting ${line}";
aws secretsmanager delete-secret --secret-id "${line}";
done <<< "$secrets"

@ -0,0 +1,7 @@
#!/bin/bash
source .venv/bin/activate
export FLASK_APP=secretshare/app.py
export FLASK_SECRETS=config.py
export FLASK_DEBUG=1
flask run

@ -0,0 +1,9 @@
#!/bin/bash
# This script assumes a lot - you should probably just set
# it up on your own rather than relying on this script
rm -rf .venv
python3 -m venv .venv
source .venv/bin/activate
pip install .

@ -0,0 +1,10 @@
#!/bin/bash
echo -e "[+] Creating a secret"
curl -X POST 127.0.0.1:5000/secret/ \
-H "Content-Type: application/json" \
-d "{
\"username\": \"$(whoami)\",
\"password\": \"$(openssl rand -base64 32)\",
\"message\": \"This is my secret password!\"
}"

@ -0,0 +1 @@
from secretshare._version import __version__

@ -0,0 +1 @@
__version__ = "0.1"

@ -0,0 +1,103 @@
#!/usr/bin/env python
from flask import Flask, jsonify, request, make_response
from flask_restplus import Api, Resource, reqparse, fields
from secretshare import __version__
from secretshare.library import secretsmanager
# Define Flask application
app = Flask(__name__)
app.config.from_envvar('FLASK_SECRETS')
api = Api(app, version=__version__, title=app.config['APP_NAME'],
description='Simple secret sharing API using AWS Secrets Manager'
)
# Define models
secret_data = api.model('secret_data', {
'username': fields.String,
'password': fields.String,
'message': fields.String,
'expiration': fields.DateTime,
})
response_data = api.inherit('response_data', secret_data, {
'token': fields.String,
'error_msg': fields.String,
'error_id': fields.String
})
# Parse query strings/arguments
parser = reqparse.RequestParser()
parser.add_argument('token', type=str, help='Token provided when creating the secret')
@api.route('/secret/')
class Secrets(Resource):
"""Represents available actions
for managing secrets on AWS Secrets
Manager. Can retrieve and create secrets.
"""
@api.doc('retrieve', parser=parser)
@api.marshal_with(response_data, code=200, skip_none=True)
def get(self):
args = parser.parse_args()
if args.get('token'):
secret_name = args.get('token')
secret = secretsmanager.Secret(secret_name=secret_name)
if secret.exists and not secret.expired:
# If secret exists and not expired, return secret
return secret.retrieve(), 200
else:
# If secret is expired or doesn't exist, return error
return {
'error_msg': 'This secret is expired or does not exist.',
'error_id': 'expired_secret'
}, 400
else:
# If no query string provided, return error
return {
'error_msg': 'No secret token provided.',
'error_id': 'no_token'
}, 400
@api.doc('create')
@api.expect(secret_data, validate=True)
@api.marshal_with(response_data, code=201, skip_none=True)
def post(self):
if api.payload:
try:
secret = secretsmanager.Secret()
secret.create(
username=api.payload.get('username', ''),
password=api.payload.get('password', ''),
message=api.payload.get('message', ''),
expiration=api.payload.get('expiration', '')
)
return {'token': secret.secret_name}, 201
except ValueError as err:
return {
'error_msg': 'Invalid expiration date',
'error_id': err
}, 400
else:
return {
'error_msg': 'No secret JSON payload provided',
'error_id': 'no_payload'
}, 400
@app.errorhandler(404)
def not_found(error):
response = make_response(jsonify({
'error_msg': 'Route not found',
'error_id': 'not_found'}
), 404)
return response
if __name__ == '__main__':
app.run()

@ -0,0 +1,21 @@
#!/usr/bin/env python
from boto3 import client as boto3_client
from arrow import get as arrow_get
from arrow import utcnow as arrow_utcnow
from secretshare.library import secretsmanager
def purge_expired_secrets():
"""Purge all expired secrets. This is run as
a recurring Lambda function.
"""
client = boto3_client('secretsmanager')
all_secrets = secretsmanager.list_secrets(client)
for secret_data in all_secrets:
secret = secretsmanager.Secret()
secret.check_tags_expired(secret_data)
if secret.expired:
print(f"[+] Purging expired secret {secret_data['Name']}")
secretsmanager.delete_secret(client, secret_data['Name'])

@ -0,0 +1,2 @@
APP_NAME = "secretshare"
DEFAULT_HOURS = 1

@ -0,0 +1,150 @@
#!/usr/bin/env python
from boto3 import client as boto3_client
from json import loads as json_loads
from json import dumps as json_dumps
from arrow import get as arrow_get
from arrow import utcnow as arrow_utcnow
from flask import current_app as app
from secrets import token_urlsafe
class Secret(object):
"""
Secret objects represent a secret on
AWS Secrets Manager. Methods involve actions
you can perform on these items via the AWS API.
"""
def __init__(self, secret_name=''):
self.secret_name = secret_name
if self.secret_name:
self.check()
def check(self):
"""Check if secret exists and if expired or not"""
client = boto3_client('secretsmanager')
try:
response = client.describe_secret(
SecretId=self.secret_name
)
self.check_tags_expired(response)
self.exists = True
except:
self.exists = False
self.expired = None
def check_tags_expired(self, json_data):
"""Given a DescribeSecret JSON response and assess whether
the 'Expiration' tag shows the secret is expired or not
"""
now = arrow_utcnow()
for tag in json_data['Tags']:
if 'Expiration' in tag.values():
self.expiration = tag['Value']
expiration_date = arrow_get(tag['Value'])
if (expiration_date - now).total_seconds() < 0:
# If delta is negative, we've passed expiration
self.expired = True
else:
self.expired = False
return
def create(self, username, password, message, expiration=''):
"""Create a secret"""
now = arrow_utcnow()
client = boto3_client('secretsmanager')
self.secret_name = token_urlsafe(32)
self.username = str(username)
self.password = str(password)
self.message = str(message)
data_object = {
"username": self.username,
"password": self.password,
"message": self.message
}
if not expiration:
# Set default time expiration
self.expiration = str(now.shift(hours=app.config['DEFAULT_HOURS']))
else:
# Otherwise validate we have a good expiration date
try:
arrow_get(expiration)
except:
raise ValueError('invalid_datestamp')
try:
assert (arrow_get(expiration) - now).total_seconds() > 0
except:
raise ValueError('expired_datestamp')
self.expiration = str(arrow_get(expiration))
response = client.create_secret(
Name=self.secret_name,
SecretString=json_dumps(data_object),
Tags=[
{
'Key': 'Expiration',
'Value': self.expiration
},
]
)
return response
def retrieve(self):
"""Retrieve a secret's value as dictionary object"""
client = boto3_client('secretsmanager')
response = client.get_secret_value(
SecretId=self.secret_name
)
secret_value = json_loads(response["SecretString"])
secret_value['expiration'] = self.expiration
return secret_value
# One-off functions used by cleanup Lambda
def list_secrets(boto_client):
"""List all secrets"""
next_token = ""
pagination_finished = False
secrets = []
response = boto_client.list_secrets(
MaxResults=20
)
while not pagination_finished:
for secret in response['SecretList']:
secrets.append(secret)
if 'NextToken' in response:
next_token = response['NextToken']
response = boto_client.list_secrets(
MaxResults=20,
NextToken=next_token
)
else:
pagination_finished = True
return secrets
def delete_secret(boto_client, secret_name):
"""Remove a secret"""
response = boto_client.delete_secret(
SecretId=secret_name,
ForceDeleteWithoutRecovery=True
)
return response

@ -0,0 +1,40 @@
# -*- coding: utf-8 -*-
from setuptools import setup, find_packages
from secretshare import __version__
NAME = "secretshare"
DESCRIPTION = "Flask application for sharing secrets using AWS Secrets Manager"
URL = "https://github.com/lalanza808/secretshare"
EMAIL = "lance@lzatech.org"
AUTHOR = "Lance Allen"
REQUIRES_PYTHON = ">=3.6.0"
VERSION = __version__
REQUIRED = [
"arrow==0.12.1",
"boto3==1.9.74",
"flask-restplus==0.12.1",
"Flask==1.0.2",
"zappa==0.47.1"
]
EXTRAS = {
}
TESTS = [
]
SETUP = [
]
setup(
name=NAME,
version=VERSION,
description=DESCRIPTION,
author=AUTHOR,
author_email=EMAIL,
include_package_data=True,
extras_require=EXTRAS,
install_requires=REQUIRED,
setup_requires=SETUP,
tests_require=TESTS,
packages=find_packages(exclude=['ez_setup']),
zip_safe=False
)

@ -0,0 +1,30 @@
{
"dev": {
"app_function": "secretshare.app.app",
"aws_region": "us-west-2",
"profile_name": "default",
"project_name": "secretshare",
"log_level": "INFO",
"runtime": "python3.6",
"s3_bucket": "##CHANGEME##",
"environment_variables": {
"FLASK_SECRETS": "config.py"
},
"events": [{
"function": "secretshare.cleanup.purge_expired_secrets",
"expression": "rate(12 hours)"
}],
"extra_permissions": [{
"Effect": "Allow",
"Action": [
"secretsmanager:ListSecrets",
"secretsmanager:DescribeSecret",
"secretsmanager:DeleteSecret",
"secretsmanager:GetSecretValue",
"secretsmanager:CreateSecret",
"secretsmanager:TagResource"
],
"Resource": "*"
}]
}
}
Loading…
Cancel
Save