Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for encrypted environment variables #909

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions compose/cli/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import os
import re
import six

from .. import config
from ..project import Project
from ..service import ConfigError
Expand Down Expand Up @@ -41,7 +40,7 @@ def dispatch(self, *args, **kwargs):
raise errors.ConnectionErrorGeneric(self.get_client().base_url)

def perform_command(self, options, handler, command_options):
if options['COMMAND'] == 'help':
if options['COMMAND'] == 'help' or options['COMMAND'] == 'encrypt':
# Skip looking up the compose file.
handler(None, command_options)
return
Expand Down
20 changes: 18 additions & 2 deletions compose/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@
from __future__ import unicode_literals
from inspect import getdoc
from operator import attrgetter
from simplecrypt import encrypt as enc
from operator import attrgetter
from docker.errors import APIError
import logging
import re
import signal
import os
import base64
import sys

from docker.errors import APIError
import dockerpty

from .. import __version__
Expand Down Expand Up @@ -82,6 +85,7 @@ class TopLevelCommand(Command):

Commands:
build Build or rebuild services
encrypt Helper to encrypt environmental variables
help Get help on a command
kill Kill containers
logs View output from containers
Expand Down Expand Up @@ -118,6 +122,18 @@ def build(self, project, options):
no_cache = bool(options.get('--no-cache', False))
project.build(service_names=options['SERVICE'], no_cache=no_cache)

def encrypt(self, project, options):
"""
Helper to encrypt a string with the current FIG_CRYPT_KEY environment variable.

Usage: encrypt STRING
"""
string = options['STRING']
if os.environ.get('FIG_CRYPT_KEY') is None:
raise SystemExit("You must set 'FIG_CRYPT_KEY in your environment")
print("Use the following as your key's value in your yml configuration(this may take a moment):")
print("encrypted:%s" % base64.urlsafe_b64encode(enc(os.environ.get('FIG_CRYPT_KEY'), string)))

def help(self, project, options):
"""
Get help on a command.
Expand Down
24 changes: 23 additions & 1 deletion compose/config.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
from simplecrypt import decrypt
from simplecrypt import DecryptionException
import logging
import base64
import os
import yaml
import six
Expand Down Expand Up @@ -50,10 +54,28 @@
'workdir': 'working_dir',
}

log = logging.getLogger(__name__)


def decrypt_config(config):
for project in config:
if 'environment' in config[project] and hasattr(config[project]['environment'], 'items'):
for key, var in config[project]['environment'].items():
if str(var).startswith('encrypted:'):
secret = os.environ.get('FIG_CRYPT_KEY')
if secret is None:
raise SystemExit("Your yml configuration has encrypted environmental variables but you haven't set 'FIG_CRYPT_KEY in your environment. Please set it and try again.")
try:
config[project]['environment'][key] = decrypt(secret, base64.urlsafe_b64decode(var.replace('encrypted:', '').encode('utf8')))
except DecryptionException:
log.fatal("Decryption Error: We couldn't decrypt the environmental variable %s in your yml config with the given FIG_CRYPT_KEY. The value has been set to BAD_DECRYPT." % key)
config[project]['environment'][key] = "BAD_DECRYPT"
return config


def load(filename):
working_dir = os.path.dirname(filename)
return from_dictionary(load_yaml(filename), working_dir=working_dir, filename=filename)
return from_dictionary(decrypt_config(load_yaml(filename)), working_dir=working_dir, filename=filename)


def from_dictionary(dictionary, working_dir=None, filename=None):
Expand Down
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@ requests==2.2.1
six==1.7.3
texttable==0.8.2
websocket-client==0.11.0
simple-crypt==4.0.0
pycrypto==2.6.1
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ def find_version(*file_paths):
'docker-py >= 1.0.0, < 1.2',
'dockerpty >= 0.3.2, < 0.4',
'six >= 1.3.0, < 2',
'simple-crypt == 4.0.0'
]


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
service:
image: busybox:latest
command: sleep 5

environment:
encrypted_foo: encrypted:c2MAAqVVdUbUUFfIpxi60K5CscqSH_2x0g4NqNpvIE9vwf8NmAaThh55ZFzd1F8TQbe5BFKSow7-l0EasOLPHt9FhtbJGmVJHOfYO1JnjGqYeld40Fqv9Y_ZhLG8gEMGCb9EBt2uhBQYP_vXQ782ZJ-iLnIEaRgCcnTjKJwq4ZN_NaGMqP3K1yY=
34 changes: 34 additions & 0 deletions tests/integration/cli_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,18 @@ def test_ps(self, mock_stdout):
self.command.dispatch(['ps'], None)
self.assertIn('simplecomposefile_simple_1', mock_stdout.getvalue())

@patch('sys.stdout', new_callable=StringIO)
def test_encrypt(self, mock_stdout):
self.project.get_service('simple').create_container()
secret = 'this is only a test'
with self.assertRaises(SystemExit) as exc_context:
self.command.dispatch(['encrypt', secret], None)
self.assertIn('You must set', str(exc_context.exception))
testphrase = 'Any sufficiently advanced technology is indistinguishable from magic.'
os.environ['FIG_CRYPT_KEY'] = secret
self.command.dispatch(['encrypt', testphrase], None)
self.assertIn('encrypted:', mock_stdout.getvalue())

@patch('sys.stdout', new_callable=StringIO)
def test_ps_default_composefile(self, mock_stdout):
self.command.base_dir = 'tests/fixtures/multiple-composefiles'
Expand Down Expand Up @@ -318,6 +330,28 @@ def test_run_service_with_map_ports(self, __):
self.assertIn("0.0.0.0", port_random)
self.assertEqual(port_assigned, "0.0.0.0:49152")

@patch('dockerpty.start')
def test_run_with_encrypted_var(self, _):
self.command.base_dir = 'tests/fixtures/encrypted-environment-composefile'
secret = 'this is only a test'
os.environ['FIG_CRYPT_KEY'] = secret
name = 'service'
self.command.dispatch(['run', name, 'env'], None)
service = self.project.get_service(name)
container = service.containers(stopped=True, one_off=True)[0]
self.assertEqual('Any sufficiently advanced technology is indistinguishable from magic.', container.environment['encrypted_foo'])

@patch('dockerpty.start')
def test_run_bad_decrypt(self, _):
self.command.base_dir = 'tests/fixtures/encrypted-environment-composefile'
secret = 'this is a bad key'
os.environ['FIG_CRYPT_KEY'] = secret
name = 'service'
self.command.dispatch(['run', name, 'env'], None)
service = self.project.get_service(name)
container = service.containers(stopped=True, one_off=True)[0]
self.assertEqual('BAD_DECRYPT', container.environment['encrypted_foo'])

def test_rm(self):
service = self.project.get_service('simple')
service.create_container()
Expand Down