├── .ansible-lint ├── .github └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .yamllint ├── Dockerfile ├── LICENSE.txt ├── README.md ├── Vagrantfile ├── defaults └── main.yml ├── files └── empty ├── handlers └── main.yml ├── meta └── main.yml ├── molecule └── default │ ├── collections.yml │ ├── converge.yml │ ├── molecule.yml │ ├── prepare.yml │ └── verify.yml ├── requirements.yml ├── tasks ├── authorized-keys.yml ├── general.yml ├── generate.yml ├── known-hosts.yml ├── main.yml ├── private-keys.yml └── public-keys.yml ├── templates └── etc │ └── ssh │ └── ssh_known_hosts.j2 ├── tests ├── inventory ├── tasks │ ├── post.yml │ └── pre.yml ├── test.yml ├── vagrant.yml └── vars │ └── main.yml └── vars └── main.yml /.ansible-lint: -------------------------------------------------------------------------------- 1 | --- 2 | warn_list: 3 | - role-name 4 | - name[play] 5 | - name[casing] 6 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: CI 3 | 'on': 4 | pull_request: 5 | push: 6 | branches: 7 | - master 8 | schedule: 9 | - cron: '30 1 * * 3' 10 | 11 | jobs: 12 | 13 | lint: 14 | name: Lint 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Check out the codebase 18 | uses: actions/checkout@v3 19 | 20 | - name: Set up Python 3 21 | uses: actions/setup-python@v4 22 | with: 23 | python-version: '3.x' 24 | 25 | - name: Install test dependencies 26 | run: | 27 | pip install ansible-lint 28 | ansible-galaxy install -r requirements.yml 29 | 30 | - name: Lint code 31 | run: | 32 | yamllint . 33 | ansible-lint 34 | 35 | molecule: 36 | name: Molecule 37 | runs-on: ubuntu-latest 38 | defaults: 39 | run: 40 | working-directory: "${{ github.repository }}" 41 | needs: 42 | - lint 43 | strategy: 44 | fail-fast: false 45 | matrix: 46 | include: 47 | - distro: debian10 48 | ansible-version: '>=9, <10' 49 | - distro: debian11 50 | - distro: debian12 51 | - distro: ubuntu1804 52 | ansible-version: '>=9, <10' 53 | - distro: ubuntu2004 54 | - distro: ubuntu2204 55 | - distro: ubuntu2404 56 | 57 | steps: 58 | - name: Check out the codebase 59 | uses: actions/checkout@v3 60 | with: 61 | path: "${{ github.repository }}" 62 | 63 | - name: Set up Python 3 64 | uses: actions/setup-python@v4 65 | with: 66 | python-version: '3.x' 67 | 68 | - name: Install test dependencies 69 | run: | 70 | pip install 'ansible${{ matrix.ansible-version }}' molecule-plugins[docker] docker 71 | - name: Run Molecule tests 72 | run: | 73 | molecule test 74 | env: 75 | ANSIBLE_FORCE_COLOR: '1' 76 | ANSIBLE_VERBOSITY: '2' 77 | MOLECULE_DEBUG: '1' 78 | MOLECULE_DISTRO: "${{ matrix.distro }}" 79 | PY_COLORS: '1' 80 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Release 3 | 'on': 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | 10 | release: 11 | name: Release 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Check out the codebase 15 | uses: actions/checkout@v3 16 | 17 | - name: Publish to Galaxy 18 | uses: robertdebock/galaxy-action@1.2.0 19 | with: 20 | galaxy_api_key: ${{ secrets.GALAXY_API_KEY }} 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OS generated files # 2 | ###################### 3 | .DS_Store 4 | .DS_Store? 5 | ._* 6 | .Spotlight-V100 7 | .Trashes 8 | Icon? 9 | ehthumbs.db 10 | Thumbs.db 11 | 12 | # IDE files # 13 | ################# 14 | /.settings 15 | /.buildpath 16 | /.project 17 | /nbproject 18 | *.komodoproject 19 | *.kpf 20 | /.idea 21 | 22 | # Vagrant files # 23 | .virtualbox/ 24 | .vagrant/ 25 | vagrant_ansible_inventory_* 26 | ansible.cfg 27 | 28 | # Other files # 29 | ############### 30 | !empty 31 | 32 | files/id_rsa* 33 | -------------------------------------------------------------------------------- /.yamllint: -------------------------------------------------------------------------------- 1 | --- 2 | extends: default 3 | 4 | rules: 5 | braces: 6 | max-spaces-inside: 1 7 | level: error 8 | brackets: 9 | max-spaces-inside: 1 10 | level: error 11 | line-length: disable 12 | truthy: disable 13 | 14 | ignore: | 15 | .tox/ 16 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:18.04 2 | MAINTAINER Mischa ter Smitten 3 | 4 | ENV LANG C.UTF-8 5 | ENV LC_ALL C.UTF-8 6 | 7 | # python 8 | RUN apt-get update && \ 9 | DEBIAN_FRONTEND=noninteractive apt-get install -y python3-minimal python3-dev curl && \ 10 | apt-get clean 11 | RUN curl -sL https://bootstrap.pypa.io/pip/3.6/get-pip.py | python3 - 12 | RUN rm -rf $HOME/.cache 13 | 14 | # ansible 15 | RUN DEBIAN_FRONTEND=noninteractive apt-get install -y python3-apt && \ 16 | apt-get clean 17 | RUN pip3 install ansible==2.10.7 18 | RUN rm -rf $HOME/.cache 19 | 20 | # provision 21 | COPY . /etc/ansible/roles/ansible-role 22 | WORKDIR /etc/ansible/roles/ansible-role 23 | RUN ansible-playbook -i tests/inventory tests/test.yml --connection=local 24 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) Oefenweb.nl 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## ssh-keys 2 | 3 | [![CI](https://github.com/Oefenweb/ansible-ssh-keys/workflows/CI/badge.svg)](https://github.com/Oefenweb/ansible-ssh-keys/actions?query=workflow%3ACI) 4 | [![Ansible Galaxy](http://img.shields.io/badge/ansible--galaxy-ssh--keys-blue.svg)](https://galaxy.ansible.com/Oefenweb/ssh_keys) 5 | 6 | Manage ssh public key authentication (public / private / authorized keys and known hosts) in Debian-like systems. 7 | 8 | #### Requirements 9 | 10 | None 11 | 12 | #### Variables 13 | 14 | * `ssh_keys_generate_keys`: [default: `[]`]: Keys to generate locally 15 | * `ssh_keys_generate_keys.{n}.path`: [required] The local path where the key should be generated 16 | * `ssh_keys_generate_keys.{n}.cipher`: [default: `None`]: The cipher to encrypt the private key 17 | * `ssh_keys_generate_keys.{n}.format`: [default: `OpenSSH`]: The format of the public key 18 | * `ssh_keys_generate_keys.{n}.force`: [default: `false`]: Whether to regenerate the key pair if it already exists 19 | * `ssh_keys_generate_keys.{n}.owner`: [required]: The name of the user that should own the key pair 20 | * `ssh_keys_generate_keys.{n}.group`: [default: `owner`]: The name of the group that should own the key pair 21 | * `ssh_keys_generate_keys.{n}.mode`: [default: `0600`]: The UNIX permission mode bits of the key pair 22 | * `ssh_keys_generate_keys.{n}.passphrase`: [default: `''`]: The passphrase for the private key 23 | * `ssh_keys_generate_keys.{n}.size`: [default: `4096`]: Size in bits of the TLS/SSL key to generate 24 | * `ssh_keys_generate_keys.{n}.type`: [default: `RSA`]: The algorithm used to generate the private key 25 | 26 | * `ssh_keys_generate_keys_command`: [optional, default: `_ssh_keys_generate_keys_command`]: 27 | * `ssh_keys_generate_keys_become`: [optional, default: `false`]: Whether or not to use `sudo` when generating ssh keys (locally) 28 | 29 | * `ssh_keys_private_keys`: [default: `[]`]: Private key declarations 30 | * `ssh_keys_private_keys.{n}.owner`: [required]: The name of the user that should own the file 31 | * `ssh_keys_private_keys.{n}.group`: [default: `owner`]: The name of the group that should own the file 32 | * `ssh_keys_private_keys.{n}.mode`: [default: `0600`]: The UNIX permission mode bits of the file 33 | * `ssh_keys_private_keys.{n}.src`: [required]: The local path of the key 34 | * `ssh_keys_private_keys.{n}.dest`: [default: `src | basename`]: The remote path of the key (relative to `home/.ssh/`) 35 | * `ssh_keys_private_keys.{n}.dest_absolute`: [optional]: The remote path of the key 36 | * `ssh_keys_private_keys.{n}.dest_managed`: [default: `true`]: Whether or not the remote path of the key should be created 37 | * `ssh_keys_private_keys.{n}.state`: [default: `present`]: State 38 | 39 | * `ssh_keys_public_keys`: [default: `[]`]: Public key declarations 40 | * `ssh_keys_public_keys.{n}.owner`: [required]: The name of the user that should own the file 41 | * `ssh_keys_public_keys.{n}.group`: [default: `owner`]: The name of the group that should own the file 42 | * `ssh_keys_public_keys.{n}.mode`: [default: `0644`]: The UNIX permission mode bits of the file 43 | * `ssh_keys_public_keys.{n}.src`: [required]: The local path of the key 44 | * `ssh_keys_public_keys.{n}.dest`: [default: `src | basename`]: The remote path of the key (relative to `home/.ssh/`) 45 | * `ssh_keys_public_keys.{n}.dest_absolute`: [optional]: The remote path of the key 46 | * `ssh_keys_public_keys.{n}.dest_managed`: [default: `true`]: Whether or not the remote path of the key should be created 47 | * `ssh_keys_public_keys.{n}.state`: [default: `present`]: State 48 | 49 | * `ssh_keys_authorized_keys`: [default: `[]`]: Authorized key declarations 50 | * `ssh_keys_authorized_keys.{n}.owner`: [required]: The name of the user that should own the file 51 | * `ssh_keys_authorized_keys.{n}.src`: [required]: The local path of the key 52 | * `ssh_keys_authorized_keys.{n}.state`: [optional, default: `present`]: State 53 | * `ssh_keys_authorized_keys.{n}.path`: [optional, default: `authorized_keys`]: Authorized keys file (absolute path, default to `~/.ssh/authorized_keys`) 54 | 55 | * `ssh_keys_known_hosts`: [default: `[]`]: Known hosts declarations 56 | * `ssh_keys_known_hosts.{n}.hostname`: [required]: The hostname 57 | * `ssh_keys_known_hosts.{n}.enctype`: [required]: The type of the fingerprint 58 | * `ssh_keys_known_hosts.{n}.fingerprint`: [required]: The actual fingerprint 59 | 60 | ## Dependencies 61 | 62 | None 63 | 64 | #### Example 65 | 66 | ```yaml 67 | --- 68 | - hosts: all 69 | roles: 70 | - oefenweb.ssh-keys 71 | vars: 72 | ssh_keys_generate_keys: 73 | - path: ../../../files/ssh-keys/id_rsa 74 | comment: RSA key 75 | ssh_keys_private_keys: 76 | - owner: root 77 | src: "{{ playbook_dir }}/files/ssh-keys/id_rsa" 78 | ssh_keys_public_keys: 79 | - owner: root 80 | src: "{{ playbook_dir }}/files/ssh-keys/id_rsa.pub" 81 | ssh_keys_authorized_keys: 82 | - owner: root 83 | src: "{{ playbook_dir }}/files/ssh-keys/id_rsa.pub" 84 | ssh_keys_known_hosts: 85 | - hostname: github.com 86 | enctype: ssh-rsa 87 | fingerprint: 'AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==' 88 | ``` 89 | 90 | #### License 91 | 92 | MIT 93 | 94 | #### Author Information 95 | 96 | * Mark van Driel 97 | * Mischa ter Smitten 98 | 99 | #### Feedback, bug-reports, requests, ... 100 | 101 | Are [welcome](https://github.com/Oefenweb/ansible-ssh-keys/issues)! 102 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby ts=2 sw=2 tw=0 et : 3 | 4 | role = File.basename(File.expand_path(File.dirname(__FILE__))) 5 | 6 | boxes = [ 7 | { 8 | :name => "ubuntu-1804", 9 | :box => "bento/ubuntu-18.04", 10 | :ip => '10.0.0.13', 11 | :cpu => "50", 12 | :ram => "384" 13 | }, 14 | { 15 | :name => "ubuntu-2004", 16 | :box => "bento/ubuntu-20.04", 17 | :ip => '10.0.0.14', 18 | :cpu => "50", 19 | :ram => "512" 20 | }, 21 | { 22 | :name => "ubuntu-2204", 23 | :box => "bento/ubuntu-22.04", 24 | :ip => '10.0.0.15', 25 | :cpu => "50", 26 | :ram => "512" 27 | }, 28 | { 29 | :name => "ubuntu-2404", 30 | :box => "bento/ubuntu-24.04", 31 | :ip => '10.0.0.16', 32 | :cpu => "50", 33 | :ram => "512" 34 | }, 35 | { 36 | :name => "debian-10", 37 | :box => "bento/debian-10", 38 | :ip => '10.0.0.18', 39 | :cpu => "50", 40 | :ram => "256" 41 | }, 42 | { 43 | :name => "debian-11", 44 | :box => "bento/debian-11", 45 | :ip => '10.0.0.19', 46 | :cpu => "50", 47 | :ram => "256" 48 | }, 49 | { 50 | :name => "debian-12", 51 | :box => "bento/debian-12", 52 | :ip => '10.0.0.20', 53 | :cpu => "50", 54 | :ram => "384" 55 | }, 56 | ] 57 | 58 | Vagrant.configure("2") do |config| 59 | boxes.each do |box| 60 | config.vm.define box[:name] do |vms| 61 | vms.vm.box = box[:box] 62 | vms.vm.hostname = "ansible-#{role}-#{box[:name]}" 63 | 64 | vms.vm.provider "virtualbox" do |v| 65 | v.customize ["modifyvm", :id, "--cpuexecutioncap", box[:cpu]] 66 | v.customize ["modifyvm", :id, "--memory", box[:ram]] 67 | end 68 | 69 | vms.vm.network :private_network, ip: box[:ip] 70 | 71 | vms.vm.provision :ansible do |ansible| 72 | ansible.playbook = "tests/vagrant.yml" 73 | ansible.verbose = "vv" 74 | end 75 | end 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /defaults/main.yml: -------------------------------------------------------------------------------- 1 | # defaults file 2 | --- 3 | ssh_keys_generate_keys: [] 4 | ssh_keys_generate_keys_become: false 5 | ssh_keys_private_keys: [] 6 | ssh_keys_public_keys: [] 7 | ssh_keys_authorized_keys: [] 8 | ssh_keys_known_hosts: [] 9 | -------------------------------------------------------------------------------- /files/empty: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oefenweb/ansible-ssh-keys/5d51afa3c5e2ddfe1f7d4b7998b4c085e472467e/files/empty -------------------------------------------------------------------------------- /handlers/main.yml: -------------------------------------------------------------------------------- 1 | # handlers file 2 | --- 3 | -------------------------------------------------------------------------------- /meta/main.yml: -------------------------------------------------------------------------------- 1 | # meta file 2 | --- 3 | galaxy_info: 4 | author: oefenweb 5 | role_name: ssh_keys 6 | company: Oefenweb.nl B.V. 7 | description: Manage ssh public key authentication (public / private / authorized keys and known hosts) in Debian-like systems 8 | license: MIT 9 | min_ansible_version: 2.10.0 10 | platforms: 11 | - name: Ubuntu 12 | versions: 13 | - bionic 14 | - focal 15 | - jammy 16 | - noble 17 | - name: Debian 18 | versions: 19 | - buster 20 | - bullseye 21 | - bookworm 22 | galaxy_tags: 23 | - system 24 | - networking 25 | dependencies: [] 26 | -------------------------------------------------------------------------------- /molecule/default/collections.yml: -------------------------------------------------------------------------------- 1 | --- 2 | collections: [] 3 | -------------------------------------------------------------------------------- /molecule/default/converge.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Converge 3 | hosts: all 4 | become: true 5 | tasks: [] 6 | roles: 7 | - ../../../ 8 | -------------------------------------------------------------------------------- /molecule/default/molecule.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependency: 3 | name: galaxy 4 | driver: 5 | name: docker 6 | platforms: 7 | - name: instance 8 | image: "geerlingguy/docker-${MOLECULE_DISTRO:-ubuntu2004}-ansible:latest" 9 | command: ${MOLECULE_DOCKER_COMMAND:-""} 10 | volumes: 11 | - /sys/fs/cgroup:/sys/fs/cgroup:rw 12 | - /var/lib/containerd 13 | cgroupns_mode: host 14 | privileged: true 15 | pre_build_image: true 16 | provisioner: 17 | name: ansible 18 | playbooks: 19 | prepare: prepare.yml 20 | converge: converge.yml 21 | verify: verify.yml 22 | -------------------------------------------------------------------------------- /molecule/default/prepare.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Prepare 3 | hosts: all 4 | become: true 5 | tasks: [] 6 | -------------------------------------------------------------------------------- /molecule/default/verify.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Verify 3 | hosts: all 4 | become: true 5 | tasks: [] 6 | -------------------------------------------------------------------------------- /requirements.yml: -------------------------------------------------------------------------------- 1 | # requirements file 2 | --- 3 | collections: 4 | - name: ansible.posix 5 | - name: community.crypto 6 | -------------------------------------------------------------------------------- /tasks/authorized-keys.yml: -------------------------------------------------------------------------------- 1 | # tasks file 2 | --- 3 | - name: authorized-keys | set up for users 4 | ansible.posix.authorized_key: 5 | user: "{{ item.owner }}" 6 | key: "{{ lookup('file', item.src) }}" 7 | state: "{{ item.state | default('present') }}" 8 | path: "{{ item.path | default('~' + item.owner + '/' + ssh_keys_sshdir + '/authorized_keys') }}" 9 | with_items: "{{ ssh_keys_authorized_keys }}" 10 | tags: 11 | ssh-keys-authorized-keys-setup 12 | -------------------------------------------------------------------------------- /tasks/general.yml: -------------------------------------------------------------------------------- 1 | # tasks file 2 | --- 3 | - name: general | create ssh directory 4 | ansible.builtin.file: 5 | path: "{{ (item.dest_absolute | default('~' + item.owner + '/' + ssh_keys_sshdir + '/' + (item.dest | default(item.src | basename)))) | dirname }}" 6 | state: directory 7 | owner: "{{ item.owner }}" 8 | group: "{{ item.group | default(item.owner) }}" 9 | mode: '0700' 10 | with_items: "{{ ssh_keys_private_keys }}" 11 | when: item.dest_managed | default(true) 12 | tags: 13 | - ssh-keys-general-create-ssh-directories 14 | - ssh-keys-general-create-ssh-directories-private-keys 15 | 16 | - name: general | create ssh directory 17 | ansible.builtin.file: 18 | path: "~{{ item.owner }}/{{ ssh_keys_sshdir }}" 19 | state: directory 20 | owner: "{{ item.owner }}" 21 | group: "{{ item.group | default(item.owner) }}" 22 | mode: '0700' 23 | with_items: "{{ ssh_keys_public_keys }}" 24 | when: item.dest_managed | default(true) 25 | tags: 26 | - ssh-keys-general-create-ssh-directories 27 | - ssh-keys-general-create-ssh-directories-public-keys 28 | -------------------------------------------------------------------------------- /tasks/generate.yml: -------------------------------------------------------------------------------- 1 | # tasks file 2 | --- 3 | - name: generate 4 | delegate_to: localhost 5 | become: "{{ ssh_keys_generate_keys_become }}" 6 | block: 7 | - name: generate | create ssh key generation directory 8 | ansible.builtin.file: 9 | path: "{{ item.path | dirname }}" 10 | owner: "{{ item.owner }}" 11 | group: "{{ item.group | default(item.owner) }}" 12 | mode: '0700' 13 | state: directory 14 | with_items: "{{ ssh_keys_generate_keys }}" 15 | tags: 16 | - ssh-keys-generate-directories 17 | 18 | - name: generate | generate private ssh key 19 | ansible.builtin.shell: > 20 | {{ ssh_keys_generate_keys_command | default(_ssh_keys_generate_keys_command) }} 21 | with_items: "{{ ssh_keys_generate_keys }}" 22 | when: >- 23 | item.force | default(false) | bool or 24 | item.path is not file 25 | tags: 26 | - ssh-keys-generate-private-keys 27 | - skip_ansible_lint 28 | 29 | - name: generate | set permissions on keys 30 | ansible.builtin.file: 31 | path: "{{ item.path }}" 32 | owner: "{{ item.owner }}" 33 | group: "{{ item.group | default(item.owner) }}" 34 | mode: "{{ item.mode | default('0600') }}" 35 | with_items: "{{ ssh_keys_generate_keys }}" 36 | tags: 37 | - ssh-keys-generate-permissions 38 | 39 | - name: generate | generate public ssh key 40 | community.crypto.openssl_publickey: 41 | path: "{{ item.path }}.pub" 42 | force: "{{ item.force | default(false) }}" 43 | format: "{{ item.format | default('OpenSSH') }}" 44 | owner: "{{ item.owner }}" 45 | group: "{{ item.group | default(item.owner) }}" 46 | mode: "{{ item.mode | default('0600') }}" 47 | privatekey_passphrase: "{{ item.passphrase | default(omit) }}" 48 | privatekey_path: "{{ item.path }}" 49 | with_items: "{{ ssh_keys_generate_keys }}" 50 | tags: 51 | - ssh-keys-generate-public-keys 52 | -------------------------------------------------------------------------------- /tasks/known-hosts.yml: -------------------------------------------------------------------------------- 1 | # tasks file 2 | --- 3 | - name: known-hosts | stat directories 4 | ansible.builtin.stat: 5 | path: "{{ item }}/" 6 | register: _stat_directories 7 | with_items: 8 | - "{{ ssh_keys_known_hosts_path | dirname }}" 9 | tags: 10 | - ssh-keys-known-hosts-directories 11 | - ssh-keys-known-hosts-directories-stat 12 | 13 | - name: known-hosts | create directories 14 | ansible.builtin.file: 15 | path: "{{ item.item }}" 16 | state: directory 17 | owner: root 18 | group: root 19 | mode: '0755' 20 | with_items: "{{ _stat_directories.results }}" 21 | when: not item.stat.exists | bool 22 | tags: 23 | - ssh-keys-known-hosts-directories 24 | - ssh-keys-known-hosts-directories-create 25 | 26 | - name: known-hosts | update file 27 | ansible.builtin.template: 28 | src: "{{ ssh_keys_known_hosts_path.lstrip('/') }}.j2" 29 | dest: "{{ ssh_keys_known_hosts_path }}" 30 | owner: root 31 | group: root 32 | mode: '0644' 33 | tags: 34 | ssh-keys-known-hosts-update 35 | -------------------------------------------------------------------------------- /tasks/main.yml: -------------------------------------------------------------------------------- 1 | # tasks file 2 | --- 3 | - name: generate 4 | ansible.builtin.import_tasks: generate.yml 5 | tags: 6 | - configuration 7 | - ssh-keys 8 | - ssh-keys-generate 9 | 10 | - name: general 11 | ansible.builtin.import_tasks: general.yml 12 | tags: 13 | - configuration 14 | - ssh-keys 15 | - ssh-keys-general 16 | 17 | - name: private-keys 18 | ansible.builtin.import_tasks: private-keys.yml 19 | tags: 20 | - configuration 21 | - ssh-keys 22 | - ssh-keys-private-keys 23 | 24 | - name: public-keys 25 | ansible.builtin.import_tasks: public-keys.yml 26 | tags: 27 | - configuration 28 | - ssh-keys 29 | - ssh-keys-public-keys 30 | 31 | - name: authorized-keys 32 | ansible.builtin.import_tasks: authorized-keys.yml 33 | tags: 34 | - configuration 35 | - ssh-keys 36 | - ssh-keys-authorized-keys 37 | 38 | - name: known-hosts 39 | ansible.builtin.import_tasks: known-hosts.yml 40 | tags: 41 | - configuration 42 | - ssh-keys 43 | - ssh-keys-known-hosts 44 | -------------------------------------------------------------------------------- /tasks/private-keys.yml: -------------------------------------------------------------------------------- 1 | # tasks file 2 | --- 3 | - name: private-keys | add 4 | ansible.builtin.copy: 5 | src: "{{ item.src }}" 6 | dest: "{{ item.dest_absolute | default('~' + item.owner + '/' + ssh_keys_sshdir + '/' + (item.dest | default(item.src | basename))) }}" 7 | owner: "{{ item.owner }}" 8 | group: "{{ item.group | default(item.owner) }}" 9 | mode: "{{ item.mode | default('0600') }}" 10 | with_items: "{{ ssh_keys_private_keys }}" 11 | when: item.state is undefined or item.state == 'present' 12 | tags: 13 | - ssh-keys-private-keys-add 14 | 15 | - name: private-keys | remove 16 | ansible.builtin.file: 17 | path: "{{ item.dest_absolute | default('~' + item.owner + '/' + ssh_keys_sshdir + '/' + (item.dest | default(item.src | basename))) }}" 18 | state: absent 19 | with_items: "{{ ssh_keys_private_keys }}" 20 | when: item.state is defined and item.state == 'absent' 21 | tags: 22 | - ssh-keys-private-keys-remove 23 | -------------------------------------------------------------------------------- /tasks/public-keys.yml: -------------------------------------------------------------------------------- 1 | # tasks file 2 | --- 3 | - name: public-keys | add 4 | ansible.builtin.copy: 5 | src: "{{ item.src }}" 6 | dest: "{{ item.dest_absolute | default('~' + item.owner + '/' + ssh_keys_sshdir + '/' + (item.dest | default(item.src | basename))) }}" 7 | owner: "{{ item.owner }}" 8 | group: "{{ item.group | default(item.owner) }}" 9 | mode: "{{ item.mode | default('0644') }}" 10 | with_items: "{{ ssh_keys_public_keys }}" 11 | when: item.state is undefined or item.state == 'present' 12 | tags: 13 | - ssh-keys-public-keys-add 14 | 15 | - name: public-keys | remove 16 | ansible.builtin.file: 17 | path: "{{ item.dest_absolute | default('~' + item.owner + '/' + ssh_keys_sshdir + '/' + (item.dest | default(item.src | basename))) }}" 18 | state: absent 19 | with_items: "{{ ssh_keys_public_keys }}" 20 | when: item.state is defined and item.state == 'absent' 21 | tags: 22 | - ssh-keys-public-keys-remove 23 | -------------------------------------------------------------------------------- /templates/etc/ssh/ssh_known_hosts.j2: -------------------------------------------------------------------------------- 1 | {% for item in ssh_keys_known_hosts %} 2 | {{ item.hostname }} {{ item.enctype }} {{ item.fingerprint }} 3 | {% endfor %} 4 | -------------------------------------------------------------------------------- /tests/inventory: -------------------------------------------------------------------------------- 1 | localhost 2 | -------------------------------------------------------------------------------- /tests/tasks/post.yml: -------------------------------------------------------------------------------- 1 | # post test file 2 | --- 3 | - name: set fact 4 | ansible.builtin.command: > 5 | echo "{{ ansible_connection }}" 6 | register: _ansible_connection 7 | changed_when: false 8 | 9 | - name: test presence of 10 | when: _ansible_connection.stdout != 'ssh' 11 | tags: 12 | - skip_ansible_lint 13 | block: 14 | - name: test presence of private key # noqa command-instead-of-module 15 | ansible.builtin.shell: > 16 | diff {{ ssh_keys_private_keys[0]['src'] }} ~{{ ssh_keys_owner }}/.ssh/{{ ssh_keys_private_keys[0]['src'] | basename }} 17 | && (echo 'Private key test: pass' && exit 0) 18 | || (echo 'Private key test: fail' && exit 1) 19 | changed_when: false 20 | tags: 21 | - skip_ansible_lint 22 | 23 | - name: test presence of public key # noqa command-instead-of-module 24 | ansible.builtin.shell: > 25 | diff {{ ssh_keys_public_keys[0]['src'] }} ~{{ ssh_keys_owner }}/.ssh/{{ ssh_keys_public_keys[0]['src'] | basename }} 26 | && (echo 'Public key test: pass' && exit 0) 27 | || (echo 'Public key test: fail' && exit 1) 28 | changed_when: false 29 | 30 | - name: test presence of known hosts # noqa command-instead-of-module 31 | ansible.builtin.shell: > 32 | grep -q 'github.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa' /etc/ssh/ssh_known_hosts 33 | && (echo 'Known hosts test: pass' && exit 0) 34 | || (echo 'Known hosts test: fail' && exit 1) 35 | changed_when: false 36 | tags: 37 | - skip_ansible_lint 38 | -------------------------------------------------------------------------------- /tests/tasks/pre.yml: -------------------------------------------------------------------------------- 1 | # pre test file 2 | --- 3 | - name: install dependencies 4 | ansible.builtin.apt: 5 | name: 6 | - openssh-client 7 | state: "{{ apt_install_state | default('latest') }}" 8 | update_cache: true 9 | cache_valid_time: "{{ apt_update_cache_valid_time | default(3600) }}" 10 | 11 | - name: generate key pair 12 | ansible.builtin.command: > 13 | ssh-keygen -t rsa -b 2048 -C '' -P '' -f {{ ssh_keys_private_key }} -q 14 | args: 15 | creates: "{{ ssh_keys_private_key }}" 16 | connection: local 17 | become: false 18 | 19 | - name: create user (if needed) 20 | ansible.builtin.user: 21 | name: "{{ ssh_keys_owner }}" 22 | -------------------------------------------------------------------------------- /tests/test.yml: -------------------------------------------------------------------------------- 1 | # test file 2 | --- 3 | - name: converge 4 | hosts: localhost 5 | connection: local 6 | become: true 7 | pre_tasks: 8 | - name: include vars 9 | ansible.builtin.include_vars: "{{ playbook_dir }}/vars/main.yml" 10 | - name: include tasks 11 | ansible.builtin.import_tasks: "{{ playbook_dir }}/tasks/pre.yml" 12 | roles: 13 | - ../../ 14 | post_tasks: 15 | - name: include tasks 16 | ansible.builtin.import_tasks: "{{ playbook_dir }}/tasks/post.yml" 17 | -------------------------------------------------------------------------------- /tests/vagrant.yml: -------------------------------------------------------------------------------- 1 | # test file 2 | --- 3 | - name: converge 4 | hosts: all 5 | remote_user: vagrant 6 | become: true 7 | pre_tasks: 8 | - name: include vars 9 | ansible.builtin.include_vars: "{{ playbook_dir }}/vars/main.yml" 10 | - name: include tasks 11 | ansible.builtin.import_tasks: "{{ playbook_dir }}/tasks/pre.yml" 12 | roles: 13 | - ../../ 14 | post_tasks: 15 | - name: include tasks 16 | ansible.builtin.import_tasks: "{{ playbook_dir }}/tasks/post.yml" 17 | -------------------------------------------------------------------------------- /tests/vars/main.yml: -------------------------------------------------------------------------------- 1 | # vars file 2 | --- 3 | ssh_keys_owner: dummy 4 | ssh_keys_private_keys: 5 | - owner: "{{ ssh_keys_owner }}" 6 | src: "{{ ssh_keys_private_key }}" 7 | ssh_keys_public_keys: 8 | - owner: "{{ ssh_keys_owner }}" 9 | src: "{{ ssh_keys_private_key }}.pub" 10 | ssh_keys_authorized_keys: 11 | - owner: "{{ ssh_keys_owner }}" 12 | src: "{{ ssh_keys_private_key }}.pub" 13 | ssh_keys_known_hosts: 14 | - hostname: github.com 15 | enctype: ssh-rsa 16 | fingerprint: 'AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==' # noqa 204 17 | 18 | ssh_keys_private_key: "{{ playbook_dir }}/../files/id_rsa" 19 | -------------------------------------------------------------------------------- /vars/main.yml: -------------------------------------------------------------------------------- 1 | # vars file 2 | --- 3 | ssh_keys_sshdir: '.ssh' 4 | ssh_keys_known_hosts_path: /etc/ssh/ssh_known_hosts 5 | _ssh_keys_generate_keys_command: >- 6 | openssl \ 7 | gen{{ item.type | default('rsa') }} \ 8 | -out {{ item.path }} \ 9 | {{ item.cipher if item.cipher is defined else '' }} \ 10 | {{ '-passout stdin' if item.passphrase is defined else '' }} \ 11 | {{ item.size | default(4096) }} \ 12 | {{ '<<< "' + item.passphrase + '"' if item.passphrase is defined else '' }} 13 | --------------------------------------------------------------------------------