Skip to content

Commit 9d60c58

Browse files
rjhaverkampamuraru
andauthored
add sops as a secret reslover (#155)
* add sops as a secret reslover to himl. Sops code from: https://github.com/ansible-collections/community.sops/tree/main * add caching for sops resolver. This prevents having to touch the yubikey on every secret resolve --------- Co-authored-by: Adrian Muraru <[email protected]>
1 parent ce8437d commit 9d60c58

File tree

4 files changed

+142
-3
lines changed

4 files changed

+142
-3
lines changed

examples/secrets/default.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
---
22
secret_path_v2: "{{vault.path(/kv2_secret)}}"
33
secret_key_v2: "{{vault.key(/kv2_secret/key)}}"
4+
sops_secret: "{{ sops.secret_file(/home/user/secrets/secret_file.yaml).secret_key(['s3']['access_key']) }}"

himl/secret_resolvers.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
import logging
1212
import os
1313

14-
1514
class SecretResolver:
1615
def supports(self, secret_type):
1716
return False
@@ -67,6 +66,15 @@ def resolve(self, secret_type, secret_params):
6766
s3 = SimpleS3(aws_profile, region_name)
6867
return s3.get(bucket, path, base64Encode)
6968

69+
class SopsSecretResolver(SecretResolver):
70+
def supports(self, secret_type):
71+
return secret_type == "sops"
72+
73+
def resolve(self, secret_type, secret_params):
74+
from .simplesops import SimpleSops
75+
file = self.get_param_or_exception("secret_file", secret_params)
76+
sops = SimpleSops()
77+
return sops.get(secret_file=file, secret_key=secret_params.get("secret_key"))
7078

7179
class VaultSecretResolver(SecretResolver):
7280
def supports(self, secret_type):
@@ -96,7 +104,7 @@ def resolve(self, secret_type, secret_params):
96104
class AggregatedSecretResolver(SecretResolver):
97105
def __init__(self, default_aws_profile=None):
98106
self.secret_resolvers = (SSMSecretResolver(default_aws_profile), S3SecretResolver(default_aws_profile),
99-
VaultSecretResolver())
107+
VaultSecretResolver(), SopsSecretResolver())
100108

101109
def supports(self, secret_type):
102110
return any([resolver.supports(secret_type) for resolver in self.secret_resolvers])

himl/simplesops.py

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
# Copyright (c), Edoardo Tenani <[email protected]>, 2018-2020
2+
# Simplified BSD License (see LICENSES/BSD-2-Clause.txt or https://opensource.org/licenses/BSD-2-Clause)
3+
# SPDX-License-Identifier: BSD-2-Clause
4+
5+
from __future__ import absolute_import, division, print_function
6+
from functools import lru_cache
7+
8+
import os, logging, yaml
9+
10+
from subprocess import Popen, PIPE
11+
12+
logger = logging.getLogger(__name__)
13+
14+
# From https://github.com/getsops/sops/blob/master/cmd/sops/codes/codes.go
15+
# Should be manually updated
16+
SOPS_ERROR_CODES = {
17+
1: "ErrorGeneric",
18+
2: "CouldNotReadInputFile",
19+
3: "CouldNotWriteOutputFile",
20+
4: "ErrorDumpingTree",
21+
5: "ErrorReadingConfig",
22+
6: "ErrorInvalidKMSEncryptionContextFormat",
23+
7: "ErrorInvalidSetFormat",
24+
8: "ErrorConflictingParameters",
25+
21: "ErrorEncryptingMac",
26+
23: "ErrorEncryptingTree",
27+
24: "ErrorDecryptingMac",
28+
25: "ErrorDecryptingTree",
29+
49: "CannotChangeKeysFromNonExistentFile",
30+
51: "MacMismatch",
31+
52: "MacNotFound",
32+
61: "ConfigFileNotFound",
33+
85: "KeyboardInterrupt",
34+
91: "InvalidTreePathFormat",
35+
100: "NoFileSpecified",
36+
128: "CouldNotRetrieveKey",
37+
111: "NoEncryptionKeyFound",
38+
200: "FileHasNotBeenModified",
39+
201: "NoEditorFound",
40+
202: "FailedToCompareVersions",
41+
203: "FileAlreadyEncrypted",
42+
}
43+
44+
45+
class SopsError(Exception):
46+
"""Extend Exception class with sops specific information"""
47+
48+
def __init__(self, filename, exit_code, message, decryption=True):
49+
if exit_code in SOPS_ERROR_CODES:
50+
exception_name = SOPS_ERROR_CODES[exit_code]
51+
message = "error with file %s: %s exited with code %d: %s" % (
52+
filename,
53+
exception_name,
54+
exit_code,
55+
message,
56+
)
57+
else:
58+
message = (
59+
"could not %s file %s; Unknown sops error code: %s; message: %s"
60+
% (
61+
"decrypt" if decryption else "encrypt",
62+
filename,
63+
exit_code,
64+
message,
65+
)
66+
)
67+
super(SopsError, self).__init__(message)
68+
69+
70+
class Sops:
71+
"""Utility class to perform sops CLI actions"""
72+
73+
@lru_cache(maxsize=2048)
74+
def decrypt(
75+
encrypted_file,
76+
decode_output=True,
77+
rstrip=True,
78+
):
79+
command = ["sops"]
80+
env = os.environ.copy()
81+
82+
command.extend(["--decrypt", encrypted_file])
83+
process = Popen(
84+
command,
85+
stdin=None,
86+
stdout=PIPE,
87+
stderr=PIPE,
88+
env=env,
89+
)
90+
(output, err) = process.communicate()
91+
exit_code = process.returncode
92+
93+
if decode_output:
94+
# output is binary, we want UTF-8 string
95+
output = output.decode("utf-8", errors="surrogate_or_strict")
96+
# the process output is the decrypted secret; be cautious
97+
if exit_code != 0:
98+
raise SopsError(encrypted_file, exit_code, err, decryption=True)
99+
100+
if rstrip:
101+
output = output.rstrip()
102+
return yaml.full_load(output)
103+
104+
def get_keys(self, secret_file, secret_key):
105+
result = Sops.decrypt(secret_file)
106+
secret_key_path = secret_key.strip("[]")
107+
keys = [key.strip("'") for key in secret_key_path.split("']['")]
108+
try:
109+
for key in keys:
110+
result = result[key]
111+
except KeyError as e:
112+
raise SopsError(secret_file, 128, "Encountered KeyError parsing yaml for key: %s" % secret_key, decryption=True)
113+
return result
114+
115+
116+
class SimpleSops:
117+
def __init__(self):
118+
pass
119+
120+
def get(self, secret_file, secret_key):
121+
try:
122+
logger.info(
123+
"Resolving sops secret %s from file %s", secret_key, secret_file
124+
)
125+
return Sops().get_keys(secret_file=secret_file, secret_key=secret_key)
126+
except SopsError as e:
127+
raise Exception(
128+
"Error while trying to read sops value for file %s, key: %s - %s"
129+
% (secret_file, secret_key, e)
130+
)

setup.py

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

3030
setup(
3131
name='himl',
32-
version="0.15.1",
32+
version="0.16.0",
3333
description='A hierarchical config using yaml',
3434
long_description=_readme + '\n\n',
3535
long_description_content_type='text/markdown',

0 commit comments

Comments
 (0)