Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add TLS certificate auth for HashiCorp Vault #14534

Merged
merged 6 commits into from
Dec 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ PROMETHEUS ?= false
GRAFANA ?= false
# If set to true docker-compose will also start a hashicorp vault instance
VAULT ?= false
# If set to true docker-compose will also start a hashicorp vault instance with TLS enabled
VAULT_TLS ?= false
# If set to true docker-compose will also start a tacacs+ instance
TACACS ?= false

Expand Down Expand Up @@ -528,13 +530,15 @@ docker-compose-sources: .git/hooks/pre-commit
-e enable_prometheus=$(PROMETHEUS) \
-e enable_grafana=$(GRAFANA) \
-e enable_vault=$(VAULT) \
-e vault_tls=$(VAULT_TLS) \
-e enable_tacacs=$(TACACS) \
$(EXTRA_SOURCES_ANSIBLE_OPTS)

docker-compose: awx/projects docker-compose-sources
ansible-galaxy install --ignore-certs -r tools/docker-compose/ansible/requirements.yml;
ansible-playbook -i tools/docker-compose/inventory tools/docker-compose/ansible/initialize_containers.yml \
-e enable_vault=$(VAULT);
-e enable_vault=$(VAULT) \
-e vault_tls=$(VAULT_TLS);
$(DOCKER_COMPOSE) -f tools/docker-compose/_sources/docker-compose.yml $(COMPOSE_OPTS) up $(COMPOSE_UP_OPTS) --remove-orphans

docker-compose-credential-plugins: awx/projects docker-compose-sources
Expand Down
47 changes: 45 additions & 2 deletions awx/main/credential_plugins/hashivault.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,34 @@
'secret': True,
'help_text': _('The Secret ID for AppRole Authentication'),
},
{
'id': 'client_cert_public',
'label': _('Client Certificate'),
'type': 'string',
'multiline': True,
'help_text': _(
'The PEM-encoded client certificate used for TLS client authentication.'
' This should include the certificate and any intermediate certififcates.'
),
},
{
'id': 'client_cert_private',
'label': _('Client Certificate Key'),
'type': 'string',
'multiline': True,
'secret': True,
'help_text': _('The certificate private key used for TLS client authentication.'),
},
{
'id': 'client_cert_role',
'label': _('TLS Authentication Role'),
'type': 'string',
'multiline': False,
'help_text': _(
'The role configured in Hashicorp Vault for TLS client authentication.'
' If not provided, Hashicorp Vault may assign roles based on the certificate used.'
),
},
{
'id': 'namespace',
'label': _('Namespace name (Vault Enterprise only)'),
Expand Down Expand Up @@ -164,8 +192,10 @@ def handle_auth(**kwargs):
token = method_auth(**kwargs, auth_param=approle_auth(**kwargs))
elif kwargs.get('kubernetes_role'):
token = method_auth(**kwargs, auth_param=kubernetes_auth(**kwargs))
elif kwargs.get('client_cert_public') and kwargs.get('client_cert_private'):
token = method_auth(**kwargs, auth_param=client_cert_auth(**kwargs))
else:
raise Exception('Either token or AppRole/Kubernetes authentication parameters must be set')
raise Exception('Either a token or AppRole, Kubernetes, or TLS authentication parameters must be set')

return token

Expand All @@ -181,6 +211,10 @@ def kubernetes_auth(**kwargs):
return {'role': kwargs['kubernetes_role'], 'jwt': jwt}


def client_cert_auth(**kwargs):
return {'name': kwargs.get('client_cert_role')}


def method_auth(**kwargs):
# get auth method specific params
request_kwargs = {'json': kwargs['auth_param'], 'timeout': 30}
Expand All @@ -193,13 +227,22 @@ def method_auth(**kwargs):
cacert = kwargs.get('cacert', None)

sess = requests.Session()

# Namespace support
if kwargs.get('namespace'):
sess.headers['X-Vault-Namespace'] = kwargs['namespace']
request_url = '/'.join([url, 'auth', auth_path, 'login']).rstrip('/')
with CertFiles(cacert) as cert:
request_kwargs['verify'] = cert
resp = sess.post(request_url, **request_kwargs)
# TLS client certificate support
if kwargs.get('client_cert_public') and kwargs.get('client_cert_private'):
# Add client cert to requests Session before making call
with CertFiles(kwargs['client_cert_public'], key=kwargs['client_cert_private']) as client_cert:
sess.cert = client_cert
resp = sess.post(request_url, **request_kwargs)
else:
# Make call without client certificate
resp = sess.post(request_url, **request_kwargs)
resp.raise_for_status()
token = resp.json()['auth']['client_token']
return token
Expand Down
36 changes: 36 additions & 0 deletions awx/main/tests/functional/test_credential_plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,26 @@ def test_hashivault_kubernetes_auth():
assert res == expected_res


def test_hashivault_client_cert_auth_explicit_role():
kwargs = {
'client_cert_role': 'test-cert-1',
}
expected_res = {
'name': 'test-cert-1',
}
res = hashivault.client_cert_auth(**kwargs)
assert res == expected_res


def test_hashivault_client_cert_auth_no_role():
kwargs = {}
expected_res = {
'name': None,
}
res = hashivault.client_cert_auth(**kwargs)
assert res == expected_res


def test_hashivault_handle_auth_token():
kwargs = {
'token': 'the_token',
Expand Down Expand Up @@ -73,6 +93,22 @@ def test_hashivault_handle_auth_kubernetes():
assert token == 'the_token'


def test_hashivault_handle_auth_client_cert():
kwargs = {
'client_cert_public': "foo",
'client_cert_private': "bar",
'client_cert_role': 'test-cert-1',
}
auth_params = {
'name': 'test-cert-1',
}
with mock.patch.object(hashivault, 'method_auth') as method_mock:
method_mock.return_value = 'the_token'
token = hashivault.handle_auth(**kwargs)
method_mock.assert_called_with(**kwargs, auth_param=auth_params)
assert token == 'the_token'


def test_hashivault_handle_auth_not_enough_args():
with pytest.raises(Exception):
hashivault.handle_auth()
Expand Down
28 changes: 24 additions & 4 deletions docs/docsite/rst/userguide/credential_plugins.rst
Original file line number Diff line number Diff line change
Expand Up @@ -265,10 +265,21 @@ When **HashiCorp Vault Secret Lookup** is selected for **Credential Type**, prov
- **CA Certificate**: specify the CA certificate used to verify HashiCorp's server
- **Approle Role_ID**: specify the ID for Approle authentication
- **Approle Secret_ID**: specify the corresponding secret ID for Approle authentication
- **Path to Approle Auth**: specify a path if other than the default path of ``/approle``
- **Client Certificate**: specify a PEM-encoded client certificate when using the TLS auth method including any required intermediate certificates expected by Vault
- **Client Certificate Key**: specify a PEM-encoded certificate private key when using the TLS auth method
- **TLS Authentication Role**: specify the role or certificate name in Vault that corresponds to your client certificate when using the TLS auth method. If it is not provided, Vault will attempt to match the certificate automatically
- **Namespace name** specify the namespace name (Vault Enterprise only)
- **Kubernetes role** specify the role name when using Kubernetes authentication
- **Path to Auth**: specify a path if other than the default path of ``/approle``
- **API Version** (required): select v1 for static lookups and v2 for versioned lookups

For more detail about Approle and its fields, refer to the `Vault documentation for Approle Auth Method <https://www.vaultproject.io/docs/auth/approle>`_. Below shows an example of a configured HashiCorp Vault Secret Lookup credential.
For more detail about the Approle auth method and its fields, refer to the `Vault documentation for Approle Auth Method <https://www.vaultproject.io/docs/auth/approle>`_.

For more detail about the Kubernetes auth method and its fields, refer to the `Vault documentation for Kubernetes auth method <https://developer.hashicorp.com/vault/docs/auth/kubernetes>` _.

For more detail about the TLS certificate auth method and its fields, refer to the `Vault documentation for TLS certificates auth method <https://developer.hashicorp.com/vault/docs/auth/cert>` _.

Below shows an example of a configured HashiCorp Vault Secret Lookup credential.

.. image:: ../common/images/credentials-create-hashicorp-kv-credential.png
:alt: Example new HashiCorp Vault Secret lookup dialog
Expand All @@ -288,9 +299,18 @@ When **HashiCorp Vault Signed SSH** is selected for **Credential Type**, provide
- **CA Certificate**: specify the CA certificate used to verify HashiCorp's server
- **Approle Role_ID**: specify the ID for Approle authentication
- **Approle Secret_ID**: specify the corresponding secret ID for Approle authentication
- **Path to Approle Auth**: specify a path if other than the default path of ``/approle``
- **Client Certificate**: specify a PEM-encoded client certificate when using the TLS auth method including any required intermediate certificates expected by Vault
- **Client Certificate Key**: specify a PEM-encoded certificate private key when using the TLS auth method
- **TLS Authentication Role**: specify the role or certificate name in Vault that corresponds to your client certificate when using the TLS auth method. If it is not provided, Vault will attempt to match the certificate automatically
- **Namespace name** specify the namespace name (Vault Enterprise only)
- **Kubernetes role** specify the role name when using Kubernetes authentication
- **Path to Auth**: specify a path if other than the default path of ``/approle``

For more detail about the Approle auth method and its fields, refer to the `Vault documentation for Approle Auth Method <https://www.vaultproject.io/docs/auth/approle>`_.

For more detail about the Kubernetes auth method and its fields, refer to the `Vault documentation for Kubernetes auth method <https://developer.hashicorp.com/vault/docs/auth/kubernetes>` _.

For more detail about Approle and its fields, refer to the `Vault documentation for Approle Auth Method <https://www.vaultproject.io/docs/auth/approle>`_.
For more detail about the TLS certificate auth method and its fields, refer to the `Vault documentation for TLS certificates auth method <https://developer.hashicorp.com/vault/docs/auth/cert>` _.

Below shows an example of a configured HashiCorp SSH Secrets Engine credential.

Expand Down
2 changes: 2 additions & 0 deletions tools/docker-compose/ansible/plumb_vault.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
- name: Plumb AWX for Vault
hosts: localhost
gather_facts: False
vars:
awx_host: "https://127.0.0.1:8043"
tasks:
- include_role:
name: vault
Expand Down
18 changes: 18 additions & 0 deletions tools/docker-compose/ansible/roles/sources/defaults/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,24 @@ ldap_public_key_file: '{{ ldap_cert_dir }}/{{ ldap_public_key_file_name }}'
ldap_private_key_file: '{{ ldap_cert_dir }}/{{ ldap_private_key_file_name }}'
ldap_cert_subject: "/C=US/ST=NC/L=Durham/O=awx/CN="

# Hashicorp Vault
enable_vault: false
vault_tls: false
hashivault_cert_dir: '{{ sources_dest }}/vault_certs'
hashivault_server_cert_subject: "/C=US/ST=NC/L=Durham/O=awx/CN=tools-vault-1"
hashivault_server_cert_extensions:
- "subjectAltName = DNS:tools_vault_1, DNS:localhost"
- "keyUsage = digitalSignature, nonRepudiation"
- "extendedKeyUsage = serverAuth"
hashivault_client_cert_extensions:
- "subjectAltName = DNS:awx-vault-client"
- "keyUsage = digitalSignature, nonRepudiation"
- "extendedKeyUsage = serverAuth, clientAuth"
hashivault_client_cert_subject: "/C=US/ST=NC/L=Durham/O=awx/CN=awx-vault-client"
hashivault_server_public_keyfile: '{{ hashivault_cert_dir }}/server.crt'
hashivault_server_private_keyfile: '{{ hashivault_cert_dir }}/server.key'
hashivault_client_public_keyfile: '{{ hashivault_cert_dir }}/client.crt'
hashivault_client_private_keyfile: '{{ hashivault_cert_dir }}/client.key'
# Metrics
enable_splunk: false
enable_grafana: false
Expand Down
4 changes: 4 additions & 0 deletions tools/docker-compose/ansible/roles/sources/tasks/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,10 @@
include_tasks: ldap.yml
when: enable_ldap | bool

- name: Include vault TLS tasks if enabled
include_tasks: vault_tls.yml
when: enable_vault | bool

- name: Render Docker-Compose
template:
src: docker-compose.yml.j2
Expand Down
31 changes: 31 additions & 0 deletions tools/docker-compose/ansible/roles/sources/tasks/vault_tls.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
- name: Create Certificates for HashiCorp Vault
block:
- name: Create Hashicorp Vault cert directory
file:
path: "{{ hashivault_cert_dir }}"
state: directory

- name: Generate vault server certificate
command: 'openssl req -new -newkey rsa:2048 -x509 -days 365 -nodes -out {{ hashivault_server_public_keyfile }} -keyout {{ hashivault_server_private_keyfile }} -subj "{{ hashivault_server_cert_subject }}"{% for ext in hashivault_server_cert_extensions %} -addext "{{ ext }}"{% endfor %}'
args:
creates: "{{ hashivault_server_public_keyfile }}"

- name: Generate vault test client certificate
command: 'openssl req -new -newkey rsa:2048 -x509 -days 365 -nodes -out {{ hashivault_client_public_keyfile }} -keyout {{ hashivault_client_private_keyfile }} -subj "{{ hashivault_client_cert_subject }}"{% for ext in hashivault_client_cert_extensions %} -addext "{{ ext }}"{% endfor %}'
args:
creates: "{{ hashivault_client_public_keyfile }}"

- name: Set mode for vault certificates
ansible.builtin.file:
path: "{{ hashivault_cert_dir }}"
recurse: true
state: directory
mode: 0777
when: vault_tls | bool

- name: Delete Certificates for HashiCorp Vault
file:
path: "{{ hashivault_cert_dir }}"
state: absent
when: vault_tls | bool == false
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,7 @@ services:
privileged: true
{% endfor %}
{% endif %}
{% if enable_vault|bool %}
{% if enable_vault | bool %}
vault:
image: hashicorp/vault:1.14
container_name: tools_vault_1
Expand All @@ -261,10 +261,17 @@ services:
ports:
- "1234:1234"
environment:
{% if vault_tls | bool %}
VAULT_LOCAL_CONFIG: '{"storage": {"file": {"path": "/vault/file"}}, "listener": [{"tcp": { "address": "0.0.0.0:1234", "tls_disable": false, "tls_cert_file": "/vault/tls/server.crt", "tls_key_file": "/vault/tls/server.key"}}], "default_lease_ttl": "168h", "max_lease_ttl": "720h", "ui": true}'
{% else %}
VAULT_LOCAL_CONFIG: '{"storage": {"file": {"path": "/vault/file"}}, "listener": [{"tcp": { "address": "0.0.0.0:1234", "tls_disable": true}}], "default_lease_ttl": "168h", "max_lease_ttl": "720h", "ui": true}'
{% endif %}
cap_add:
- IPC_LOCK
volumes:
{% if vault_tls | bool %}
- '../../docker-compose/_sources/vault_certs:/vault/tls'
{% endif %}
- 'hashicorp_vault_data:/vault/file'
{% endif %}

Expand Down
5 changes: 5 additions & 0 deletions tools/docker-compose/ansible/roles/vault/defaults/main.yml
Original file line number Diff line number Diff line change
@@ -1,2 +1,7 @@
---
vault_file: "{{ sources_dest }}/secrets/vault_init.yml"
admin_password_file: "{{ sources_dest }}/secrets/admin_password.yml"
vault_cert_dir: '{{ sources_dest }}/vault_certs'
vault_server_cert: "{{ vault_cert_dir }}/server.crt"
vault_client_cert: "{{ vault_cert_dir }}/client.crt"
vault_client_key: "{{ vault_cert_dir }}/client.key"
53 changes: 47 additions & 6 deletions tools/docker-compose/ansible/roles/vault/tasks/initialize.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
---
- name: Set vault_addr
include_tasks: set_vault_addr.yml

- block:
- name: Start the vault
community.docker.docker_compose:
Expand All @@ -12,9 +15,16 @@
command: vault operator init
container: tools_vault_1
env:
VAULT_ADDR: "http://127.0.0.1:1234"
VAULT_ADDR: "{{ vault_addr }}"
VAULT_SKIP_VERIFY: "true"
register: vault_initialization
ignore_errors: true
failed_when:
- vault_initialization.rc != 0
- vault_initialization.stderr.find("Vault is already initialized") == -1
changed_when:
- vault_initialization.rc == 0
retries: 5
delay: 5

- name: Write out initialization file
copy:
Expand All @@ -34,21 +44,52 @@
name: vault
tasks_from: unseal.yml

- name: Configure the vault with cert auth
block:
- name: Create a cert auth mount
flowerysong.hvault.write:
path: "sys/auth/cert"
vault_addr: "{{ vault_addr_from_host }}"
validate_certs: false
token: "{{ Initial_Root_Token }}"
data:
type: "cert"
register: vault_auth_cert
failed_when:
- vault_auth_cert.result.errors | default([]) | length > 0
- "'path is already in use at cert/' not in vault_auth_cert.result.errors | default([])"
changed_when:
- vault_auth_cert.result.errors | default([]) | length == 0

- name: Configure client certificate
flowerysong.hvault.write:
path: "auth/cert/certs/awx-client"
vault_addr: "{{ vault_addr_from_host }}"
validate_certs: false
token: "{{ Initial_Root_Token }}"
data:
name: awx-client
certificate: "{{ lookup('ansible.builtin.file', '{{ vault_client_cert }}') }}"
policies:
- root
when: vault_tls | bool

- name: Create an engine
flowerysong.hvault.engine:
path: "my_engine"
type: "kv"
vault_addr: "http://localhost:1234"
vault_addr: "{{ vault_addr_from_host }}"
validate_certs: false
token: "{{ Initial_Root_Token }}"
register: engine

- name: Create a secret
- name: Create a demo secret
flowerysong.hvault.kv:
mount_point: "my_engine/my_root"
key: "my_folder"
value:
my_key: "this_is_the_secret_value"
vault_addr: "http://localhost:1234"
vault_addr: "{{ vault_addr_from_host }}"
validate_certs: false
token: "{{ Initial_Root_Token }}"

always:
Expand Down
Loading
Loading