add support for HashiCorp signed SSH certificates

This commit is contained in:
Ryan Petrello 2019-02-22 14:45:27 -05:00 committed by Jake McDermott
parent 4ed5bca5e3
commit 7a43f00a5d
No known key found for this signature in database
GPG Key ID: 9A6F084352C3A0B7
7 changed files with 114 additions and 35 deletions

View File

@ -37,7 +37,7 @@ azure_keyvault_inputs = {
}
def azure_keyvault_backend(**kwargs):
def azure_keyvault_backend(raw, **kwargs):
url = kwargs['url']
def auth_callback(server, resource, scope):

View File

@ -6,22 +6,12 @@ from .plugin import CredentialPlugin
from hvac import Client
hashi_inputs = {
base_inputs = {
'fields': [{
'id': 'url',
'label': 'Server URL',
'type': 'string',
'help_text': 'The URL to the HashiCorp Vault',
}, {
'id': 'secret_path',
'label': 'Path to Secret',
'type': 'string',
'help_text': 'The path to the secret e.g., /some-engine/some-secret/',
}, {
'id': 'secret_field',
'label': 'Key Name',
'type': 'string',
'help_text': 'The name of the key to look up in the secret.',
}, {
'id': 'token',
'label': 'Token',
@ -29,31 +19,60 @@ hashi_inputs = {
'secret': True,
'help_text': 'The access token used to authenticate to the Vault server',
}, {
'id': 'api_version',
'label': 'API Version',
'choices': ['v1', 'v2'],
'help_text': 'API v1 is for static key/value lookups. API v2 is for versioned key/value lookups.',
'default': 'v1',
'id': 'secret_path',
'label': 'Path to Secret',
'type': 'string',
'help_text': 'The path to the secret e.g., /some-engine/some-secret/',
}],
'required': ['url', 'token', 'secret_path'],
}
hashi_kv_inputs = {
'fields': base_inputs['fields'] + [{
'id': 'secret_field',
'label': 'Key Name',
'type': 'string',
'help_text': 'The name of the key to look up in the secret.',
}, {
'id': 'secret_version',
'label': 'Secret Version (v2 only)',
'type': 'string',
'help_text': 'Used to specify a specific secret version (if left empty, the latest version will be used).',
}, {
'id': 'api_version',
'label': 'API Version',
'choices': ['v1', 'v2'],
'help_text': 'API v1 is for static key/value lookups. API v2 is for versioned key/value lookups.',
'default': 'v1',
}],
'required': ['url', 'secret_path', 'secret_field', 'token', 'api_version'],
'required': base_inputs['required'] + ['secret_field', 'api_version']
}
hashi_ssh_inputs = {
'fields': base_inputs['fields'] + [{
'id': 'role',
'label': 'Role Name',
'type': 'string',
'help_text': 'The name of the role used to sign.'
}, {
'id': 'valid_principals',
'label': 'Valid Principals',
'type': 'string',
'help_text': 'Valid principals (either usernames or hostnames) that the certificate should be signed for.',
}],
'required': base_inputs['required'] + ['role']
}
def hashi_backend(**kwargs):
token = kwargs.get('token')
url = kwargs.get('url')
secret_path = kwargs.get('secret_path')
def kv_backend(raw, **kwargs):
token = kwargs['token']
url = kwargs['url']
secret_path = kwargs['secret_path']
secret_field = kwargs.get('secret_field', None)
verify = kwargs.get('verify', False)
api_version = kwargs.get('api_version', None)
client = Client(url=url, token=token, verify=verify)
client = Client(url=url, token=token, verify=True)
if api_version == 'v2':
try:
mount_point, *path = pathlib.Path(secret_path.lstrip(os.sep)).parts
@ -90,8 +109,31 @@ def hashi_backend(**kwargs):
return response['data']
hashivault_plugin = CredentialPlugin(
def ssh_backend(raw, **kwargs):
token = kwargs['token']
url = kwargs['url']
client = Client(url=url, token=token, verify=True)
json = {
'public_key': raw
}
if kwargs.get('valid_principals'):
json['valid_principals'] = kwargs['valid_principals']
resp = client._adapter.post(
'/v1/{}/sign/{}'.format(kwargs['secret_path'], kwargs['role']),
json=json,
)
return resp.json()['data']['signed_key']
hashivault_kv_plugin = CredentialPlugin(
'HashiCorp Vault Secret Lookup',
inputs=hashi_inputs,
backend=hashi_backend
inputs=hashi_kv_inputs,
backend=kv_backend
)
hashivault_ssh_plugin = CredentialPlugin(
'HashiCorp Vault Signed SSH',
inputs=hashi_ssh_inputs,
backend=ssh_backend
)

View File

@ -808,6 +808,11 @@ ManagedCredentialType(
'format': 'ssh_private_key',
'secret': True,
'multiline': True
}, {
'id': 'ssh_public_key_data',
'label': ugettext_noop('Signed SSH Certificate'),
'type': 'string',
'multiline': True,
}, {
'id': 'ssh_key_unlock',
'label': ugettext_noop('Private Key Passphrase'),
@ -1342,7 +1347,10 @@ class CredentialInputSource(PrimordialModel):
backend_kwargs[field_name] = decrypt_field(self.source_credential, field_name)
else:
backend_kwargs[field_name] = value
return backend(**backend_kwargs)
return backend(
self.target_credential.inputs.get(self.input_field_name),
**backend_kwargs
)
def get_absolute_url(self, request=None):
view_name = 'api:credential_input_source_detail'

View File

@ -772,7 +772,12 @@ class BaseTask(object):
'credentials': {
<awx.main.models.Credential>: '/path/to/decrypted/data',
<awx.main.models.Credential>: '/path/to/decrypted/data',
<awx.main.models.Credential>: '/path/to/decrypted/data',
...
},
'certificates': {
<awx.main.models.Credential>: /path/to/signed/ssh/certificate,
<awx.main.models.Credential>: /path/to/signed/ssh/certificate,
...
}
}
'''
@ -787,7 +792,6 @@ class BaseTask(object):
# and we're running an earlier version (<6.5).
if 'OPENSSH PRIVATE KEY' in data and not openssh_keys_supported:
raise RuntimeError(OPENSSH_KEY_ERROR)
for credential, data in private_data.get('credentials', {}).items():
# OpenSSH formatted keys must have a trailing newline to be
# accepted by ssh-add.
if 'OPENSSH PRIVATE KEY' in data and not data.endswith('\n'):
@ -813,6 +817,13 @@ class BaseTask(object):
f.close()
os.chmod(path, stat.S_IRUSR | stat.S_IWUSR)
private_data_files['credentials'][credential] = path
for credential, data in private_data.get('certificates', {}).items():
name = 'credential_%d-cert.pub' % credential.pk
path = os.path.join(kwargs['private_data_dir'], name)
with open(path, 'w') as f:
f.write(data)
f.close()
os.chmod(path, stat.S_IRUSR | stat.S_IWUSR)
return private_data_files
def build_passwords(self, instance, runtime_passwords):
@ -1269,7 +1280,12 @@ class RunJob(BaseTask):
'credentials': {
<awx.main.models.Credential>: <credential_decrypted_ssh_key_data>,
<awx.main.models.Credential>: <credential_decrypted_ssh_key_data>,
<awx.main.models.Credential>: <credential_decrypted_ssh_key_data>
...
},
'certificates': {
<awx.main.models.Credential>: <signed SSH certifacte data>,
<awx.main.models.Credential>: <signed SSH certifacte data>,
...
}
}
'''
@ -1279,6 +1295,8 @@ class RunJob(BaseTask):
# back (they will be written to a temporary file).
if credential.has_input('ssh_key_data'):
private_data['credentials'][credential] = credential.get_input('ssh_key_data', default='')
if credential.has_input('ssh_public_key_data'):
private_data.setdefault('certificates', {})[credential] = credential.get_input('ssh_public_key_data', default='')
if credential.kind == 'openstack':
openstack_auth = dict(auth_url=credential.get_input('host', default=''),
@ -2187,7 +2205,12 @@ class RunAdHocCommand(BaseTask):
'credentials': {
<awx.main.models.Credential>: <credential_decrypted_ssh_key_data>,
<awx.main.models.Credential>: <credential_decrypted_ssh_key_data>,
<awx.main.models.Credential>: <credential_decrypted_ssh_key_data>
...
},
'certificates': {
<awx.main.models.Credential>: <signed SSH certifacte data>,
<awx.main.models.Credential>: <signed SSH certifacte data>,
...
}
}
'''
@ -2197,6 +2220,8 @@ class RunAdHocCommand(BaseTask):
private_data = {'credentials': {}}
if creds and creds.has_input('ssh_key_data'):
private_data['credentials'][creds] = creds.get_input('ssh_key_data', default='')
if creds and creds.has_input('ssh_public_key_data'):
private_data.setdefault('certificates', {})[creds] = creds.get_input('ssh_public_key_data', default='')
return private_data
def build_passwords(self, ad_hoc_command, runtime_passwords):

View File

@ -76,10 +76,12 @@ GLqbpJyX2r3p/Rmo6mLY71SqpA==
def test_default_cred_types():
assert sorted(CredentialType.defaults.keys()) == [
'aws',
'azure_kv',
'azure_rm',
'cloudforms',
'gce',
'hashivault',
'hashivault_kv',
'hashivault_ssh',
'insights',
'net',
'openstack',

View File

@ -115,7 +115,8 @@ setup(
'awx-manage = awx:manage',
],
'awx.credential_plugins': [
'hashivault = awx.main.credential_plugins.hashivault:hashivault_plugin',
'hashivault_kv = awx.main.credential_plugins.hashivault:hashivault_kv_plugin',
'hashivault_ssh = awx.main.credential_plugins.hashivault:hashivault_ssh_plugin',
'azure_kv = awx.main.credential_plugins.azure_kv:azure_keyvault_plugin',
]
},

View File

@ -3,5 +3,6 @@ tower-manage = awx:manage
awx-manage = awx:manage
[awx.credential_plugins]
hashivault = awx.main.credential_plugins.hashivault:hashivault_plugin
hashivault_kv = awx.main.credential_plugins.hashivault:hashivault_kv_plugin
hashivault_ssh = awx.main.credential_plugins.hashivault:hashivault_ssh_plugin
azure_kv = awx.main.credential_plugins.azure_kv:azure_keyvault_plugin