Skip to content

Commit

Permalink
Added AWS kms support (#105)
Browse files Browse the repository at this point in the history
* Refactor

* Updated conf with encryption

* Added AWS KMS support
  • Loading branch information
avara1986 committed Mar 30, 2020
1 parent 5a5c446 commit 54933a0
Show file tree
Hide file tree
Showing 32 changed files with 533 additions and 85 deletions.
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

0 comments on commit 54933a0

Please sign in to comment.