Skip to content

Commit

Permalink
Merge pull request #3 from reynardmh/master
Browse files Browse the repository at this point in the history
Add option to pass a file containing the secrets during encoding action
  • Loading branch information
Just-Insane authored Apr 1, 2020
2 parents 5a643ee + 2d9bcca commit f992b94
Show file tree
Hide file tree
Showing 4 changed files with 197 additions and 5 deletions.
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,14 @@ Input a value for /nextcloud/password: asdf1
Input a value for /mariadb/db/password: asdf2
```

If you don't want to enter the secrets manually on stdin, you can pass a file containing the secrets. Copy `values.yaml` to `values.yaml.dec` and edit the file, replacing "changeme" (the deliminator) with the secret value. Then you can save the secret to vault by running:

```
$ helm vault enc values.yaml -s values.yaml.dec
```

By default the name of the secret file has to end in `.yaml.dec` so you can add this extension to gitignore to prevent committing a secret to your git repo.

#### Decrypt

The decrypt operation decrypts a values.yaml file and saves the decrypted result in values.yaml.dec:
Expand Down
38 changes: 33 additions & 5 deletions src/vault.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#!/usr/bin/env python3

import re
import ruamel.yaml
import hvac
import os
Expand Down Expand Up @@ -35,6 +36,7 @@ def parse_args(args):
encrypt.add_argument("-d", "--deliminator", type=str, help="The secret deliminator used when parsing. Default: \"changeme\"")
encrypt.add_argument("-vp", "--vaultpath", type=str, help="The Vault Path (secret mount location in Vault) Default: \"secret/helm\"")
encrypt.add_argument("-kv", "--kvversion", choices=['v1', 'v2'], default='v1', type=str, help="The KV Version (v1, v2) Default: \"v1\"")
encrypt.add_argument("-s", "--secret-file", type=str, help="File containing the secret for input. Must end in .yaml.dec")
encrypt.add_argument("-v", "--verbose", help="Verbose logs", const=True, nargs="?")

# Decrypt help
Expand Down Expand Up @@ -302,29 +304,54 @@ def cleanup(args):
else:
sys.exit()

def dict_walker(pattern, data, args, envs, path=None):
# Get value from a nested hash structure given a path of key names
# For example:
# secret_data['mysql']['password'] = "secret"
# value_from_path(secret_data, "/mysql/password") => returns "secret"
def value_from_path(secret_data, path):
val = secret_data
for key in path.split('/'):
if not key:
continue
if key in val.keys():
val = val[key]
else:
raise Exception(f"Missing secret value. Key {key} does not exist when retrieving value from path {path}")
return val

def dict_walker(pattern, data, args, envs, secret_data, path=None):
# Walk through the loaded dicts looking for the values we want
path = path if path is not None else ""
action = args.action
if isinstance(data, dict):
for key, value in data.items():
if value == pattern:
if action == "enc":
data[key] = input(f"Input a value for {path}/{key}: ")
if secret_data:
data[key] = value_from_path(secret_data, f"{path}/{key}")
else:
data[key] = input(f"Input a value for {path}/{key}: ")
vault = Vault(args, envs)
vault.vault_write(data[key], path, key)
elif (action == "dec") or (action == "view") or (action == "edit") or (action == "install") or (action == "template") or (action == "upgrade") or (action == "lint") or (action == "diff"):
vault = Vault(args, envs)
vault = vault.vault_read(value, path, key)
value = vault
data[key] = value
for res in dict_walker(pattern, value, args, envs, path=f"{path}/{key}"):
for res in dict_walker(pattern, value, args, envs, secret_data, path=f"{path}/{key}"):
yield res
elif isinstance(data, list):
for item in data:
for res in dict_walker(pattern, item, args, envs, path=f"{path}"):
for res in dict_walker(pattern, item, args, envs, secret_data, path=f"{path}"):
yield res


def load_secret(args):
if args.secret_file:
if not re.search(r'\.yaml\.dec$', args.secret_file):
raise Exception(f"ERROR: Secret file name must end with \".yaml.dec\". {args.secret_file} was given instead.")
return load_yaml(args.secret_file)

def main(argv=None):

# Parse arguments from argparse
Expand All @@ -343,8 +370,9 @@ def main(argv=None):
envs = envs.get_envs()
yaml = ruamel.yaml.YAML()
yaml.preserve_quotes = True
secret_data = load_secret(args) if args.action == 'enc' else None

for path, key, value in dict_walker(envs[1], data, args, envs):
for path, key, value in dict_walker(envs[1], data, args, envs, secret_data):
print("Done")

if action == "dec":
Expand Down
118 changes: 118 additions & 0 deletions tests/test.yaml.dec
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
## Official nextcloud image version
## ref: https://hub.docker.com/r/library/nextcloud/tags/
##
image:
repository: nextcloud
tag: 15.0.2-apache
pullPolicy: IfNotPresent
# pullSecrets:
# - myRegistrKeySecretName

nameOverride: ""
fullnameOverride: ""

# Number of replicas to be deployed
replicaCount: 1

## Allowing use of ingress controllers
## ref: https://kubernetes.io/docs/concepts/services-networking/ingress/
##
ingress:
enabled: true
annotations: {}

nextcloud:
host: nextcloud.corp.justin-tech.com
username: admin
password: nextpass


internalDatabase:
enabled: true
name: nextcloud


##
## External database configuration
##
externalDatabase:
enabled: false

## Database host
host:

## Database user
user: nextcloud

## Database password
password:

## Database name
database: nextcloud

##
## MariaDB chart configuration
##
mariadb:
## Whether to deploy a mariadb server to satisfy the applications database requirements. To use an external database set this to false and configure the externalDatabase parameters
enabled: true

db:
name: nextcloud
user: nextcloud
password: mariapass

## Enable persistence using Persistent Volume Claims
## ref: http://kubernetes.io/docs/user-guide/persistent-volumes/
##
persistence:
enabled: true
storageClass: "nfs-client"
accessMode: ReadWriteOnce
size: 8Gi

service:
type: ClusterIP
port: 8080
loadBalancerIP: nil


## Enable persistence using Persistent Volume Claims
## ref: http://kubernetes.io/docs/user-guide/persistent-volumes/
##
persistence:
enabled: true
## nextcloud data Persistent Volume Storage Class
## If defined, storageClassName: <storageClass>
## If set to "-", storageClassName: "", which disables dynamic provisioning
## If undefined (the default) or set to null, no storageClassName spec is
## set, choosing the default provisioner. (gp2 on AWS, standard on
## GKE, AWS & OpenStack)
##
storageClass: "nfs-client"

## A manually managed Persistent Volume and Claim
## Requires persistence.enabled: true
## If defined, PVC must be created manually before volume will be bound
# existingClaim:

accessMode: ReadWriteOnce
size: 8Gi

resources: {}
# We usually recommend not to specify default resources and to leave this as a conscious
# choice for the user. This also increases chances charts run on environments with little
# resources, such as Minikube. If you do want to specify resources, uncomment the following
# lines, adjust them as necessary, and remove the curly braces after 'resources:'.
# limits:
# cpu: 100m
# memory: 128Mi
# requests:
# cpu: 100m
# memory: 128Mi

nodeSelector: {}

tolerations: []

affinity: {}
38 changes: 38 additions & 0 deletions tests/test_vault.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,17 @@ def mock_input(s):
'Input a value for /mariadb/db/password: ',
]

def test_refuse_enc_from_file_with_bad_name():
with pytest.raises(Exception) as e:
vault.main(['enc', './tests/test.yaml', '-s', './tests/test.yaml.bad'])
assert "ERROR: Secret file name must end with" in str(e.value)

def test_enc_from_file():
os.environ["KVVERSION"] = "v2"
vault.main(['enc', './tests/test.yaml', '-s', './tests/test.yaml.dec'])
assert True # If it reaches here without error then encoding was a success
# TODO: Maybe test if the secret is correctly saved to vault

def test_dec():
os.environ["KVVERSION"] = "v2"
input_values = ["adfs1", "adfs2"]
Expand All @@ -76,6 +87,33 @@ def mock_input(s):
'Done Decrypting',
]

def test_value_from_path():
data = {
"chapter1": {
"chapter1.1": {
"chapter1.1.1": "good",
"chapter1.1.2": "bad",
},
"chapter1.2": {
"chapter1.2.1": "good",
"chapter1.2.2": "bad",
}
}
}
val = vault.value_from_path(data, "/")
assert val == data
val = vault.value_from_path(data, "/chapter1/chapter1.1")
assert val == {
"chapter1.1.1": "good",
"chapter1.1.2": "bad",
}
val = vault.value_from_path(data, "/chapter1/chapter1.1/chapter1.1.2")
assert val == "bad"

with pytest.raises(Exception) as e:
val = vault.value_from_path(data, "/chapter1/chapter1.1/bleh")
assert "Missing secret value" in str(e.value)

def test_clean():
os.environ["KVVERSION"] = "v2"
copyfile("./tests/test.yaml.dec", "./tests/test.yaml.dec.bak")
Expand Down

0 comments on commit f992b94

Please sign in to comment.