mirror of
https://github.com/ansible/awx.git
synced 2026-03-09 13:39:27 -02:30
add support for HashiCorp signed SSH certificates
This commit is contained in:
committed by
Jake McDermott
parent
4ed5bca5e3
commit
7a43f00a5d
@@ -37,7 +37,7 @@ azure_keyvault_inputs = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def azure_keyvault_backend(**kwargs):
|
def azure_keyvault_backend(raw, **kwargs):
|
||||||
url = kwargs['url']
|
url = kwargs['url']
|
||||||
|
|
||||||
def auth_callback(server, resource, scope):
|
def auth_callback(server, resource, scope):
|
||||||
|
|||||||
@@ -6,22 +6,12 @@ from .plugin import CredentialPlugin
|
|||||||
from hvac import Client
|
from hvac import Client
|
||||||
|
|
||||||
|
|
||||||
hashi_inputs = {
|
base_inputs = {
|
||||||
'fields': [{
|
'fields': [{
|
||||||
'id': 'url',
|
'id': 'url',
|
||||||
'label': 'Server URL',
|
'label': 'Server URL',
|
||||||
'type': 'string',
|
'type': 'string',
|
||||||
'help_text': 'The URL to the HashiCorp Vault',
|
'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',
|
'id': 'token',
|
||||||
'label': 'Token',
|
'label': 'Token',
|
||||||
@@ -29,31 +19,60 @@ hashi_inputs = {
|
|||||||
'secret': True,
|
'secret': True,
|
||||||
'help_text': 'The access token used to authenticate to the Vault server',
|
'help_text': 'The access token used to authenticate to the Vault server',
|
||||||
}, {
|
}, {
|
||||||
'id': 'api_version',
|
'id': 'secret_path',
|
||||||
'label': 'API Version',
|
'label': 'Path to Secret',
|
||||||
'choices': ['v1', 'v2'],
|
'type': 'string',
|
||||||
'help_text': 'API v1 is for static key/value lookups. API v2 is for versioned key/value lookups.',
|
'help_text': 'The path to the secret e.g., /some-engine/some-secret/',
|
||||||
'default': 'v1',
|
}],
|
||||||
|
'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',
|
'id': 'secret_version',
|
||||||
'label': 'Secret Version (v2 only)',
|
'label': 'Secret Version (v2 only)',
|
||||||
'type': 'string',
|
'type': 'string',
|
||||||
'help_text': 'Used to specify a specific secret version (if left empty, the latest version will be used).',
|
'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):
|
def kv_backend(raw, **kwargs):
|
||||||
token = kwargs.get('token')
|
token = kwargs['token']
|
||||||
url = kwargs.get('url')
|
url = kwargs['url']
|
||||||
secret_path = kwargs.get('secret_path')
|
secret_path = kwargs['secret_path']
|
||||||
secret_field = kwargs.get('secret_field', None)
|
secret_field = kwargs.get('secret_field', None)
|
||||||
verify = kwargs.get('verify', False)
|
|
||||||
|
|
||||||
api_version = kwargs.get('api_version', None)
|
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':
|
if api_version == 'v2':
|
||||||
try:
|
try:
|
||||||
mount_point, *path = pathlib.Path(secret_path.lstrip(os.sep)).parts
|
mount_point, *path = pathlib.Path(secret_path.lstrip(os.sep)).parts
|
||||||
@@ -90,8 +109,31 @@ def hashi_backend(**kwargs):
|
|||||||
return response['data']
|
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',
|
'HashiCorp Vault Secret Lookup',
|
||||||
inputs=hashi_inputs,
|
inputs=hashi_kv_inputs,
|
||||||
backend=hashi_backend
|
backend=kv_backend
|
||||||
|
)
|
||||||
|
|
||||||
|
hashivault_ssh_plugin = CredentialPlugin(
|
||||||
|
'HashiCorp Vault Signed SSH',
|
||||||
|
inputs=hashi_ssh_inputs,
|
||||||
|
backend=ssh_backend
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -808,6 +808,11 @@ ManagedCredentialType(
|
|||||||
'format': 'ssh_private_key',
|
'format': 'ssh_private_key',
|
||||||
'secret': True,
|
'secret': True,
|
||||||
'multiline': True
|
'multiline': True
|
||||||
|
}, {
|
||||||
|
'id': 'ssh_public_key_data',
|
||||||
|
'label': ugettext_noop('Signed SSH Certificate'),
|
||||||
|
'type': 'string',
|
||||||
|
'multiline': True,
|
||||||
}, {
|
}, {
|
||||||
'id': 'ssh_key_unlock',
|
'id': 'ssh_key_unlock',
|
||||||
'label': ugettext_noop('Private Key Passphrase'),
|
'label': ugettext_noop('Private Key Passphrase'),
|
||||||
@@ -1342,7 +1347,10 @@ class CredentialInputSource(PrimordialModel):
|
|||||||
backend_kwargs[field_name] = decrypt_field(self.source_credential, field_name)
|
backend_kwargs[field_name] = decrypt_field(self.source_credential, field_name)
|
||||||
else:
|
else:
|
||||||
backend_kwargs[field_name] = value
|
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):
|
def get_absolute_url(self, request=None):
|
||||||
view_name = 'api:credential_input_source_detail'
|
view_name = 'api:credential_input_source_detail'
|
||||||
|
|||||||
@@ -772,7 +772,12 @@ class BaseTask(object):
|
|||||||
'credentials': {
|
'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',
|
<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).
|
# and we're running an earlier version (<6.5).
|
||||||
if 'OPENSSH PRIVATE KEY' in data and not openssh_keys_supported:
|
if 'OPENSSH PRIVATE KEY' in data and not openssh_keys_supported:
|
||||||
raise RuntimeError(OPENSSH_KEY_ERROR)
|
raise RuntimeError(OPENSSH_KEY_ERROR)
|
||||||
for credential, data in private_data.get('credentials', {}).items():
|
|
||||||
# OpenSSH formatted keys must have a trailing newline to be
|
# OpenSSH formatted keys must have a trailing newline to be
|
||||||
# accepted by ssh-add.
|
# accepted by ssh-add.
|
||||||
if 'OPENSSH PRIVATE KEY' in data and not data.endswith('\n'):
|
if 'OPENSSH PRIVATE KEY' in data and not data.endswith('\n'):
|
||||||
@@ -813,6 +817,13 @@ class BaseTask(object):
|
|||||||
f.close()
|
f.close()
|
||||||
os.chmod(path, stat.S_IRUSR | stat.S_IWUSR)
|
os.chmod(path, stat.S_IRUSR | stat.S_IWUSR)
|
||||||
private_data_files['credentials'][credential] = path
|
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
|
return private_data_files
|
||||||
|
|
||||||
def build_passwords(self, instance, runtime_passwords):
|
def build_passwords(self, instance, runtime_passwords):
|
||||||
@@ -1269,7 +1280,12 @@ class RunJob(BaseTask):
|
|||||||
'credentials': {
|
'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>,
|
<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).
|
# back (they will be written to a temporary file).
|
||||||
if credential.has_input('ssh_key_data'):
|
if credential.has_input('ssh_key_data'):
|
||||||
private_data['credentials'][credential] = credential.get_input('ssh_key_data', default='')
|
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':
|
if credential.kind == 'openstack':
|
||||||
openstack_auth = dict(auth_url=credential.get_input('host', default=''),
|
openstack_auth = dict(auth_url=credential.get_input('host', default=''),
|
||||||
@@ -2187,7 +2205,12 @@ class RunAdHocCommand(BaseTask):
|
|||||||
'credentials': {
|
'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>,
|
<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': {}}
|
private_data = {'credentials': {}}
|
||||||
if creds and creds.has_input('ssh_key_data'):
|
if creds and creds.has_input('ssh_key_data'):
|
||||||
private_data['credentials'][creds] = creds.get_input('ssh_key_data', default='')
|
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
|
return private_data
|
||||||
|
|
||||||
def build_passwords(self, ad_hoc_command, runtime_passwords):
|
def build_passwords(self, ad_hoc_command, runtime_passwords):
|
||||||
|
|||||||
@@ -76,10 +76,12 @@ GLqbpJyX2r3p/Rmo6mLY71SqpA==
|
|||||||
def test_default_cred_types():
|
def test_default_cred_types():
|
||||||
assert sorted(CredentialType.defaults.keys()) == [
|
assert sorted(CredentialType.defaults.keys()) == [
|
||||||
'aws',
|
'aws',
|
||||||
|
'azure_kv',
|
||||||
'azure_rm',
|
'azure_rm',
|
||||||
'cloudforms',
|
'cloudforms',
|
||||||
'gce',
|
'gce',
|
||||||
'hashivault',
|
'hashivault_kv',
|
||||||
|
'hashivault_ssh',
|
||||||
'insights',
|
'insights',
|
||||||
'net',
|
'net',
|
||||||
'openstack',
|
'openstack',
|
||||||
|
|||||||
3
setup.py
3
setup.py
@@ -115,7 +115,8 @@ setup(
|
|||||||
'awx-manage = awx:manage',
|
'awx-manage = awx:manage',
|
||||||
],
|
],
|
||||||
'awx.credential_plugins': [
|
'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',
|
'azure_kv = awx.main.credential_plugins.azure_kv:azure_keyvault_plugin',
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,5 +3,6 @@ tower-manage = awx:manage
|
|||||||
awx-manage = awx:manage
|
awx-manage = awx:manage
|
||||||
|
|
||||||
[awx.credential_plugins]
|
[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
|
azure_kv = awx.main.credential_plugins.azure_kv:azure_keyvault_plugin
|
||||||
|
|||||||
Reference in New Issue
Block a user