diff --git a/Makefile b/Makefile index 2add3dafc3..ca3b134728 100644 --- a/Makefile +++ b/Makefile @@ -532,6 +532,9 @@ docker-compose-sources: .git/hooks/pre-commit $(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); $(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 @@ -583,7 +586,7 @@ docker-clean: -$(foreach image_id,$(shell docker images --filter=reference='*/*/*awx_devel*' --filter=reference='*/*awx_devel*' --filter=reference='*awx_devel*' -aq),docker rmi --force $(image_id);) docker-clean-volumes: docker-compose-clean docker-compose-container-group-clean - docker volume rm -f tools_awx_db tools_grafana_storage tools_prometheus_storage $(docker volume ls --filter name=tools_redis_socket_ -q) + docker volume rm -f tools_awx_db tools_vault_1 tools_grafana_storage tools_prometheus_storage $(docker volume ls --filter name=tools_redis_socket_ -q) docker-refresh: docker-clean docker-compose diff --git a/tools/docker-compose/README.md b/tools/docker-compose/README.md index 4670690672..7634059fd6 100644 --- a/tools/docker-compose/README.md +++ b/tools/docker-compose/README.md @@ -505,32 +505,45 @@ Run a HashiVault container alongside of AWX. VAULT=true make docker-compose ``` -Go to `http://localhost:1234` sign in with method "Token". +You can find the initialization data at `tools/docker-compose/_sources/secrets/vault_init.yml`, +This includes the unseal keys and a root token. -You can find the generated token at `tools/docker-compose/_sources/secrets/vault_password.yml`, -this is a root token, and it should not need a corresponding username. -Note that the token will be different on each restart, as it is re-generated by the playbook, -and the container does not use a persistent volume. +You will need to unseal the HashiVault each time the container is started. +The easiest way to do that is to run: +```bash +ansible-playbook tools/docker-compose/ansible/unseal_vault.yml +``` +This will perform the unseal and also display the root token for login. -As a demo, click "Enable new engine +", click "KV" and Next. -In the "Path" enter "my_engine" and click "Enable Engine". -Click on the name of the engine and then "Create secret +". -In the "Path for this secret" enter "my_root/my_folder" and in the "Secret Data" put "my_key" for key and **"my_value"** for value. +For demo purposes, Vault will be auto-configured to include a Key Value (KV) vault called `my_engine` along with a secret called `my_key` in `/my_engine/my_root/my_folder`. +The secret value is `this_is_the_secret_value`. -Then go to AWX and create a new HashiVault credential with the generated token. -Then go to any other arbitrary credential and click the key icon on an input to use a credential lookup plugin. -In the "External Secret Management System" menu, first select the already-created HashiVault credential. +To create a secret connected to this vault in AWX you can run the following playbook: +```bash +export CONTROLLER_USERNAME= +export CONTROLLER_PASSWORD= +ansible-playbook tools/docker-compose/ansible/plumb_vault.yml +``` -Then in the "Metadata" menu, put in this data which is important for the integration: - - Name of Secret Backend: "my_engine" - - Path to Secret: "data/my_root/my_folder" - - Key Name: "my_key" +This will create the following items in your AWX instance: +* A credential called `Vault Lookup Cred` tied to the vault instance. +* A custom credential type called `Vault Custom Cred Type`. +* A credential called `Credential From Vault` which is of the created type using the `Vault Lookup Cred` to get the password. -After this, apply the credential to a job template that writes the data in a debug task. -In the job output, you should see **my_value**. +The custom credential type adds a variable when used in a playbook called `the_secret_from_vault`. +If you have a playbook like: +``` +--- +- name: Show a vault secret + hosts: localhost + connection: local + gather_facts: False + tasks: + - debug: + var: the_secret_from_vault +``` -(NOTE: the "arbitrary credential" could be a new custom credential type that injects to extra vars -which is used in corresponding playbook that prints hostvars, but this doc assumes you know how to do that) +And run it through AWX with the credential `Credential From Vault` tied to it, the debug should result in `this_is_the_secret_value` The extremely non-obvious input is the fact that the fact prefixes "data/" unexpectedly. This was discovered by inspecting the secret with the vault CLI, which may help with future troubleshooting. diff --git a/tools/docker-compose/ansible/host_vars/localhost.yml b/tools/docker-compose/ansible/host_vars/localhost.yml new file mode 100644 index 0000000000..fae4e0bb67 --- /dev/null +++ b/tools/docker-compose/ansible/host_vars/localhost.yml @@ -0,0 +1,2 @@ +--- +sources_dest: '../_sources' diff --git a/tools/docker-compose/ansible/initialize_containers.yml b/tools/docker-compose/ansible/initialize_containers.yml new file mode 100644 index 0000000000..39d234ec90 --- /dev/null +++ b/tools/docker-compose/ansible/initialize_containers.yml @@ -0,0 +1,10 @@ +--- +- name: Run any pre-hooks for other container + hosts: localhost + gather_facts: false + tasks: + - name: Initialize vault + include_role: + name: vault + tasks_from: initialize + when: enable_vault | bool diff --git a/tools/docker-compose/ansible/plumb_vault.yml b/tools/docker-compose/ansible/plumb_vault.yml new file mode 100644 index 0000000000..a9cadf1554 --- /dev/null +++ b/tools/docker-compose/ansible/plumb_vault.yml @@ -0,0 +1,8 @@ +--- +- name: Plumb AWX for Vault + hosts: localhost + gather_facts: False + tasks: + - include_role: + name: vault + tasks_from: plumb diff --git a/tools/docker-compose/ansible/requirements.yml b/tools/docker-compose/ansible/requirements.yml new file mode 100644 index 0000000000..e944dd807e --- /dev/null +++ b/tools/docker-compose/ansible/requirements.yml @@ -0,0 +1,5 @@ +--- +collections: + - awx.awx + - flowerysong.hvault + - community.docker diff --git a/tools/docker-compose/ansible/roles/sources/defaults/main.yml b/tools/docker-compose/ansible/roles/sources/defaults/main.yml index cf5a65e0b6..03e7d05bd3 100644 --- a/tools/docker-compose/ansible/roles/sources/defaults/main.yml +++ b/tools/docker-compose/ansible/roles/sources/defaults/main.yml @@ -1,5 +1,4 @@ --- -sources_dest: '../_sources' compose_name: 'docker-compose.yml' awx_image: 'ghcr.io/ansible/awx_devel' pg_port: 5432 diff --git a/tools/docker-compose/ansible/roles/sources/tasks/main.yml b/tools/docker-compose/ansible/roles/sources/tasks/main.yml index 9dac12030a..417807f02b 100644 --- a/tools/docker-compose/ansible/roles/sources/tasks/main.yml +++ b/tools/docker-compose/ansible/roles/sources/tasks/main.yml @@ -101,10 +101,6 @@ include_tasks: ldap.yml when: enable_ldap | bool -- name: Include Vault tasks if enabled - include_tasks: vault.yaml - when: enable_vault | bool - - name: Render Docker-Compose template: src: docker-compose.yml.j2 diff --git a/tools/docker-compose/ansible/roles/sources/tasks/vault.yaml b/tools/docker-compose/ansible/roles/sources/tasks/vault.yaml deleted file mode 100644 index 0f71fc50bb..0000000000 --- a/tools/docker-compose/ansible/roles/sources/tasks/vault.yaml +++ /dev/null @@ -1,20 +0,0 @@ ---- -- name: create vault secret file and scope into ansible-runtime - block: - - ansible.builtin.stat: - path: "{{ sources_dest }}/secrets/{{ item }}.yml" - register: vault_secret - loop: - - vault_password - - - ansible.builtin.template: - src: "secrets.yml.j2" - dest: "{{ sources_dest }}/secrets/{{ item.item }}.yml" - mode: "0600" - loop: "{{ vault_secret.results }}" - loop_control: - label: "{{ item.item }}" - - - include_vars: "{{ sources_dest }}/secrets/{{ item.item }}.yml" - loop: "{{ vault_secret.results }}" - no_log: true diff --git a/tools/docker-compose/ansible/roles/sources/templates/docker-compose.yml.j2 b/tools/docker-compose/ansible/roles/sources/templates/docker-compose.yml.j2 index 8b9acc4c9f..b02bea9ade 100644 --- a/tools/docker-compose/ansible/roles/sources/templates/docker-compose.yml.j2 +++ b/tools/docker-compose/ansible/roles/sources/templates/docker-compose.yml.j2 @@ -235,16 +235,18 @@ services: {% endif %} {% if enable_vault|bool %} vault: - image: hashicorp/vault:latest + image: hashicorp/vault:1.14 container_name: tools_vault_1 + command: server hostname: vault ports: - "1234:1234" environment: - VAULT_DEV_ROOT_TOKEN_ID: "{{ vault_password }}" - VAULT_DEV_LISTEN_ADDRESS: "0.0.0.0:1234" + 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}' cap_add: - IPC_LOCK + volumes: + - 'hashicorp_vault_data:/vault/file' {% endif %} volumes: @@ -260,6 +262,10 @@ volumes: name: tools_ldap_1 driver: local {% endif %} +{% if enable_vault|bool %} + hashicorp_vault_data: + name: tools_vault_1 +{% endif %} {% if enable_prometheus|bool %} prometheus_storage: name: tools_prometheus_storage diff --git a/tools/docker-compose/ansible/roles/vault/defaults/main.yml b/tools/docker-compose/ansible/roles/vault/defaults/main.yml new file mode 100644 index 0000000000..7aac7ecf60 --- /dev/null +++ b/tools/docker-compose/ansible/roles/vault/defaults/main.yml @@ -0,0 +1,2 @@ +--- +vault_file: "{{ sources_dest }}/secrets/vault_init.yml" diff --git a/tools/docker-compose/ansible/roles/vault/tasks/initialize.yml b/tools/docker-compose/ansible/roles/vault/tasks/initialize.yml new file mode 100644 index 0000000000..68a9de7119 --- /dev/null +++ b/tools/docker-compose/ansible/roles/vault/tasks/initialize.yml @@ -0,0 +1,62 @@ +--- +- name: See if vault has been initialized + ansible.builtin.stat: + path: "{{ vault_file }}" + register: vault_secret_file_info + +- block: + - name: Start the vault + community.docker.docker_compose: + state: present + services: vault + project_src: "{{ sources_dest }}" + + - name: Run the initialization + community.docker.docker_container_exec: + command: vault operator init + container: tools_vault_1 + env: + VAULT_ADDR: "http://127.0.0.1:1234" + register: vault_initialization + + - name: Write out initialization file + copy: + # lines 1-4 are the keys, 6 is the root token + content: | + {{ vault_initialization.stdout_lines[0] | regex_replace('Unseal Key ', 'Unseal_Key_') }} + {{ vault_initialization.stdout_lines[1] | regex_replace('Unseal Key ', 'Unseal_Key_') }} + {{ vault_initialization.stdout_lines[2] | regex_replace('Unseal Key ', 'Unseal_Key_') }} + {{ vault_initialization.stdout_lines[3] | regex_replace('Unseal Key ', 'Unseal_Key_') }} + {{ vault_initialization.stdout_lines[4] | regex_replace('Unseal Key ', 'Unseal_Key_') }} + {{ vault_initialization.stdout_lines[6] | regex_replace('Initial Root Token', 'Initial_Root_Token') }} + dest: "{{ vault_file }}" + + - name: Unlock the vault + include_role: + name: vault + tasks_from: unseal.yml + + - name: Create an engine + flowerysong.hvault.engine: + path: "my_engine" + type: "kv" + vault_addr: "http://localhost:1234" + token: "{{ Initial_Root_Token }}" + register: engine + + - name: Create a 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" + token: "{{ Initial_Root_Token }}" + + always: + - name: Stop the vault + community.docker.docker_compose: + state: absent + project_src: "{{ sources_dest }}" + + when: not vault_secret_file_info.stat.exists diff --git a/tools/docker-compose/ansible/roles/vault/tasks/plumb.yml b/tools/docker-compose/ansible/roles/vault/tasks/plumb.yml new file mode 100644 index 0000000000..8690050f74 --- /dev/null +++ b/tools/docker-compose/ansible/roles/vault/tasks/plumb.yml @@ -0,0 +1,56 @@ +--- +- name: Load vault keys + include_vars: + file: "{{ vault_file }}" + +- name: Create a HashiCorp Vault Credential + awx.awx.credential: + credential_type: HashiCorp Vault Secret Lookup + name: Vault Lookup Cred + organization: Default + inputs: + api_version: "v1" + cacert: "" + default_auth_path: "approle" + kubernetes_role: "" + namespace: "" + role_id: "" + secret_id: "" + token: "{{ Initial_Root_Token }}" + url: "http://tools_vault_1:1234" + register: vault_cred + +- name: Create a custom credential type + awx.awx.credential_type: + name: Vault Custom Cred Type + kind: cloud + injectors: + extra_vars: + the_secret_from_vault: "{{ '{{' }} password {{ '}}' }}" + inputs: + fields: + - type: "string" + id: "password" + label: "Password" + secret: true + register: custom_vault_cred_type + +- name: Create a credential of the custom type + awx.awx.credential: + credential_type: "{{ custom_vault_cred_type.id }}" + name: Credential From Vault + inputs: {} + organization: Default + register: custom_credential + +- name: Use the Vault Credential For the new credential + awx.awx.credential_input_source: + input_field_name: password + target_credential: "{{ custom_credential.id }}" + source_credential: "{{ vault_cred.id }}" + metadata: + auth_path: "" + secret_backend: "my_engine" + secret_key: "my_key" + secret_path: "/my_root/my_folder" + secret_version: "" diff --git a/tools/docker-compose/ansible/roles/vault/tasks/unseal.yml b/tools/docker-compose/ansible/roles/vault/tasks/unseal.yml new file mode 100644 index 0000000000..e0cb3c4a2a --- /dev/null +++ b/tools/docker-compose/ansible/roles/vault/tasks/unseal.yml @@ -0,0 +1,14 @@ +--- +- name: Load vault keys + include_vars: + file: "{{ vault_file }}" + +- name: Unseal the vault + flowerysong.hvault.seal: + vault_addr: "http://localhost:1234" + state: unsealed + key: "{{ item }}" + loop: + - "{{ Unseal_Key_1 }}" + - "{{ Unseal_Key_2 }}" + - "{{ Unseal_Key_3 }}" diff --git a/tools/docker-compose/ansible/unseal_vault.yml b/tools/docker-compose/ansible/unseal_vault.yml new file mode 100644 index 0000000000..6f1c412104 --- /dev/null +++ b/tools/docker-compose/ansible/unseal_vault.yml @@ -0,0 +1,13 @@ +--- +- name: Run tasks post startup + hosts: localhost + gather_facts: False + tasks: + - name: Unseal the vault + include_role: + name: vault + tasks_from: unseal + + - name: Display root token + debug: + var: Initial_Root_Token