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