init
commit
ae892791a9
@ -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,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…
Reference in New Issue