From 1278e71342ffd528a12be6f2813d88abd063a80f Mon Sep 17 00:00:00 2001 From: David Challoner Date: Sat, 31 Jan 2015 23:03:26 -0800 Subject: [PATCH 1/5] add support for encrypted environmnental variables Signed-off-by: David Challoner --- compose/cli/command.py | 16 ++++++++++++++-- compose/cli/main.py | 18 ++++++++++++++++++ requirements.txt | 2 ++ 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/compose/cli/command.py b/compose/cli/command.py index 67b77f31b57..ce46a2ce02e 100644 --- a/compose/cli/command.py +++ b/compose/cli/command.py @@ -6,7 +6,9 @@ import re import yaml import six +import base64 +from simplecrypt import decrypt from ..project import Project from ..service import ConfigError from .docopt_command import DocoptCommand @@ -41,7 +43,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 @@ -72,7 +74,17 @@ def get_client(self, verbose=False): def get_config(self, config_path): try: with open(config_path, 'r') as fh: - return yaml.safe_load(fh) + config = yaml.safe_load(fh) + for project in config: + if 'environment' in config[project]: + for key,var in config[project]['environment'].items(): + if 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.") + config[project]['environment'][key] = decrypt(secret, base64.urlsafe_b64decode(var.replace('encrypted:','').encode('utf8'))) + return config + except IOError as e: raise errors.UserError(six.text_type(e)) diff --git a/compose/cli/main.py b/compose/cli/main.py index cfe29cd077d..44880dcf322 100644 --- a/compose/cli/main.py +++ b/compose/cli/main.py @@ -4,6 +4,9 @@ import sys import re import signal +import os +from simplecrypt import encrypt as enc +import base64 from operator import attrgetter from inspect import getdoc @@ -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 @@ -118,12 +122,26 @@ 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. Usage: help COMMAND """ + import ipdb; ipdb.set_trace() command = options['COMMAND'] if not hasattr(self, command): raise NoSuchCommand(command, self) diff --git a/requirements.txt b/requirements.txt index a31a19ae9c6..ed2b19a7b4a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,3 +6,5 @@ requests==2.2.1 six==1.7.3 texttable==0.8.1 websocket-client==0.11.0 +simple-crypt==4.0.0 +pycrypto==2.6.1 From fbb6d465932fb54a8e8420430d7c15e88d38e42c Mon Sep 17 00:00:00 2001 From: David Challoner Date: Sun, 1 Feb 2015 00:17:17 -0800 Subject: [PATCH 2/5] added tests Signed-off-by: David Challoner --- compose/cli/main.py | 1 - .../docker-compose.yml | 6 +++++ tests/integration/cli_test.py | 25 +++++++++++++++++++ 3 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 tests/fixtures/encrypted-environment-composefile/docker-compose.yml diff --git a/compose/cli/main.py b/compose/cli/main.py index 44880dcf322..cc99f31ab8e 100644 --- a/compose/cli/main.py +++ b/compose/cli/main.py @@ -141,7 +141,6 @@ def help(self, project, options): Usage: help COMMAND """ - import ipdb; ipdb.set_trace() command = options['COMMAND'] if not hasattr(self, command): raise NoSuchCommand(command, self) diff --git a/tests/fixtures/encrypted-environment-composefile/docker-compose.yml b/tests/fixtures/encrypted-environment-composefile/docker-compose.yml new file mode 100644 index 00000000000..cd4493483ec --- /dev/null +++ b/tests/fixtures/encrypted-environment-composefile/docker-compose.yml @@ -0,0 +1,6 @@ +service: + image: busybox:latest + command: sleep 5 + + environment: + encrypted_foo: encrypted:c2MAAqVVdUbUUFfIpxi60K5CscqSH_2x0g4NqNpvIE9vwf8NmAaThh55ZFzd1F8TQbe5BFKSow7-l0EasOLPHt9FhtbJGmVJHOfYO1JnjGqYeld40Fqv9Y_ZhLG8gEMGCb9EBt2uhBQYP_vXQ782ZJ-iLnIEaRgCcnTjKJwq4ZN_NaGMqP3K1yY= diff --git a/tests/integration/cli_test.py b/tests/integration/cli_test.py index cf939837929..bdee5d84b4f 100644 --- a/tests/integration/cli_test.py +++ b/tests/integration/cli_test.py @@ -1,5 +1,6 @@ from __future__ import absolute_import import sys +import os from six import StringIO from mock import patch @@ -42,6 +43,30 @@ 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('dockerpty.start') + def test_encrypt_var_gets_encrypted(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] + # env overriden + self.assertEqual('Any sufficiently advanced technology is indistinguishable from magic.', container.environment['encrypted_foo']) + @patch('sys.stdout', new_callable=StringIO) def test_ps_default_composefile(self, mock_stdout): self.command.base_dir = 'tests/fixtures/multiple-composefiles' From 1e9bc55d08b8125bd55f262234d9d31f8e96d7f9 Mon Sep 17 00:00:00 2001 From: David Challoner Date: Sun, 1 Feb 2015 02:04:19 -0800 Subject: [PATCH 3/5] Adding decryption error case and tests. Fixing flake problems. Signed-off-by: David Challoner --- compose/cli/command.py | 11 +++++++--- compose/cli/main.py | 1 - tests/integration/cli_test.py | 40 ++++++++++++++++++++++------------- 3 files changed, 33 insertions(+), 19 deletions(-) diff --git a/compose/cli/command.py b/compose/cli/command.py index ce46a2ce02e..687b934a7df 100644 --- a/compose/cli/command.py +++ b/compose/cli/command.py @@ -9,6 +9,7 @@ import base64 from simplecrypt import decrypt +from simplecrypt import DecryptionException from ..project import Project from ..service import ConfigError from .docopt_command import DocoptCommand @@ -77,12 +78,16 @@ def get_config(self, config_path): config = yaml.safe_load(fh) for project in config: if 'environment' in config[project]: - for key,var in config[project]['environment'].items(): + for key, var in config[project]['environment'].items(): if var.startswith('encrypted:'): - secret=os.environ.get('FIG_CRYPT_KEY') + 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.") - config[project]['environment'][key] = decrypt(secret, base64.urlsafe_b64decode(var.replace('encrypted:','').encode('utf8'))) + 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 except IOError as e: diff --git a/compose/cli/main.py b/compose/cli/main.py index cc99f31ab8e..0d808e9ce96 100644 --- a/compose/cli/main.py +++ b/compose/cli/main.py @@ -134,7 +134,6 @@ def encrypt(self, project, options): 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. diff --git a/tests/integration/cli_test.py b/tests/integration/cli_test.py index bdee5d84b4f..ae2a766cb8e 100644 --- a/tests/integration/cli_test.py +++ b/tests/integration/cli_test.py @@ -46,26 +46,14 @@ def test_ps(self, mock_stdout): @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' + 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.' + 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('dockerpty.start') - def test_encrypt_var_gets_encrypted(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] - # env overriden - self.assertEqual('Any sufficiently advanced technology is indistinguishable from magic.', container.environment['encrypted_foo']) + self.assertIn('encrypted:', mock_stdout.getvalue()) @patch('sys.stdout', new_callable=StringIO) def test_ps_default_composefile(self, mock_stdout): @@ -313,6 +301,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:9999") + @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() From 62826350b72260e7ab41715dfed6e59031603b51 Mon Sep 17 00:00:00 2001 From: David Challoner Date: Sat, 28 Feb 2015 17:25:01 -0800 Subject: [PATCH 4/5] forgot to add simple crypt --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 9dfe9321267..7aca4fe962e 100644 --- a/setup.py +++ b/setup.py @@ -33,6 +33,7 @@ def find_version(*file_paths): 'docker-py >= 0.6.0, < 0.8', 'dockerpty >= 0.3.2, < 0.4', 'six >= 1.3.0, < 2', + 'simple-crypt == 4.0.0' ] tests_require = [ From 861e1edabe42c182d697ea2ae30ddb2cd798207a Mon Sep 17 00:00:00 2001 From: David Challoner Date: Thu, 14 May 2015 16:47:16 -0700 Subject: [PATCH 5/5] fixing allen bug --- compose/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compose/config.py b/compose/config.py index ff86f574d96..3e00e380a50 100644 --- a/compose/config.py +++ b/compose/config.py @@ -61,7 +61,7 @@ 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 var.startswith('encrypted:'): + 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.")