diff --git a/.gitignore b/.gitignore index 5c199eb..9893533 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ # ---> Ansible *.retry - +ansible/hosts.yml +ansible/group_vars/all/* +ansible/roles/20-common-20-ssh/defaults/* +ansible/roles/20-common-20-ssh/files/root/.ssh/* +!*.example diff --git a/README.md b/README.md index c0aade5..ad4ee47 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,41 @@ # ansible-mail-infra -Set up infrastructure for all things e-mail \ No newline at end of file +Set up infrastructure for all things e-mail + +## Prep + +On your Ansible controller make sure the `sshpass` binary exists if Ansible has to connect to target machines via SSH username-password authentication instead of SSH key authentication. The binary usually comes with a package of the same name. + +We're assuming that you're running Ansible as a Python package inside a virtual environment. Install Ansible like so: +``` +pip install ansible +``` + +We're also assuming that secrets are stored in a HashiCorp Vault instance to which you have access. These role access Vault via `hvac`, the HashiCorp Vault API client for Python 3.x, see [github.com/hvac/hvac](https://github.com/hvac/hvac) for reference. Install it like so: +``` +pip install hvac +``` + +## Vars + +* For Vault access copy [ansible/roles/20-common-20-ssh/defaults/main.yml.example](ansible/roles/20-common-20-ssh/defaults/main.yml.example) to a proper `ansible/roles/20-common-20-ssh/defaults/main.yml` and set Vault credentials and locations as needed. + +* Create your inventory, copy [ansible/hosts.yml.example](ansible/hosts.yml.example) into a proper `ansible/hosts.yml` file with at least one host in host group `all`. + +* Replace [ansible/group_vars/all/vars.yml.example](ansible/group_vars/all/vars.yml.example) with a proper `ansible/group_vars/all/vars.yml` file and set at least `ansible_user`. It defaults to `ansible_user: 'root'`. + +* In [ansible/roles/20-common-20-ssh/files/root/.ssh](ansible/roles/20-common-20-ssh/files/root/.ssh) copy both [authorized_keys.example](ansible/roles/20-common-20-ssh/files/root/.ssh/authorized_keys.example) and [known_hosts.example](ansible/roles/20-common-20-ssh/files/root/.ssh/known_hosts.example) to proper files. They contain SSH authorized_keys and public SSH host keys you want installed on target machines. + +## Run it + +On first run execute it like so: +``` +ansible-playbook --tags 'first_run' --inventory hosts.yml playbook.yml +``` + +The `first_run` tag ensures that `ansible_password` variable gets set which in turn causes Ansible to log in to target machines via SSH username-password authentication. + +On subsequent runs like so: +``` +ansible-playbook --inventory hosts.yml playbook.yml +``` diff --git a/ansible/group_vars/all/vars.yml.example b/ansible/group_vars/all/vars.yml.example new file mode 100644 index 0000000..13b092a --- /dev/null +++ b/ansible/group_vars/all/vars.yml.example @@ -0,0 +1 @@ +ansible_user: 'root' diff --git a/ansible/hosts.yml.example b/ansible/hosts.yml.example new file mode 100644 index 0000000..ca58c21 --- /dev/null +++ b/ansible/hosts.yml.example @@ -0,0 +1,3 @@ +all: + hosts: + fully.qualified.domain.name: diff --git a/ansible/playbook.yml b/ansible/playbook.yml new file mode 100644 index 0000000..a35069d --- /dev/null +++ b/ansible/playbook.yml @@ -0,0 +1,4 @@ +- name: 'Set up SSH' + hosts: all + roles: + - role: 20-common-20-ssh diff --git a/ansible/roles/20-common-20-change-local-account-password/tasks/main.yml b/ansible/roles/20-common-20-change-local-account-password/tasks/main.yml new file mode 100644 index 0000000..e69de29 diff --git a/ansible/roles/20-common-20-ssh/defaults/main.yml.example b/ansible/roles/20-common-20-ssh/defaults/main.yml.example new file mode 100644 index 0000000..b9e710f --- /dev/null +++ b/ansible/roles/20-common-20-ssh/defaults/main.yml.example @@ -0,0 +1,6 @@ +root_home_dir_abs: '/root' +ansible_hashi_vault_auth_method: 'token' +ansible_hashi_vault_token: 'hvs.xxxxxxxxxx' +ansible_hashi_vault_engine_mount_point: 'kv' +ansible_hashi_vault_token_validate: 'false' +ansible_hashi_vault_url: 'http://localhost:8200/' diff --git a/ansible/roles/20-common-20-ssh/files/etc/ssh/sshd_config b/ansible/roles/20-common-20-ssh/files/etc/ssh/sshd_config new file mode 100644 index 0000000..7c40eed --- /dev/null +++ b/ansible/roles/20-common-20-ssh/files/etc/ssh/sshd_config @@ -0,0 +1,6 @@ +PubkeyAuthentication yes +PasswordAuthentication no +PermitEmptyPasswords no +UsePAM no +PermitRootLogin without-password +PrintMotd yes diff --git a/ansible/roles/20-common-20-ssh/files/root/.ssh/authorized_keys.example b/ansible/roles/20-common-20-ssh/files/root/.ssh/authorized_keys.example new file mode 100644 index 0000000..9f10465 --- /dev/null +++ b/ansible/roles/20-common-20-ssh/files/root/.ssh/authorized_keys.example @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZ... comment diff --git a/ansible/roles/20-common-20-ssh/files/root/.ssh/known_hosts.example b/ansible/roles/20-common-20-ssh/files/root/.ssh/known_hosts.example new file mode 100644 index 0000000..686bf91 --- /dev/null +++ b/ansible/roles/20-common-20-ssh/files/root/.ssh/known_hosts.example @@ -0,0 +1 @@ +host.name ssh-rsa AAAAB3NzaC1... diff --git a/ansible/roles/20-common-20-ssh/handlers/main.yml b/ansible/roles/20-common-20-ssh/handlers/main.yml new file mode 100644 index 0000000..685c3ae --- /dev/null +++ b/ansible/roles/20-common-20-ssh/handlers/main.yml @@ -0,0 +1,5 @@ +- name: 'Restart sshd' + listen: 'Restart sshd.service' + ansible.builtin.service: + name: 'sshd.service' + state: 'restarted' diff --git a/ansible/roles/20-common-20-ssh/tasks/20-ssh.yml b/ansible/roles/20-common-20-ssh/tasks/20-ssh.yml new file mode 100644 index 0000000..01e9689 --- /dev/null +++ b/ansible/roles/20-common-20-ssh/tasks/20-ssh.yml @@ -0,0 +1,85 @@ +- name: 'Get secrets' + no_log: 'true' + loop_control: + loop_var: 'server' + with_community.hashi_vault.vault_kv2_get: + - '{{ inventory_hostname | split(".") | reverse | join("/") }}/os/root/creds' + ansible.builtin.set_fact: + vault_data: '{{ server.secret }}' + + + +- name: 'If first run: set SSH password' + tags: + - 'first_run' + - 'never' + no_log: 'true' + ansible.builtin.set_fact: + ansible_password: '{{ vault_data.initial_password }}' + + + +- name: 'Make sure ''{{ root_home_dir_abs }}/.ssh'' exists with correct permissions' + file: + path: '{{ root_home_dir_abs }}/.ssh' + state: 'directory' + mode: 'u=rwX,go=' + owner: '{{ ansible_user }}' + group: '{{ ansible_user }}' + recurse: 'yes' + + + +- name: 'Copy ''authorized_keys'' file to server' + copy: + src: 'root/.ssh/authorized_keys' + dest: '{{ root_home_dir_abs }}/.ssh/authorized_keys' + mode: '0600' + owner: '{{ ansible_user }}' + group: '{{ ansible_user }}' + + + +- name: 'Copy ''known_hosts'' file to server' + copy: + src: 'root/.ssh/known_hosts' + dest: '{{ root_home_dir_abs }}/.ssh/known_hosts' + mode: '0600' + owner: '{{ ansible_user }}' + group: '{{ ansible_user }}' + + + +- name: 'If on Red Hat or derivative OS: secure sshd' + register: 'rv_secure_sshd' + when: '(ansible_facts[''os_family''] | lower == ''redhat'')' + blockinfile: + block: "{{ lookup('file', 'etc/ssh/sshd_config') }}" + dest: "/etc/ssh/sshd_config" + state: 'present' + insertbefore: 'BOF' + marker: '{mark}' + marker_begin: '####### Managed remotely via config management ####### quico-ops start' + marker_end: '####### Managed remotely via config management ####### quico-ops end' + validate: '/usr/sbin/sshd -T -f %s' + notify: + - 'Restart sshd.service' + + + +- name: 'Flush handlers' + meta: flush_handlers + + + +- name: 'Reset connection' + ansible.builtin.meta: 'reset_connection' + + + +- name: 'Wait for SSH connection to return' + when: '(rv_secure_sshd.changed)' + ansible.builtin.wait_for_connection: + connect_timeout: '1' + delay: '1' + sleep: '2' diff --git a/ansible/roles/20-common-20-ssh/tasks/main.yml b/ansible/roles/20-common-20-ssh/tasks/main.yml new file mode 100644 index 0000000..f80e5e4 --- /dev/null +++ b/ansible/roles/20-common-20-ssh/tasks/main.yml @@ -0,0 +1,3 @@ +- import_tasks: '20-ssh.yml' + tags: + - 'first_run'