From 1554c55ed6e83118a6d1c4917bb21a0ade85aa46 Mon Sep 17 00:00:00 2001 From: tdviet Date: Tue, 21 Jun 2022 18:49:23 +0200 Subject: [PATCH 1/5] Add `fedcloud secret put` --- fedcloudclient/secret.py | 43 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/fedcloudclient/secret.py b/fedcloudclient/secret.py index c0e6fff..bd410ad 100644 --- a/fedcloudclient/secret.py +++ b/fedcloudclient/secret.py @@ -34,7 +34,7 @@ def secret_client(access_token, command, path, data): "read_secret": client.secrets.kv.v1.read_secret, "delete_secret": client.secrets.kv.v1.read_secret, } - if command == "set": + if command == "put": response = client.secrets.kv.v1.create_or_update_secret( path=full_path, mount_point=VAULT_MOUNT_POINT, @@ -45,6 +45,31 @@ def secret_client(access_token, command, path, data): return response +def secret_params_to_dict(params): + """ + Convert secret params "key=value" to dict {"key":"value"} + :param params: input string in format "key=value" + :return: dict {"key":"value"} + """ + + result = {} + + if len(params) == 0: + raise click.UsageError( + f"Expecting 'key=value' arguments for secrets, None provided" + ) + + for param in params: + try: + key, value = param.split("=", 1) + except ValueError: + raise click.UsageError( + f"Expecting 'key=value' arguments for secrets. '{param}' provided." + ) + result[key] = value + return result + + @click.group() def secret(): """ @@ -80,3 +105,19 @@ def list_( data = secret_client(access_token, "list_secrets", short_path, None) print("\n".join(map(str, data["data"]["keys"]))) + + +@secret.command() +@oidc_params +@click.argument("short_path") +@click.argument("secrets", nargs=-1, metavar="[key=value...]") +def put( + access_token, + short_path, + secrets +): + """ + Put secrets to the path + """ + secret_dict = secret_params_to_dict(secrets) + secret_client(access_token, "put", short_path, secret_dict) From fddafb10e02133e8baa8d915f29094d98798da12 Mon Sep 17 00:00:00 2001 From: tdviet Date: Tue, 21 Jun 2022 18:56:29 +0200 Subject: [PATCH 2/5] fix linting --- fedcloudclient/secret.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fedcloudclient/secret.py b/fedcloudclient/secret.py index bd410ad..55940e3 100644 --- a/fedcloudclient/secret.py +++ b/fedcloudclient/secret.py @@ -114,7 +114,7 @@ def list_( def put( access_token, short_path, - secrets + secrets, ): """ Put secrets to the path From 2a41d5221fa26e4efbecdc6d4e1a771e74caff55 Mon Sep 17 00:00:00 2001 From: tdviet Date: Tue, 21 Jun 2022 19:04:16 +0200 Subject: [PATCH 3/5] fix linting --- fedcloudclient/secret.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fedcloudclient/secret.py b/fedcloudclient/secret.py index 55940e3..b6e87c5 100644 --- a/fedcloudclient/secret.py +++ b/fedcloudclient/secret.py @@ -56,7 +56,7 @@ def secret_params_to_dict(params): if len(params) == 0: raise click.UsageError( - f"Expecting 'key=value' arguments for secrets, None provided" + "Expecting 'key=value' arguments for secrets, None provided." ) for param in params: From 76af17a3859f02b1d013235011dbc4d4cd5b928b Mon Sep 17 00:00:00 2001 From: tdviet Date: Tue, 21 Jun 2022 23:33:28 +0200 Subject: [PATCH 4/5] Add encryption --- fedcloudclient/secret.py | 97 +++++++++++++++++++++++++++++++++++++--- requirements.txt | 23 +++++----- 2 files changed, 103 insertions(+), 17 deletions(-) diff --git a/fedcloudclient/secret.py b/fedcloudclient/secret.py index b6e87c5..598ac0c 100644 --- a/fedcloudclient/secret.py +++ b/fedcloudclient/secret.py @@ -1,10 +1,16 @@ """ Implementation of "fedcloud secret" commands for accessing secret management service """ +import base64 +import json import click import hvac +import yaml +from cryptography.fernet import Fernet +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC from tabulate import tabulate from fedcloudclient.checkin import get_checkin_id @@ -13,6 +19,7 @@ VAULT_ADDR = "https://vault.services.fedcloud.eu:8200" VAULT_ROLE = "demo" VAULT_MOUNT_POINT = "/secrets" +VAULT_SALT = b'e8d3af638e26ede70afc3b3755e7c093' def secret_client(access_token, command, path, data): @@ -66,10 +73,59 @@ def secret_params_to_dict(params): raise click.UsageError( f"Expecting 'key=value' arguments for secrets. '{param}' provided." ) + if value.startswith("@"): + with open(value[1:]) as f: + value = f.read() result[key] = value return result +def generate_derived_key(passphrase): + """ + Generate derived encryption/decryption key from passphrase + :param passphrase: + :return: derived key + """ + + kdf = PBKDF2HMAC( + algorithm=hashes.SHA256(), + length=32, + salt=VAULT_SALT, + iterations=390000, + ) + return base64.urlsafe_b64encode(kdf.derive(passphrase.encode())) + + +def encrypt_data(encrypt_key, secrets): + """ + Encrypt values in secrets using key + :param encrypt_key: encryption key + :param secrets: dict containing secrets + :return: dict with encrypted values + """ + + derived_key = generate_derived_key(encrypt_key) + fernet = Fernet(derived_key) + for key in secrets: + secrets[key] = fernet.encrypt(secrets[key].encode()) + return secrets + + +def decrypt_data(decrypt_key, secrets): + """ + Decrypt values in secrets using key + :param decrypt_key: decryption key + :param secrets: dict containing encrypted secrets + :return: dict with decrypted values + """ + + derived_key = generate_derived_key(decrypt_key) + fernet = Fernet(derived_key) + for key in secrets: + secrets[key] = fernet.decrypt(secrets[key].encode()).decode() + return secrets + + @click.group() def secret(): """ @@ -79,22 +135,47 @@ def secret(): @secret.command() @oidc_params -@click.argument("short_path") +@click.option( + "--output-format", + "-f", + required=False, + type=click.Choice(["text", "YAML", "JSON"], case_sensitive=False), + +) +@click.option("--decrypt-key", required=False) +@click.argument("short_path", metavar="[secret path]") +@click.argument("key", metavar="[key]", required=False) def get( access_token, short_path, + key, + output_format, + decrypt_key, ): """ - Get a secret from the path + Get a secret from the path. If a key is given, print only the value of the key """ data = secret_client(access_token, "read_secret", short_path, None) - print(tabulate(data["data"].items(), headers=["key", "value"])) + if decrypt_key: + data["data"] = decrypt_data(decrypt_key, data["data"]) + if not key: + if output_format == "JSON": + print(json.dumps(data["data"], indent=4)) + elif output_format == "YAML": + print(yaml.dump(data["data"], sort_keys=False)) + else: + print(tabulate(data["data"].items(), headers=["key", "value"])) + else: + if key in data["data"]: + print(data["data"][key]) + else: + raise SystemExit(f"Error: {key} not found in {short_path}") @secret.command("list") @oidc_params -@click.argument("short_path", required=False, default="") +@click.argument("short_path", metavar="[secret path]", required=False, default="") def list_( access_token, short_path, @@ -109,15 +190,19 @@ def list_( @secret.command() @oidc_params -@click.argument("short_path") +@click.argument("short_path", metavar="[secret path]") @click.argument("secrets", nargs=-1, metavar="[key=value...]") +@click.option("--encrypt-key", required=False) def put( access_token, short_path, secrets, + encrypt_key, ): """ - Put secrets to the path + Put secrets to the path. Secrets are provided in form key=value """ secret_dict = secret_params_to_dict(secrets) + if encrypt_key: + secret_dict = encrypt_data(encrypt_key, secret_dict) secret_client(access_token, "put", short_path, secret_dict) diff --git a/requirements.txt b/requirements.txt index fc63f5d..debfe1c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,14 +1,15 @@ -click==8.1.3 +click~=8.1.3 click_option_group>=0.5.3 -tabulate~=0.8.9 -requests==2.28.0 -defusedxml==0.7.1 -pyjwt==2.4.0 +tabulate==0.8.10 +requests~=2.28.0 +defusedxml~=0.7.1 +pyjwt~=2.4.0 python-openstackclient==5.8.0 -liboidcagent==0.4.0 +liboidcagent~=0.4.0 jsonpath-ng==1.5.3 -PyYAML==6.0 -setuptools==62.6.0 -jsonschema==4.6.0 -psutil==5.9.1 -hvac~=0.11.2 \ No newline at end of file +PyYAML~=6.0 +setuptools~=62.6.0 +jsonschema~=4.6.0 +psutil~=5.9.1 +hvac~=0.11.2 +cryptography==37.0.3 \ No newline at end of file From fffb19400d3c4c3d73e774038a4b440984175b39 Mon Sep 17 00:00:00 2001 From: tdviet Date: Tue, 21 Jun 2022 23:39:38 +0200 Subject: [PATCH 5/5] Add encryption --- fedcloudclient/secret.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/fedcloudclient/secret.py b/fedcloudclient/secret.py index 598ac0c..3289468 100644 --- a/fedcloudclient/secret.py +++ b/fedcloudclient/secret.py @@ -19,7 +19,7 @@ VAULT_ADDR = "https://vault.services.fedcloud.eu:8200" VAULT_ROLE = "demo" VAULT_MOUNT_POINT = "/secrets" -VAULT_SALT = b'e8d3af638e26ede70afc3b3755e7c093' +VAULT_SALT = b"e8d3af638e26ede70afc3b3755e7c093" def secret_client(access_token, command, path, data): @@ -140,7 +140,6 @@ def secret(): "-f", required=False, type=click.Choice(["text", "YAML", "JSON"], case_sensitive=False), - ) @click.option("--decrypt-key", required=False) @click.argument("short_path", metavar="[secret path]")