Skip to content

Commit 54933a0

Browse files
authored
Added AWS kms support (#105)
* Refactor * Updated conf with encryption * Added AWS KMS support
1 parent 5a5c446 commit 54933a0

32 files changed

+533
-85
lines changed

docs/encrypt_decryt_configuration.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
## Configuration
44

5+
# Method 1: Encrypt and decrypt with key file and Fernet
6+
7+
58
When you work in multiple environments: local, dev, testing, production... you must set critical configuration in your
69
variables, like:
710

@@ -70,6 +73,8 @@ pyms encrypt 'mysql+mysqlconnector://important_user:****@localhost/my_schema'
7073
And put this string in your `config_pro.yml`:
7174
```yaml
7275
pyms:
76+
crypt:
77+
method: "fernet"
7378
config:
7479
DEBUG: true
7580
TESTING: true
@@ -98,3 +103,45 @@ SQLALCHEMY_DATABASE_URI: mysql+mysqlconnector://user_of_db:user_of_db@localhost/
98103
```
99104

100105
And you can access to this var with `current_app.config["SQLALCHEMY_DATABASE_URI"]`
106+
107+
# Method 2: Encrypt and decrypt with AWS KMS
108+
109+
## 1. Configure AWS
110+
111+
Pyms knows if a variable is encrypted if this var start with the prefix `enc_` or `ENC_`. PyMS uses boto3 and
112+
aws cli to decrypt this value and store it in the same variable without the `enc_` prefix.
113+
114+
First, configure aws your aws account credentials:
115+
116+
```bash
117+
aws configure
118+
```
119+
120+
## 2. Encrypt with KMS
121+
122+
Cypher a string with this command:
123+
124+
```bash
125+
aws kms encrypt --key-id alias/prueba-avara --plaintext "mysql+mysqlconnector://important_user:****@localhost/my_schema" --query CiphertextBlob --output text
126+
>> AQICAHiALhLQv4eW8jqUccFSnkyDkBAWLAm97Lr2qmdItkUCIAF+P4u/uqzu8KRT74PsnQXhAAAAoDCBnQYJKoZIhvcNAQcGoIGPMIGMAgEAMIGGBgkqhkiG9w0BBwEwHgYJYIZIAWUDBAEuMBEEDPo+k3ZxoI9XVKtHgQIBEIBZmp7UUVjNWd6qKrLVK8oBNczY0CfLH6iAZE3UK5Ofs4+nZFi0PL3SEW8M15VgTpQoC/b0YxDPHjF0V6NHUJcWirSAqKkP5Sz5eSTk91FTuiwDpvYQ2q9aY6w=
127+
128+
```
129+
130+
## 3. Decrypt from your config file
131+
132+
And put this string in your `config_pro.yml`:
133+
```yaml
134+
pyms:
135+
crypt:
136+
method: "aws_kms"
137+
key_id: "alias/your-kms-key"
138+
config:
139+
DEBUG: true
140+
TESTING: true
141+
APPLICATION_ROOT : ""
142+
SECRET_KEY: "gjr39dkjn344_!67#"
143+
ENC_SQLALCHEMY_DATABASE_URI: "AQICAHiALhLQv4eW8jqUccFSnkyDkBAWLAm97Lr2qmdItkUCIAF+P4u/uqzu8KRT74PsnQXhAAAAoDCBnQYJKoZIhvcNAQcGoIGPMIGMAgEAMIGGBgkqhkiG9w0BBwEwHgYJYIZIAWUDBAEuMBEEDPo+k3ZxoI9XVKtHgQIBEIBZmp7UUVjNWd6qKrLVK8oBNczY0CfLH6iAZE3UK5Ofs4+nZFi0PL3SEW8M15VgTpQoC/b0YxDPHjF0V6NHUJcWirSAqKkP5Sz5eSTk91FTuiwDpvYQ2q9aY6w=
144+
"
145+
```
146+
147+

examples/microservice_crypt_aws_kms/__init__.py

Whitespace-only changes.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
pyms:
2+
crypt:
3+
method: "aws_kms"
4+
key_id: "alias/prueba-avara"
5+
config:
6+
DEBUG: true
7+
TESTING: false
8+
SWAGGER: true
9+
APP_NAME: business-glossary
10+
APPLICATION_ROOT : ""
11+
SECRET_KEY: "gjr39dkjn344_!67#"
12+
enc_encrypted_key: "AQICAHiALhLQv4eW8jqUccFSnkyDkBAWLAm97Lr2qmdItkUCIAEVoPzSHLW+If9sxSRJ420jAAAAoDCBnQYJKoZIhvcNAQcGoIGPMIGMAgEAMIGGBgkqhkiG9w0BBwEwHgYJYIZIAWUDBAEuMBEEDHoNko2L0A0m/r/h9QIBEIBZPsxFUeHFQzEacdLde5eeJRTHw8e0eSwG7UkJzc+ZdBp1xS9DyqBsHQw4Xnx58iQxCgH6ivRKOraZGKX5ebIZUrw/d+XD8YmbdCosx/TwnHVLneehSbWjF1c="
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from base64 import b64decode
2+
3+
from flask import jsonify
4+
5+
from pyms.flask.app import Microservice
6+
7+
ms = Microservice()
8+
app = ms.create_app()
9+
10+
11+
@app.route("/")
12+
def example():
13+
return jsonify({"main": app.ms.config.encrypted_key})
14+
15+
16+
if __name__ == '__main__':
17+
app.run()

examples/mininum_microservice/main.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from pyms.flask.app import Microservice
44

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

88

pyms/cloud/aws/kms.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import base64
2+
3+
from pyms.crypt.driver import CryptAbstract
4+
from pyms.utils import check_package_exists, import_package
5+
6+
7+
class Crypt(CryptAbstract):
8+
encryption_algorithm = "SYMMETRIC_DEFAULT" # 'SYMMETRIC_DEFAULT' | 'RSAES_OAEP_SHA_1' | 'RSAES_OAEP_SHA_256'
9+
key_id = ""
10+
11+
def __init__(self, *args, **kwargs):
12+
self._init_boto()
13+
super().__init__(*args, **kwargs)
14+
15+
def encrypt(self, message): # pragma: no cover
16+
ciphertext = self.client.encrypt(
17+
KeyId=self.config.key_id,
18+
Plaintext=bytes(message, encoding="UTF-8"),
19+
)
20+
return str(base64.b64encode(ciphertext["CiphertextBlob"]), encoding="UTF-8")
21+
22+
def _init_boto(self): # pragma: no cover
23+
check_package_exists("boto3")
24+
boto3 = import_package("boto3")
25+
boto3.set_stream_logger(name='botocore')
26+
self.client = boto3.client('kms')
27+
28+
def _aws_decrypt(self, blob_text): # pragma: no cover
29+
response = self.client.decrypt(
30+
CiphertextBlob=blob_text,
31+
KeyId=self.config.key_id,
32+
EncryptionAlgorithm=self.encryption_algorithm
33+
)
34+
return str(response['Plaintext'], encoding="UTF-8")
35+
36+
def _parse_encrypted(self, encrypted):
37+
blob_text = base64.b64decode(encrypted)
38+
return blob_text
39+
40+
def decrypt(self, encrypted):
41+
blob_text = self._parse_encrypted(encrypted)
42+
decrypted = self._aws_decrypt(blob_text)
43+
44+
return decrypted

pyms/cmd/main.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import sys
77

88
from pyms.utils import check_package_exists, import_from
9-
from pyms.utils.crypt import Crypt
9+
from pyms.crypt.fernet import Crypt
1010

1111

1212
class Command:

pyms/config/confile.py

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77

88
from pyms.constants import CONFIGMAP_FILE_ENVIRONMENT, LOGGER_NAME, DEFAULT_CONFIGMAP_FILENAME
99
from pyms.exceptions import AttrDoesNotExistException, ConfigDoesNotFoundException
10-
from pyms.utils.crypt import Crypt
1110
from pyms.utils.files import LoadFile
1211

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

2626
def __init__(self, *args, **kwargs):
2727
"""
@@ -35,7 +35,9 @@ def __init__(self, *args, **kwargs):
3535
```
3636
"""
3737
self._loader = LoadFile(kwargs.get("path"), CONFIGMAP_FILE_ENVIRONMENT, DEFAULT_CONFIGMAP_FILENAME)
38-
self._crypt = Crypt(path=kwargs.get("path"))
38+
self._crypt_cls = kwargs.get("crypt")
39+
if self._crypt_cls:
40+
self._crypt = self._crypt_cls(path=kwargs.get("path"))
3941
self._empty_init = kwargs.get("empty_init", False)
4042
config = kwargs.get("config")
4143
if config is None:
@@ -52,7 +54,7 @@ def __init__(self, *args, **kwargs):
5254
super(ConfFile, self).__init__(config)
5355

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

5759
def set_config(self, config: Dict) -> Dict:
5860
"""
@@ -63,10 +65,14 @@ def set_config(self, config: Dict) -> Dict:
6365
"""
6466
config = dict(self.normalize_config(config))
6567
pop_encripted_keys = []
68+
add_decripted_keys = []
6669
for k, v in config.items():
6770
if k.lower().startswith("enc_"):
6871
k_not_crypt = re.compile(re.escape('enc_'), re.IGNORECASE)
69-
setattr(self, k_not_crypt.sub('', k), self._crypt.decrypt(v))
72+
decrypted_key = k_not_crypt.sub('', k)
73+
decrypted_value = self._crypt.decrypt(v) if self._crypt else None
74+
setattr(self, decrypted_key, decrypted_value)
75+
add_decripted_keys.append((decrypted_key, decrypted_value))
7076
pop_encripted_keys.append(k)
7177
else:
7278
setattr(self, k, v)
@@ -75,12 +81,15 @@ def set_config(self, config: Dict) -> Dict:
7581
for x in pop_encripted_keys:
7682
config.pop(x)
7783

84+
for k, v in add_decripted_keys:
85+
config[k] = v
86+
7887
return config
7988

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

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

109118
def reload(self):

pyms/config/resource.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from pyms.config import get_conf
2+
3+
4+
class ConfigResource:
5+
6+
config_resource = None
7+
8+
def __init__(self, *args, **kwargs):
9+
self.config = get_conf(service=self.config_resource, empty_init=True, uppercase=False, *args, **kwargs)

pyms/constants.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,6 @@
1212

1313
SERVICE_BASE = "pyms.services"
1414

15-
PYMS_CONFIG_WHITELIST_KEYWORDS = ["config", "services"]
15+
CRYPT_BASE = "pyms.crypt"
16+
17+
PYMS_CONFIG_WHITELIST_KEYWORDS = ["config", "services", "crypt"]

0 commit comments

Comments
 (0)