Skip to content

Commit

Permalink
some sweet diff crumbs
Browse files Browse the repository at this point in the history
  • Loading branch information
krysopath committed Apr 9, 2019
1 parent aa255ba commit 367f0b2
Show file tree
Hide file tree
Showing 8 changed files with 265 additions and 15 deletions.
139 changes: 139 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,78 @@
# security
secring.*

# Emacs
# -*- mode: gitignore; -*-
*~
\#*\#
/.emacs.desktop
/.emacs.desktop.lock
*.elc
auto-save-list
tramp
.\#*

# Org-mode
.org-id-locations
*_archive

# flymake-mode
*_flymake.*

# eshell files
/eshell/history
/eshell/lastdir

# elpa packages
/elpa/

# reftex files
*.rel

# AUCTeX auto folder
/auto/

# cask packages
.cask/
dist/

# Flycheck
flycheck_*.el

# server auth directory
/server/

# projectiles files
.projectile

# directory configuration
.dir-locals.el

# network security
/network-security.data


# Vim files
# Swap
[._]*.s[a-v][a-z]
[._]*.sw[a-p]
[._]s[a-rt-v][a-z]
[._]ss[a-gi-z]
[._]sw[a-p]

# Session
Session.vim

# Temporary
.netrwhist
*~
# Auto-generated tag files
tags
# Persistent undo
[._]*.un~



# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
Expand Down Expand Up @@ -102,3 +177,67 @@ venv.bak/

# mypy
.mypy_cache/

# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf

# Generated files
.idea/**/contentModel.xml

# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml

# Gradle
.idea/**/gradle.xml
.idea/**/libraries

# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/modules.xml
# .idea/*.iml
# .idea/modules

# CMake
cmake-build-*/

# Mongo Explorer plugin
.idea/**/mongoSettings.xml

# File-based project format
*.iws

# IntelliJ
out/

# mpeltonen/sbt-idea plugin
.idea_modules/

# JIRA plugin
atlassian-ide-plugin.xml

# Cursive Clojure plugin
.idea/replstate.xml

# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties

# Editor-based Rest Client
.idea/httpRequests

# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
13 changes: 10 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,17 @@ WORKDIR /code
COPY requirements.txt .

RUN mkdir secrets \
&& apk add --no-cache gnupg \
&& pip3 install -r requirements.txt
&& apk add --no-cache \
gnupg\
openssl\
&& pip3 install -r requirements.txt\
&& adduser -D vaultify\
&& chown vaultify .

COPY ./vaultify vaultify
COPY ./entry.py entry.py

ENTRYPOINT ["python3", "/code/entry.py"]
USER vaultify

ENTRYPOINT ["python3"]
CMD ["/code/entry.py"]
17 changes: 17 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,3 +1,20 @@

run/test:
rm -rf tests/new* assets/*
gpg \
--symmetric\
--batch\
--passphrase=abc\
-o assets/test.gpg\
tests/secrets.env
openssl enc \
-k abc \
-aes-256-cbc \
-salt \
-a \
-in tests/secrets.env \
-out assets/test.enc
VAULTIFY_LOG_LEVEL=WARNING python3 runtests.py

manual:
@groff -man -Tascii man/vaultify.1
1 change: 0 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
hvac>=0.6.4
pyyaml
PyYAML==3.12
25 changes: 25 additions & 0 deletions runtests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#!/usr/bin/env python3

import unittest
import doctest
import os

files = []
root_dir = 'vaultify/'

for root, _, filenames in os.walk(root_dir):
for filename in filenames:
if (filename == '__init__.py'
or filename[-3:] != '.py'
or filename.startswith('.#')
):
continue
f = os.path.join(root, filename)
f = f.replace('/', '.')
f = f[:-3]
files.append(f)

suite = unittest.TestSuite()
for module in files:
suite.addTest(doctest.DocTestSuite(module))
unittest.TextTestRunner(verbosity=1).run(suite)
4 changes: 4 additions & 0 deletions tests/echo-vars.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/bin/sh
printf "K1=%s\n" "$K1"
printf "K2=%s\n" "$K2"
printf "K3=%s" "$K3"
77 changes: 67 additions & 10 deletions vaultify/consumers.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,40 @@ class DotEnvWriter(Consumer):
"""
This Consumer will write secrets as a set of sourceable `export KEY=value`
lines
We should fail for existing destination files, when trying to consume:
>>> DotEnvWriter('/etc/passwd').consume_secrets({"":""})
Traceback (most recent call last):
...
RuntimeError: /etc/passwd already exists
We want the dictionary outputted in the right format:
>>> DotEnvWriter('tests/new.env').consume_secrets({"K1":"V1","K2":"V2"})
>>> open('tests/new.env').read()
"export K1='V1'\\nexport K2='V2'\\n"
We may specify to overwrite the destination file
>>> DotEnvWriter(
... 'tests/new.env',
... overwrite=True
... ).consume_secrets({"K1":"V1","K2":"V2"})
>>> open('tests/new.env').read()
"export K1='V1'\\nexport K2='V2'\\n"
"""
def __init__(self, path: str = './secrets.env'):
self.path = os.environ.get("VAULTIFY_DESTFILE", path)
def __init__(self, path: str, overwrite: bool = False):
self.path = path
self.overwrite = overwrite

def consume_secrets(self, data: dict):
def consume_secrets(self, data: dict) -> bool:
"""
Write data as `export K=V` pairs into self.path, but die if self.path
already exists.
That file can be sourced or evaluated with the unix shell
"""
if os.path.exists(self.path):
if os.path.exists(self.path) and not self.overwrite:
raise RuntimeError(f'{self.path} already exists')

with open(self.path, 'w') as secrets_file:
Expand All @@ -51,14 +73,35 @@ def consume_secrets(self, data: dict):
)
)
secrets_file.write('\n')



class JsonWriter(Consumer):
"""
This Consumer will write secrets as a JSON dictionary
>>> JsonWriter('/etc/passwd').consume_secrets({"K":"V"})
Traceback (most recent call last):
...
RuntimeError: /etc/passwd already exists
We want the dictionary outputted in the right format:
>>> JsonWriter('tests/new.json').consume_secrets({"K1":"V1","K2":"V2"})
>>> open('tests/new.json').read()
'{\\n "K1": "V1",\\n "K2": "V2"\\n}\\n'
We may specify to overwrite the destination file
>>> JsonWriter(
... 'tests/new.json',
... overwrite=True
... ).consume_secrets({"K1":"V1","K2":"V2"})
>>> open('tests/new.json').read()
'{\\n "K1": "V1",\\n "K2": "V2"\\n}\\n'
"""
def __init__(self, path='./secrets.json'):
self.path = os.environ.get("VAULTIFY_DESTFILE", path)
def __init__(self, path: str, overwrite: bool = False):
self.path = path
self.overwrite = overwrite

def __str__(self):
return f'{self.__class__}->{self.path}'
Expand All @@ -68,7 +111,7 @@ def consume_secrets(self, data: dict):
Write data as json into fname
That file can be evaluated by any json-aware application.
"""
if os.path.exists(self.path):
if os.path.exists(self.path) and not self.overwrite:
raise RuntimeError(f'{self.path} already exists')

with open(self.path, 'w') as json_file:
Expand All @@ -85,8 +128,21 @@ class EnvRunner(Consumer):
"""
This Consumer will update the environment and then run a subprocess in that
altered environment.
We carry our local environment over into the spawned process:
>>> os.environ.update({"K1": "V1"})
>>> EnvRunner('./tests/echo-vars.sh').consume_secrets({"K2":"V2","K3":"V3"})
K1=V1
K2=V2
K3=V3
We fail when the command can not be found:
>>> EnvRunner('nowhere.sh').consume_secrets({"K1":"V1"})
Traceback (most recent call last):
...
FileNotFoundError: [Errno 2] No such file or directory: 'nowhere.sh': 'nowhere.sh'
"""
def __init__(self, path: str = 'env'):
def __init__(self, path: str):
self.path = os.environ.get(
"VAULTIFY_TARGET", path
).split()
Expand All @@ -106,6 +162,7 @@ def consume_secrets(self, data: dict):
f'{self} enriched the environment')

try:
# TODO Overhaul this
proc = run(
self.path,
stdout=PIPE,
Expand All @@ -115,9 +172,9 @@ def consume_secrets(self, data: dict):
logger.info(
f'running the process "{self.path}"')

except Exception as error:
except FileNotFoundError as error:
logger.critical(
f'encountered error in {self} executing "{self.path}"')
f'error in {self} executing "{self.path}"')
raise error

print(
Expand Down
4 changes: 3 additions & 1 deletion vaultify/providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,9 @@ class OpenSSLProvider(Provider):
"""
Decrypt and provide secrets from a static file encrypted symmetrically with
OpenSSL.
"""
def __init__(self, secret: str): # nosec
def __init__(self, secret: str):
self.secret = os.environ.get('VAULTIFY_SECRET', secret)
self.popen_kwargs = dict(
bufsize=-1,
Expand All @@ -75,6 +76,7 @@ def __init__(self, secret: str): # nosec

def get_secrets(self):
"""
This implementation uses a preexisting openssl from the host system to
run a command equivalent to:
Expand Down

0 comments on commit 367f0b2

Please sign in to comment.