Skip to content

Added AWS kms support #105

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

Merged
merged 18 commits into from
Mar 30, 2020
Merged
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
47 changes: 47 additions & 0 deletions docs/encrypt_decryt_configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## Configuration

# Method 1: Encrypt and decrypt with key file and Fernet


When you work in multiple environments: local, dev, testing, production... you must set critical configuration in your
variables, like:

Expand Down Expand Up @@ -70,6 +73,8 @@ pyms encrypt 'mysql+mysqlconnector://important_user:****@localhost/my_schema'
And put this string in your `config_pro.yml`:
```yaml
pyms:
crypt:
method: "fernet"
config:
DEBUG: true
TESTING: true
Expand Down Expand Up @@ -98,3 +103,45 @@ SQLALCHEMY_DATABASE_URI: mysql+mysqlconnector://user_of_db:user_of_db@localhost/
```

And you can access to this var with `current_app.config["SQLALCHEMY_DATABASE_URI"]`

# Method 2: Encrypt and decrypt with AWS KMS

## 1. Configure AWS

Pyms knows if a variable is encrypted if this var start with the prefix `enc_` or `ENC_`. PyMS uses boto3 and
aws cli to decrypt this value and store it in the same variable without the `enc_` prefix.

First, configure aws your aws account credentials:

```bash
aws configure
```

## 2. Encrypt with KMS

Cypher a string with this command:

```bash
aws kms encrypt --key-id alias/prueba-avara --plaintext "mysql+mysqlconnector://important_user:****@localhost/my_schema" --query CiphertextBlob --output text
>> AQICAHiALhLQv4eW8jqUccFSnkyDkBAWLAm97Lr2qmdItkUCIAF+P4u/uqzu8KRT74PsnQXhAAAAoDCBnQYJKoZIhvcNAQcGoIGPMIGMAgEAMIGGBgkqhkiG9w0BBwEwHgYJYIZIAWUDBAEuMBEEDPo+k3ZxoI9XVKtHgQIBEIBZmp7UUVjNWd6qKrLVK8oBNczY0CfLH6iAZE3UK5Ofs4+nZFi0PL3SEW8M15VgTpQoC/b0YxDPHjF0V6NHUJcWirSAqKkP5Sz5eSTk91FTuiwDpvYQ2q9aY6w=

```

## 3. Decrypt from your config file

And put this string in your `config_pro.yml`:
```yaml
pyms:
crypt:
method: "aws_kms"
key_id: "alias/your-kms-key"
config:
DEBUG: true
TESTING: true
APPLICATION_ROOT : ""
SECRET_KEY: "gjr39dkjn344_!67#"
ENC_SQLALCHEMY_DATABASE_URI: "AQICAHiALhLQv4eW8jqUccFSnkyDkBAWLAm97Lr2qmdItkUCIAF+P4u/uqzu8KRT74PsnQXhAAAAoDCBnQYJKoZIhvcNAQcGoIGPMIGMAgEAMIGGBgkqhkiG9w0BBwEwHgYJYIZIAWUDBAEuMBEEDPo+k3ZxoI9XVKtHgQIBEIBZmp7UUVjNWd6qKrLVK8oBNczY0CfLH6iAZE3UK5Ofs4+nZFi0PL3SEW8M15VgTpQoC/b0YxDPHjF0V6NHUJcWirSAqKkP5Sz5eSTk91FTuiwDpvYQ2q9aY6w=
"
```


Empty file.
12 changes: 12 additions & 0 deletions examples/microservice_crypt_aws_kms/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
pyms:
crypt:
method: "aws_kms"
key_id: "alias/prueba-avara"
config:
DEBUG: true
TESTING: false
SWAGGER: true
APP_NAME: business-glossary
APPLICATION_ROOT : ""
SECRET_KEY: "gjr39dkjn344_!67#"
enc_encrypted_key: "AQICAHiALhLQv4eW8jqUccFSnkyDkBAWLAm97Lr2qmdItkUCIAEVoPzSHLW+If9sxSRJ420jAAAAoDCBnQYJKoZIhvcNAQcGoIGPMIGMAgEAMIGGBgkqhkiG9w0BBwEwHgYJYIZIAWUDBAEuMBEEDHoNko2L0A0m/r/h9QIBEIBZPsxFUeHFQzEacdLde5eeJRTHw8e0eSwG7UkJzc+ZdBp1xS9DyqBsHQw4Xnx58iQxCgH6ivRKOraZGKX5ebIZUrw/d+XD8YmbdCosx/TwnHVLneehSbWjF1c="
17 changes: 17 additions & 0 deletions examples/microservice_crypt_aws_kms/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from base64 import b64decode

from flask import jsonify

from pyms.flask.app import Microservice

ms = Microservice()
app = ms.create_app()


@app.route("/")
def example():
return jsonify({"main": app.ms.config.encrypted_key})


if __name__ == '__main__':
app.run()
2 changes: 1 addition & 1 deletion examples/mininum_microservice/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from pyms.flask.app import Microservice

ms = Microservice(service="my-minimal-microservice", path=__file__)
ms = Microservice(path=__file__)
app = ms.create_app()


Expand Down
44 changes: 44 additions & 0 deletions pyms/cloud/aws/kms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import base64

from pyms.crypt.driver import CryptAbstract
from pyms.utils import check_package_exists, import_package


class Crypt(CryptAbstract):
encryption_algorithm = "SYMMETRIC_DEFAULT" # 'SYMMETRIC_DEFAULT' | 'RSAES_OAEP_SHA_1' | 'RSAES_OAEP_SHA_256'
key_id = ""

def __init__(self, *args, **kwargs):
self._init_boto()
super().__init__(*args, **kwargs)

def encrypt(self, message): # pragma: no cover
ciphertext = self.client.encrypt(
KeyId=self.config.key_id,
Plaintext=bytes(message, encoding="UTF-8"),
)
return str(base64.b64encode(ciphertext["CiphertextBlob"]), encoding="UTF-8")

def _init_boto(self): # pragma: no cover
check_package_exists("boto3")
boto3 = import_package("boto3")
boto3.set_stream_logger(name='botocore')
self.client = boto3.client('kms')

def _aws_decrypt(self, blob_text): # pragma: no cover
response = self.client.decrypt(
CiphertextBlob=blob_text,
KeyId=self.config.key_id,
EncryptionAlgorithm=self.encryption_algorithm
)
return str(response['Plaintext'], encoding="UTF-8")

def _parse_encrypted(self, encrypted):
blob_text = base64.b64decode(encrypted)
return blob_text

def decrypt(self, encrypted):
blob_text = self._parse_encrypted(encrypted)
decrypted = self._aws_decrypt(blob_text)

return decrypted
2 changes: 1 addition & 1 deletion pyms/cmd/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import sys

from pyms.utils import check_package_exists, import_from
from pyms.utils.crypt import Crypt
from pyms.crypt.fernet import Crypt


class Command:
Expand Down
21 changes: 15 additions & 6 deletions pyms/config/confile.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@

from pyms.constants import CONFIGMAP_FILE_ENVIRONMENT, LOGGER_NAME, DEFAULT_CONFIGMAP_FILENAME
from pyms.exceptions import AttrDoesNotExistException, ConfigDoesNotFoundException
from pyms.utils.crypt import Crypt
from pyms.utils.files import LoadFile

logger = logging.getLogger(LOGGER_NAME)
Expand All @@ -22,6 +21,7 @@ class ConfFile(dict):
* config: Allow to pass a dictionary to ConfFile without use a file
"""
_empty_init = False
_crypt = None

def __init__(self, *args, **kwargs):
"""
Expand All @@ -35,7 +35,9 @@ def __init__(self, *args, **kwargs):
```
"""
self._loader = LoadFile(kwargs.get("path"), CONFIGMAP_FILE_ENVIRONMENT, DEFAULT_CONFIGMAP_FILENAME)
self._crypt = Crypt(path=kwargs.get("path"))
self._crypt_cls = kwargs.get("crypt")
if self._crypt_cls:
self._crypt = self._crypt_cls(path=kwargs.get("path"))
self._empty_init = kwargs.get("empty_init", False)
config = kwargs.get("config")
if config is None:
Expand All @@ -52,7 +54,7 @@ def __init__(self, *args, **kwargs):
super(ConfFile, self).__init__(config)

def to_flask(self) -> Dict:
return ConfFile(config={k.upper(): v for k, v in self.items()})
return ConfFile(config={k.upper(): v for k, v in self.items()}, crypt=self._crypt_cls)

def set_config(self, config: Dict) -> Dict:
"""
Expand All @@ -63,10 +65,14 @@ def set_config(self, config: Dict) -> Dict:
"""
config = dict(self.normalize_config(config))
pop_encripted_keys = []
add_decripted_keys = []
for k, v in config.items():
if k.lower().startswith("enc_"):
k_not_crypt = re.compile(re.escape('enc_'), re.IGNORECASE)
setattr(self, k_not_crypt.sub('', k), self._crypt.decrypt(v))
decrypted_key = k_not_crypt.sub('', k)
decrypted_value = self._crypt.decrypt(v) if self._crypt else None
setattr(self, decrypted_key, decrypted_value)
add_decripted_keys.append((decrypted_key, decrypted_value))
pop_encripted_keys.append(k)
else:
setattr(self, k, v)
Expand All @@ -75,12 +81,15 @@ def set_config(self, config: Dict) -> Dict:
for x in pop_encripted_keys:
config.pop(x)

for k, v in add_decripted_keys:
config[k] = v

return config

def normalize_config(self, config: Dict) -> Iterable[Tuple[Text, Union[Dict, Text, bool]]]:
for key, item in config.items():
if isinstance(item, dict):
item = ConfFile(config=item, empty_init=self._empty_init)
item = ConfFile(config=item, empty_init=self._empty_init, crypt=self._crypt_cls)
yield self.normalize_keys(key), item

@staticmethod
Expand All @@ -103,7 +112,7 @@ def __getattr__(self, name, *args, **kwargs):
return aux_dict
except KeyError:
if self._empty_init:
return ConfFile(config={}, empty_init=self._empty_init)
return ConfFile(config={}, empty_init=self._empty_init, crypt=self._crypt_cls)
raise AttrDoesNotExistException("Variable {} not exist in the config file".format(name))

def reload(self):
Expand Down
9 changes: 9 additions & 0 deletions pyms/config/resource.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from pyms.config import get_conf


class ConfigResource:

config_resource = None

def __init__(self, *args, **kwargs):
self.config = get_conf(service=self.config_resource, empty_init=True, uppercase=False, *args, **kwargs)
4 changes: 3 additions & 1 deletion pyms/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@

SERVICE_BASE = "pyms.services"

PYMS_CONFIG_WHITELIST_KEYWORDS = ["config", "services"]
CRYPT_BASE = "pyms.crypt"

PYMS_CONFIG_WHITELIST_KEYWORDS = ["config", "services", "crypt"]
Empty file added pyms/crypt/__init__.py
Empty file.
53 changes: 53 additions & 0 deletions pyms/crypt/driver.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import logging
from abc import ABC, abstractmethod

from pyms.config.resource import ConfigResource
from pyms.constants import CRYPT_BASE, LOGGER_NAME
from pyms.utils import import_from

logger = logging.getLogger(LOGGER_NAME)

CRYPT_RESOURCES_CLASS = "Crypt"


class CryptAbstract(ABC):

def __init__(self, *args, **kwargs):
self.config = kwargs.get("config")

@abstractmethod
def encrypt(self, message):
raise NotImplementedError

@abstractmethod
def decrypt(self, encrypted):
raise NotImplementedError


class CryptNone(CryptAbstract):

def encrypt(self, message):
return message

def decrypt(self, encrypted):
return encrypted


class CryptResource(ConfigResource):
"""This class works between `pyms.flask.create_app.Microservice` and `pyms.flask.services.[THESERVICE]`. Search
for a file with the name you want to load, set the configuration and return a instance of the class you want
"""
config_resource = CRYPT_BASE

def get_crypt(self, *args, **kwargs) -> CryptAbstract:
if self.config.method == "fernet":
crypt_object = import_from("pyms.crypt.fernet", CRYPT_RESOURCES_CLASS)
elif self.config.method == "aws_kms":
crypt_object = import_from("pyms.cloud.aws.kms", CRYPT_RESOURCES_CLASS)
else:
crypt_object = CryptNone
logger.debug("Init crypt {}".format(crypt_object))
return crypt_object(config=self.config, *args, **kwargs)

def __call__(self, *args, **kwargs):
return self.get_crypt(*args, **kwargs)
4 changes: 3 additions & 1 deletion pyms/utils/crypt.py → pyms/crypt/fernet.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC

from pyms.constants import CRYPT_FILE_KEY_ENVIRONMENT, DEFAULT_KEY_FILENAME
from pyms.crypt.driver import CryptAbstract
from pyms.exceptions import FileDoesNotExistException
from pyms.utils.files import LoadFile


class Crypt:
class Crypt(CryptAbstract):
def __init__(self, *args, **kwargs):
self._loader = LoadFile(kwargs.get("path"), CRYPT_FILE_KEY_ENVIRONMENT, DEFAULT_KEY_FILENAME)
super().__init__(*args, **kwargs)

def generate_key(self, password: Text, write_to_file: bool = False):
password = password.encode() # Convert to type bytes
Expand Down
Loading