├── .gitignore ├── LICENSE ├── README.md ├── plays ├── ansible.cfg ├── lockdown.yml └── password.yml └── roles ├── create-user └── tasks │ └── main.yml ├── disable-passwords └── tasks │ └── main.yml ├── expand-filesystem └── tasks │ └── main.yml ├── pi-password └── tasks │ └── main.yml ├── set-hostname └── tasks │ └── main.yml └── static-ip ├── handlers └── main.yml └── tasks └── main.yml /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.retry 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2017, Gary Gale 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ansible Playbooks for Initial Raspberry Pi Lockdown 2 | 3 | Simple Ansible playbooks, roles and tasks to lock down and perform initial setup for a new Raspberry Pi. 4 | 5 | ## Assumptions and Dependencies 6 | 7 | These playbooks assume a freshly minted Raspberry Pi running the current version of either Raspbian or Raspbian Lite. Other Raspberry Pi distros exist and [YMMV](https://www.urbandictionary.com/define.php?term=ymmv). 8 | 9 | These playbooks also assume that you have [Ansible installed](https://docs.ansible.com/ansible/latest/intro_installation.html) and ready on your control machine. 10 | 11 | ## Inventory 12 | 13 | When a Pi first boots it (usually) receives a DHCP assigned IP address, which the Lockdown playbook changes to a static IP. 14 | 15 | To save having to create an inventory file and then immediately update it, these playbooks use a _feature_ of the `--inventory` command line argument for `ansible-playbook` where you can supply an IP address followed _**immediately**_ by a comma so that Ansible knows the inventory is a list of hosts (even though there's a single host being targeted). 16 | 17 | Like this ... `--inventory 192.168.10.20,` 18 | 19 | ## Password Playbook 20 | 21 | Changes the password for the default `pi` account. 22 | 23 | Why the separate playbook? As this playbook changes the password that Ansible is using to authenticate, Ansible will have reload its inventory and host variables, which will fail as the password provided at the start of the playbook is no longer valid. 24 | 25 | See [this discussion](https://github.com/ansible/ansible/issues/15227) for more background. 26 | 27 | ### Usage 28 | 29 | ```bash 30 | $ ansible-playbook --user pi --ask-pass --inventory 'IP-ADDRESS,' password.yml 31 | ``` 32 | 33 | Running this playbook on a Raspberry Pi with an initial DHCP assigned IP address of `192.168.1.237` will look something like this. 34 | 35 | ```bash 36 | $ cd plays 37 | $ ansible-playbook --user pi --ask-pass --inventory '192.168.1.237,' password.yml 38 | SSH password: 39 | New pi account password: 40 | confirm New pi account password: 41 | 42 | PLAY [Default "pi" account password reset playbook] **************************** 43 | 44 | TASK [Gathering Facts] ********************************************************* 45 | ok: [192.168.1.237] 46 | 47 | TASK [pi-password : Set a new password for the default "pi" account] *********** 48 | changed: [192.168.1.237] 49 | 50 | PLAY RECAP ********************************************************************* 51 | 192.168.1.237 : ok=2 changed=1 unreachable=0 failed=0 52 | ``` 53 | 54 | 55 | ## Lockdown Playbook 56 | 57 | Performs some initial setup and lockdown on your new Pi. 58 | 59 | * Sets the hostname for the Pi 60 | * Creates a new user and deploys an SSH public key for the user 61 | * Disables password authentication and enforces SSH key authentication 62 | * Sets a static IP address, router and DNS servers 63 | * Expands the root filesystem to fill any remaining space on the Pi's SD card 64 | 65 | ### Usage 66 | 67 | ```bash 68 | $ cd plays 69 | $ ansible-playbook --user pi --ask-pass --inventory 'IP-ADDRESS,' lockdown.yml 70 | ``` 71 | 72 | Running this playbook on the same Raspberry Pi described above, with a static IP of `192.168.1.2` looks something like this (remember to use the new password for the `pi` account!) 73 | 74 | ```bash 75 | $ ansible-playbook --user pi --ask-pass --inventory '192.168.1.237,' lockdown.yml 76 | SSH password: 77 | Hostname: dns.vicchi.local 78 | User name: guest 79 | Password: 80 | confirm Password: 81 | Username description: Guest Account 82 | Path to public SSH key: /tmp/id_rsa.pub 83 | Ethernet interface [eth0]: 84 | Static IPv4 address: 192.168.1.2 85 | Routers (comma separated): 192.168.1.1 86 | DNS servers (comma separated) [8.8.8.8,8.8.4.4]: 87 | 88 | PLAY [Application server specific playbook] ************************************ 89 | 90 | TASK [Gathering Facts] ********************************************************* 91 | ok: [192.168.1.237] 92 | 93 | TASK [set-hostname : Set the hostname] ***************************************** 94 | changed: [192.168.1.237] 95 | 96 | TASK [set-hostname : Update /etc/hosts with new hostname] ********************** 97 | changed: [192.168.1.237] 98 | 99 | TASK [create-user : Create a (non default) user account] *********************** 100 | changed: [192.168.1.237] 101 | 102 | TASK [create-user : Deploy user's SSH key] ************************************* 103 | changed: [192.168.1.237] 104 | 105 | TASK [disable-passwords : Disable SSH password authentication] ***************** 106 | changed: [192.168.1.237] 107 | 108 | TASK [static-ip : Configure static IP in /etc/dhcpcd.conf] ******************** 109 | changed: [192.168.1.237] => (item={u'regexp': u'^interface eth[0-9]$', u'line': u'interface eth0'}) 110 | changed: [192.168.1.237] => (item={u'regexp': u'^static ip_address', u'line': u'static ip_address=192.168.1.2'}) 111 | changed: [192.168.1.237] => (item={u'regexp': u'^static routers', u'line': u'static routers=192.168.1.1'}) 112 | changed: [192.168.1.237] => (item={u'regexp': u'^static domain_name_servers', u'line': u'static domain_name_servers=8.8.8.8,8.8.4.4'}) 113 | 114 | TASK [expand-filesystem : Expand filesystem to fill disk] ********************** 115 | changed: [192.168.1.237] 116 | 117 | RUNNING HANDLER [static-ip : reboot] ******************************************* 118 | changed: [192.168.1.237] 119 | 120 | PLAY RECAP ********************************************************************* 121 | 192.168.1.237 : ok=9 changed=8 unreachable=0 failed=0 122 | ``` 123 | -------------------------------------------------------------------------------- /plays/ansible.cfg: -------------------------------------------------------------------------------- 1 | [defaults] 2 | roles_path = ../roles 3 | -------------------------------------------------------------------------------- /plays/lockdown.yml: -------------------------------------------------------------------------------- 1 | # ansible-playbook -u pi -k -i 'TARGET-DHCP-IP,' lockdown.yml 2 | --- 3 | 4 | - name: Application server specific playbook 5 | hosts: all 6 | become: yes 7 | roles: 8 | - set-hostname 9 | - create-user 10 | - disable-passwords 11 | - static-ip 12 | - expand-filesystem 13 | 14 | vars_prompt: 15 | - name: "hostname" 16 | prompt: "Hostname" 17 | private: no 18 | - name: "username" 19 | prompt: "User name" 20 | private: no 21 | - name: "password" 22 | prompt: "Password" 23 | private: yes 24 | encrypt: "sha512_crypt" 25 | confirm: yes 26 | - name: "comment" 27 | prompt: "Username description" 28 | private: no 29 | - name: "public_key" 30 | prompt: "Path to public SSH key" 31 | private: no 32 | - name: "interface" 33 | prompt: "Ethernet interface" 34 | default: "eth0" 35 | private: no 36 | - name: "ipaddress" 37 | prompt: "Static IPv4 address" 38 | private: no 39 | - name: "routers" 40 | prompt: "Routers (comma separated)" 41 | private: no 42 | - name: "dns_servers" 43 | prompt: "DNS servers (space separated)" 44 | default: "8.8.8.8 8.8.4.4" 45 | private: no 46 | -------------------------------------------------------------------------------- /plays/password.yml: -------------------------------------------------------------------------------- 1 | # ansible-playbook -u pi -k -i 'TARGET-DHCP-IP,' password.yml 2 | --- 3 | 4 | - name: Default "pi" account password reset playbook 5 | hosts: all 6 | become: yes 7 | roles: 8 | - pi-password 9 | 10 | vars_prompt: 11 | - name: "pi_password" 12 | prompt: "New pi account password" 13 | private: yes 14 | encrypt: "sha512_crypt" 15 | confirm: yes 16 | -------------------------------------------------------------------------------- /roles/create-user/tasks/main.yml: -------------------------------------------------------------------------------- 1 | # roles/create-user/tasks/main.yml 2 | --- 3 | 4 | - name: Create a (non default) user account 5 | become: yes 6 | user: 7 | name: "{{ username }}" 8 | shell: /bin/bash 9 | createhome: yes 10 | comment: "{{ comment }}" 11 | groups: adm,dialout,cdrom,sudo,audio,video,plugdev,games,users,input,netdev,gpio,i2c,spi 12 | password: "{{ password }}" 13 | 14 | - name: Deploy user's SSH key 15 | become: yes 16 | authorized_key: 17 | user: "{{ username }}" 18 | key: "{{ lookup('file', '{{ public_key }}') }}" 19 | -------------------------------------------------------------------------------- /roles/disable-passwords/tasks/main.yml: -------------------------------------------------------------------------------- 1 | # roles/disable-passwords/tasks/main.yml 2 | --- 3 | 4 | # We would normally want to HUP sshd to reload the config file 5 | # But we'll be rebooting as a result of changing the IP address 6 | # to static, so just fire the reboot handler 7 | 8 | - name: Disable SSH password authentication 9 | lineinfile: 10 | dest: /etc/ssh/sshd_config 11 | regexp: "^PasswordAuthentication" 12 | line: "PasswordAuthentication no" 13 | state: present 14 | notify: reboot 15 | -------------------------------------------------------------------------------- /roles/expand-filesystem/tasks/main.yml: -------------------------------------------------------------------------------- 1 | # roles/expand-filesystem/tasks/main.yml 2 | --- 3 | 4 | - name: Expand filesystem to fill disk 5 | command: raspi-config --expand-rootfs 6 | become: yes 7 | notify: 8 | - reboot 9 | -------------------------------------------------------------------------------- /roles/pi-password/tasks/main.yml: -------------------------------------------------------------------------------- 1 | # roles/pi-password/tasks/main.yml 2 | --- 3 | 4 | - name: Set a new password for the default "pi" account 5 | become: yes 6 | user: 7 | name: "pi" 8 | password: "{{ pi_password }}" 9 | -------------------------------------------------------------------------------- /roles/set-hostname/tasks/main.yml: -------------------------------------------------------------------------------- 1 | # roles/hostname/tasks/main.yml 2 | --- 3 | 4 | - name: Set the hostname 5 | become: yes 6 | command: hostnamectl set-hostname "{{ hostname }}" 7 | 8 | - name: Update /etc/hosts with new hostname 9 | become: yes 10 | lineinfile: 11 | dest: /etc/hosts 12 | regexp: "^127.0.1.1\traspberrypi$" 13 | line: "127.0.1.1\t{{ hostname }}" 14 | state: present 15 | -------------------------------------------------------------------------------- /roles/static-ip/handlers/main.yml: -------------------------------------------------------------------------------- 1 | # roles/static-ip/handlers/main.yml 2 | --- 3 | 4 | - name: reboot 5 | command: shutdown -r +0 'Ansible Reboot triggered' 6 | async: 0 7 | poll: 0 8 | ignore_errors: true 9 | become: true 10 | 11 | - name: wait for reboot to finish 12 | local_action: wait_for host={{ ipaddress }} state=started port=22 delay=50 timeout=120 13 | become: false 14 | -------------------------------------------------------------------------------- /roles/static-ip/tasks/main.yml: -------------------------------------------------------------------------------- 1 | # roles/static-ip/tasks/main.yml 2 | --- 3 | 4 | - name: Configure static IP in /etc/dhcpcd.conf 5 | become: yes 6 | lineinfile: 7 | dest: /etc/dhcpcd.conf 8 | regexp: "{{ item.regexp }}" 9 | line: "{{ item.line }}" 10 | state: present 11 | with_items: 12 | - { regexp: "^interface eth[0-9]$", line: "interface {{ interface }}" } 13 | - { regexp: "^static ip_address", line: "static ip_address={{ ipaddress }}" } 14 | - { regexp: "^static routers", line: "static routers={{ routers }}" } 15 | - { regexp: "^static domain_name_servers", line: "static domain_name_servers={{ dns_servers }}" } 16 | notify: reboot 17 | --------------------------------------------------------------------------------