├── .ansible-lint ├── .gitignore ├── .yamllint ├── LICENSE.md ├── README.md ├── ansible.cfg ├── group_vars └── all │ └── vars.yml ├── pass.sh ├── requirements.yml ├── roles ├── containers │ ├── tasks │ │ ├── install.yml │ │ └── main.yml │ └── templates │ │ └── compose.yaml ├── neovim │ └── tasks │ │ └── main.yml ├── system │ ├── defaults │ │ └── .gitkeep │ ├── handlers │ │ └── main.yml │ ├── tasks │ │ ├── dotfiles.yml │ │ ├── essential.yml │ │ ├── main.yml │ │ ├── netplan.yml │ │ └── user.yml │ └── templates │ │ └── netplan.yaml └── tailscale │ └── tasks │ ├── configure.yml │ ├── install.yml │ └── main.yml ├── run.yml ├── tasks ├── repo_arch.yml └── ssh_juggle_port.yml └── upload_iso.yml /.ansible-lint: -------------------------------------------------------------------------------- 1 | --- 2 | skip_list: 3 | - git-latest 4 | - package-latest 5 | - yaml 6 | 7 | warn_list: 8 | - load-failure 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /files/docker_persistent_data/* 2 | !/files/docker_persistent_data/.gitkeep 3 | /hosts 4 | /files/fonts/* 5 | .DS_Store 6 | secret.yml 7 | /group_vars/ 8 | /host_vars/ 9 | !/host_vars/.gitkeep 10 | !/group_vars/all/vars.yml 11 | mountsraspi 12 | .fact_cache 13 | -------------------------------------------------------------------------------- /.yamllint: -------------------------------------------------------------------------------- 1 | --- 2 | extends: default 3 | 4 | rules: 5 | line-length: disable 6 | truthy: 7 | allowed-values: ['true', 'false', 'yes', 'no'] 8 | comments: 9 | min-spaces-from-content: 1 10 | braces: 11 | min-spaces-inside: 0 12 | max-spaces-inside: 1 13 | 14 | ignore: | 15 | .cache -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 2 | Version 2, December 2004 3 | 4 | Copyright (C) 2004 Sam Hocevar 5 | 6 | Everyone is permitted to copy and distribute verbatim or modified 7 | copies of this license document, and changing it is allowed as long 8 | as the name is changed. 9 | 10 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 11 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 12 | 13 | 0. You just DO WHAT THE FUCK YOU WANT TO. 14 | 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # notthebee/infra 2 | 3 | Superseded by [nix-config](https://github.com/notthebee/nix-config) 4 | 5 | Original README: 6 | ``` 7 | An Ansible playbook that sets up an Ubuntu-based server with reasonable security, auto-updates, e-mail notifications for S.M.A.R.T. and Snapraid errors. Currently being completely rewritten 8 | 9 | It assumes a fresh Ubuntu Server 20.04 install, access to a non-root user with sudo privileges and a public SSH key. This can be configured during the installation process. 10 | 11 | The playbook is mostly being developed for personal use, so stuff is going to be constantly changing and breaking. Use at your own risk and don't expect any help in setting it up on your machine. 12 | 13 | ## Special thanks 14 | * David Stephens for his [Ansible NAS](https://github.com/davestephens/ansible-nas) project. This is where I got the idea and "borrowed" a lot of concepts and implementations from. 15 | * Jeff Geerling for his book, [Ansible for DevOps](https://www.ansiblefordevops.com/) and his [Ansible 101 series](https://www.youtube.com/watch?v=goclfp6a2IQ&list=PL2_OBreMn7FqZkvMYt6ATmgC0KAGGJNAN) on YouTube. 16 | * Jonathan Hanson for his [SSH port juggling](https://gist.github.com/triplepoint/1ad6c6060c0f12112403d98180bcf0b4) implementation. 17 | * Alex Kretzschmar and Chris Fisher from [Self Hosted Show](https://selfhosted.show/) for introducing me to the idea of Infrastracture as Code 18 | * TylerAlterio for the [mergerfs](https://github.com/tyalt1/mediaserver/tree/master/roles/mergerfs) role 19 | * Jake Howard and Alex Kretzschmar for the [snapraid](https://github.com/RealOrangeOne/ansible-role-snapraid/commits?author=IronicBadger) role 20 | 21 | ## Services included: 22 | * [Home Assistant](https://hub.docker.com/r/homeassistant/home-assistant) 23 | * [Phoscon-GW](https://hub.docker.com/r/marthoc/deconz) 24 | * [nginx-proxy-manager](https://nginxproxymanager.com/) 25 | ``` 26 | -------------------------------------------------------------------------------- /ansible.cfg: -------------------------------------------------------------------------------- 1 | [defaults] 2 | INVENTORY = hosts 3 | vault_password_file = pass.sh 4 | ansible_python_interpreter=/usr/bin/python3 5 | timeout=30 6 | gathering = smart 7 | fact_caching = jsonfile 8 | fact_caching_connection = .fact_cache 9 | fact_caching_timeout = 43200 10 | hash_behaviour = merge 11 | forks = 32 12 | 13 | [ssh_connection] 14 | pipelining = True 15 | -------------------------------------------------------------------------------- /group_vars/all/vars.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Misc 3 | # 4 | hostname: '{{ inventory_hostname }}' 5 | 6 | timezone: Europe/Amsterdam 7 | 8 | ntp_timezone: '{{ timezone }}' 9 | 10 | locale: en_US.UTF-8 11 | 12 | keyboard_layout: us 13 | 14 | username: notthebee 15 | 16 | shell: /usr/bin/fish 17 | 18 | dotfiles_repo: https://github.com/notthebee/dotfiles 19 | 20 | guid: 1000 21 | 22 | # 23 | # Networking 24 | # 25 | tailscale_enabled: yes 26 | 27 | tailscale_exit_node: no 28 | 29 | networks: 30 | - name: lan 31 | cidr: 192.168.2.0/24 32 | base: 192.168.2 33 | interface: ens18 34 | tailscale: yes # whether the subnet should be exposed to other Tailscale nodes 35 | - name: app 36 | cidr: 10.0.0.0/24 37 | base: 10.0.0 38 | tailscale: yes 39 | - name: iot 40 | cidr: 192.168.32.0/24 41 | base: 192.168.32 42 | tailscale: yes 43 | 44 | # 45 | # Docker apps 46 | # 47 | # 48 | docker_dir: /opt/docker/data 49 | 50 | docker_compose_dir: /opt/docker/compose 51 | 52 | services: 53 | - homeassistant 54 | - mqtt 55 | - nginxproxymanager 56 | - deconz 57 | - photoprism 58 | - syncthing 59 | 60 | # 61 | # Packages 62 | # 63 | extra_packages: 64 | - fish 65 | - iperf3 66 | - speedtest-cli 67 | - htop 68 | - stow 69 | - exa 70 | - git 71 | - neofetch 72 | - neovim 73 | - tmux 74 | - mosh 75 | - rsync 76 | - lm-sensors 77 | - iotop 78 | - ncdu 79 | 80 | # 81 | # Email credentials (for notifications) 82 | # 83 | 84 | email: moe@notthebe.ee 85 | 86 | email_login: '{{ email }}' 87 | 88 | email_smtp_host: smtp.mailbox.org 89 | 90 | email_smtp_port: 465 91 | 92 | email_smtp_port_startls: 587 93 | 94 | # MSMTP 95 | msmtp_accounts: 96 | - account: mailbox 97 | host: '{{ email_smtp_host }}' 98 | port: '{{ email_smtp_port_startls }}' 99 | auth: 'on' 100 | from: '{{ email }}' 101 | user: '{{ email }}' 102 | password: '{{ email_password }}' 103 | 104 | msmtp_default_account: 'mailbox' 105 | 106 | msmtp_alias_default: '{{ email }}' 107 | 108 | # 109 | # SSH (geerlingguy.security) 110 | # 111 | security_ssh_port: 69 112 | 113 | security_sudoers_passwordless: ['{{ username }}'] 114 | 115 | security_autoupdate_reboot: true 116 | 117 | security_autoupdate_mail_to: '{{ email }}' 118 | 119 | security_autoupdate_reboot_time: '23:00' 120 | 121 | security_autoupdate_mail_on_error: false 122 | -------------------------------------------------------------------------------- /pass.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Keychain query fields. 4 | # LABEL is the value you put for "Keychain Item Name" in Keychain.app. 5 | LABEL="ansible-vault-password" 6 | ACCOUNT_NAME="notthebee" 7 | 8 | /usr/bin/security find-generic-password -w -a "$ACCOUNT_NAME" -l "$LABEL" 9 | -------------------------------------------------------------------------------- /requirements.yml: -------------------------------------------------------------------------------- 1 | --- 2 | roles: 3 | - name: geerlingguy.repo-epel 4 | - name: geerlingguy.security 5 | - name: geerlingguy.docker 6 | - name: geerlingguy.ntp 7 | - name: chriswayg.msmtp-mailer 8 | -------------------------------------------------------------------------------- /roles/containers/tasks/install.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Install Python and python3-pip 3 | package: 4 | name: 5 | - python3 6 | - python3-pip 7 | state: present 8 | 9 | - name: Install docker module for Python 10 | pip: 11 | name: 12 | - docker 13 | - docker-compose 14 | 15 | - name: Make sure that the docker folders exists 16 | ansible.builtin.file: 17 | path: "{{ item }}" 18 | owner: "{{ username }}" 19 | group: "{{ username }}" 20 | state: directory 21 | loop: 22 | - "{{ docker_compose_dir }}" 23 | - "{{ docker_dir }}" 24 | 25 | - name: Copy the compose file 26 | template: 27 | src: templates/compose.yaml 28 | dest: "{{ docker_compose_dir }}/compose.yaml" 29 | vars: 30 | app_cidr: "{{ (networks | selectattr('name', '==', 'app') | map(attribute='cidr') | first) | default('') }}" 31 | app_base: "{{ (networks | selectattr('name', '==', 'app') | map(attribute='base') | first) | default('') }}" 32 | 33 | - name: Docker-compose up 34 | community.docker.docker_compose: 35 | project_src: "{{ docker_compose_dir }}" 36 | 37 | -------------------------------------------------------------------------------- /roles/containers/tasks/main.yml: -------------------------------------------------------------------------------- 1 | - include_tasks: install.yml 2 | -------------------------------------------------------------------------------- /roles/containers/templates/compose.yaml: -------------------------------------------------------------------------------- 1 | {% if not app_cidr == "" %} 2 | networks: 3 | app: 4 | driver: macvlan 5 | driver_opts: 6 | parent: app 7 | ipam: 8 | config: 9 | - subnet: {{ app_cidr }} 10 | {% endif %} 11 | 12 | services: 13 | {% if "nginxproxymanager" in services %} 14 | nginxproxymanager: 15 | container_name: nginxproxymanager 16 | image: jc21/nginx-proxy-manager:latest 17 | {% if not app_cidr == "" %} 18 | networks: 19 | app: 20 | ipv4_address: {{ app_base }}.24 21 | {% else %} 22 | ports: 23 | - "80:80" 24 | - "81:81" 25 | - "443:443" 26 | {% endif %} 27 | restart: always 28 | volumes: 29 | - "{{ docker_dir }}/nginxproxymanager/data:/data" 30 | - "{{ docker_dir }}/nginxproxymanager/letsencrypt:/etc/letsencrypt" 31 | {% endif %} 32 | 33 | {% if "homeassistant" in services %} 34 | homeassistant: 35 | container_name: homeassistant 36 | image: homeassistant/home-assistant:stable 37 | {% if not app_cidr == "" %} 38 | networks: 39 | app: 40 | ipv4_address: {{ app_base }}.18 41 | {% else %} 42 | ports: 43 | - "8123:8123" 44 | {% endif %} 45 | restart: always 46 | volumes: 47 | - "{{ docker_dir }}/homeassistant:/config" 48 | environment: 49 | - TZ={{ timezone }} 50 | restart: always 51 | {% endif %} 52 | 53 | {% if "mqtt" in services %} 54 | mqtt: 55 | container_name: mqtt 56 | image: eclipse-mosquitto 57 | networks: 58 | app: 59 | ipv4_address: {{ app_base }}.13 60 | volumes: 61 | - "{{ docker_dir }}/mosquitto:/mosquitto" 62 | restart: always 63 | {% endif %} 64 | 65 | {% if "deconz" in services %} 66 | deconz: 67 | container_name: deconz 68 | image: deconzcommunity/deconz 69 | restart: always 70 | networks: 71 | app: 72 | ipv4_address: {{ app_base }}.25 73 | volumes: 74 | - "{{ docker_dir }}/deconz:/opt/deCONZ" 75 | devices: 76 | - /dev/ttyACM0 77 | environment: 78 | - DECONZ_VNC_MODE=1 79 | - TZ={{ timezone }} 80 | {% endif %} 81 | 82 | {% if "syncthing" in services %} 83 | syncthing: 84 | image: syncthing/syncthing 85 | container_name: syncthing 86 | hostname: "{{ inventory_hostname }}" 87 | environment: 88 | - PUID=1000 89 | - PGID=1000 90 | volumes: 91 | - {{ docker_dir }}/syncthing:/var/syncthing 92 | ports: 93 | - 8384:8384 # Web UI 94 | - 22000:22000/tcp # TCP file transfers 95 | - 22000:22000/udp # QUIC file transfers 96 | - 21027:21027/udp # Receive local discovery broadcasts 97 | restart: unless-stopped 98 | {% endif %} 99 | 100 | {% if "photoprism" in services %} 101 | photoprism_mariadb: 102 | container_name: photoprism-mariadb 103 | image: mariadb:latest 104 | restart: unless-stopped 105 | volumes: 106 | - {{ docker_dir }}/photoprism/db:/var/lib/mysql 107 | environment: 108 | - TZ={{ timezone }} 109 | - MYSQL_ROOT_PASSWORD={{ photoprism.mysql.root_password }} 110 | - MYSQL_DATABASE={{ photoprism.mysql.db }} 111 | - MYSQL_USER={{ photoprism.mysql.user }} 112 | - MYSQL_PASSWORD={{ photoprism.mysql.password }} 113 | 114 | photoprism: 115 | container_name: photoprism 116 | image: photoprism/photoprism:latest 117 | restart: unless-stopped 118 | ports: 119 | - 2342:2342 120 | volumes: 121 | - {{ docker_dir }}/photoprism/app:/photoprism 122 | environment: 123 | - PUID={{ guid }} 124 | - PGID={{ guid }} 125 | - PHOTOPRISM_GID={{ guid }} 126 | - PHOTOPRISM_UID={{ guid }} 127 | - TZ={{ timezone }} 128 | - PHOTOPRISM_ADMIN_PASSWORD={{ photoprism.password }} 129 | - PHOTOPRISM_SITE_URL=http://localhost:2342 130 | - PHOTOPRISM_EXPERIMENTAL=false 131 | - PHOTOPRISM_HTTP_COMPRESSION=gzip 132 | - PHOTOPRISM_DATABASE_DRIVER=mysql 133 | - PHOTOPRISM_DATABASE_SERVER=photoprism-mariadb:3306 134 | - PHOTOPRISM_AUTH_MODE="public" 135 | - PHOTOPRISM_DATABASE_NAME={{ photoprism.mysql.db }} 136 | - PHOTOPRISM_DATABASE_USER={{ photoprism.mysql.user }} 137 | - PHOTOPRISM_DATABASE_PASSWORD={{ photoprism.mysql.password }} 138 | - PHOTOPRISM_DISABLE_CHOWN=false 139 | - PHOTOPRISM_DISABLE_BACKUPS=true 140 | - PHOTOPRISM_DISABLE_WEBDAV=false 141 | - PHOTOPRISM_DETECT_NSFW=true 142 | - PHOTOPRISM_UPLOAD_NSFW=false 143 | - PHOTOPRISM_DEBUG=true 144 | - PHOTOPRISM_THUMB_FILTER=lanczos 145 | - PHOTOPRISM_THUMB_UNCACHED=true 146 | - PHOTOPRISM_THUMB_SIZE=2048 147 | - PHOTOPRISM_THUMB_SIZE_UNCACHED=7680 148 | - PHOTOPRISM_JPEG_SIZE=7680 149 | - PHOTOPRISM_JPEG_QUALITY=92 150 | - TF_CPP_MIN_LOG_LEVEL=0 151 | - PHOTOPRISM_FFMPEG_ENCODER=h264_qsv 152 | - PHOTOPRISM_INIT=tensorflow-amd64-avx2 153 | {% endif %} 154 | -------------------------------------------------------------------------------- /roles/neovim/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Check if neovim is installed via the package manager 3 | package_facts: 4 | manager: auto 5 | 6 | - name: Check if neovim is installed 7 | command: 8 | cmd: which nvim 9 | register: nvim 10 | changed_when: False 11 | failed_when: False 12 | 13 | - name: Check the nvim version if installed 14 | shell: 15 | cmd: "nvim --version | head -n 1 | cut -c 6-" 16 | register: neovim_version 17 | when: nvim.rc == 0 18 | changed_when: False 19 | 20 | - name: Set current neovim version to 0 if not installed 21 | set_fact: 22 | neovim_version: 23 | stdout: 0 24 | when: nvim.rc == 1 25 | 26 | - name: Install python3 and pip 27 | package: 28 | name: 29 | - python3 30 | - python3-pip 31 | state: latest 32 | 33 | - name: Install github3 module 34 | pip: 35 | name: 36 | - github3.py 37 | 38 | - name: Check latest neovim release 39 | github_release: 40 | user: neovim 41 | repo: neovim 42 | action: latest_release 43 | token: "{{ github_token }}" 44 | register: neovim_release 45 | changed_when: neovim_release.tag != neovim_version.stdout 46 | 47 | - name: Delete the old version of nvim via the package manager 48 | package: 49 | name: neovim 50 | state: absent 51 | when: "'neovim' in ansible_facts.packages and neovim_release.tag != neovim_version.stdout" 52 | 53 | - name: Install neovim 54 | when: neovim_release.tag != neovim_version.stdout 55 | block: 56 | - name: Check if the node repo is present 57 | stat: 58 | path: "/etc/apt/sources.list.d/nodesource.list" 59 | register: nodesource 60 | 61 | - name: Add the node repo 62 | shell: 63 | cmd: "curl -sL https://deb.nodesource.com/setup_16.x | bash -" 64 | when: not nodesource.stat.exists 65 | tags: 66 | - skip_ansible_lint 67 | 68 | - name: Add the yarn key 69 | apt_key: 70 | url: https://dl.yarnpkg.com/debian/pubkey.gpg 71 | state: present 72 | 73 | - name: Add the yarn repo 74 | apt_repository: 75 | repo: "deb https://dl.yarnpkg.com/debian/ stable main" 76 | state: present 77 | filename: yarn 78 | 79 | - name: Install the dependencies 80 | package: 81 | name: 82 | - golang 83 | - ninja-build 84 | - gettext 85 | - libtool 86 | - libtool-bin 87 | - autoconf 88 | - automake 89 | - cmake 90 | - g++ 91 | - pkg-config 92 | - unzip 93 | - curl 94 | - doxygen 95 | - yarn 96 | state: present 97 | 98 | - name: Install python modules 99 | pip: 100 | name: 101 | - setuptools 102 | - pynvim 103 | 104 | - name: Grab the latest release source 105 | unarchive: 106 | src: "https://github.com/neovim/neovim/archive/{{ neovim_release['tag'] }}.tar.gz" 107 | dest: /tmp 108 | remote_src: true 109 | 110 | - name: Get the neovim folder 111 | find: 112 | paths: /tmp 113 | patterns: "^neovim.*$" 114 | use_regex: yes 115 | file_type: directory 116 | recurse: no 117 | register: neovim_source 118 | 119 | - name: Compile and install neovim 120 | shell: 121 | cmd: cd {{ neovim_source.files[0].path }} && make install 122 | 123 | - name: Clean up 124 | file: 125 | path: "{{ neovim_source.files[0].path }}" 126 | state: absent 127 | -------------------------------------------------------------------------------- /roles/system/defaults/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notthebee/infra/52ba1f0aee1f94817b6d231151fa17bde4093af6/roles/system/defaults/.gitkeep -------------------------------------------------------------------------------- /roles/system/handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Apply netplan config 3 | command: 4 | cmd: netplan apply 5 | -------------------------------------------------------------------------------- /roles/system/tasks/dotfiles.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Chown the repo 3 | file: 4 | path: '/home/{{ username }}/dotfiles' 5 | recurse: yes 6 | state: directory 7 | owner: '{{ username }}' 8 | group: '{{ username }}' 9 | 10 | - name: Clone the latest dotfiles repo 11 | become_user: '{{ username }}' 12 | git: 13 | repo: '{{ dotfiles_repo }}' 14 | dest: '/home/{{ username }}/dotfiles' 15 | recursive: no 16 | force: yes 17 | 18 | - name: Stow the dotfiles 19 | become_user: '{{ username }}' 20 | shell: 21 | cmd: stow -v */ 22 | chdir: '/home/{{ username }}/dotfiles' 23 | register: stow_result 24 | changed_when: stow_result.stdout != "" 25 | -------------------------------------------------------------------------------- /roles/system/tasks/essential.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Update and upgrade packages 3 | apt: 4 | update_cache: yes 5 | upgrade: yes 6 | autoremove: yes 7 | 8 | - name: Check if reboot required 9 | stat: 10 | path: /var/run/reboot-required 11 | register: reboot_required_file 12 | 13 | - name: Reboot if required 14 | reboot: 15 | msg: Rebooting due to a kernel update 16 | when: reboot_required_file.stat.exists 17 | 18 | - name: Install extra packages 19 | package: 20 | name: "{{ extra_packages }}" 21 | state: present 22 | 23 | - name: Set the hostname 24 | hostname: 25 | name: "{{ inventory_hostname }}" 26 | 27 | - name: Replace the hostname entry with our own 28 | ansible.builtin.lineinfile: 29 | path: /etc/hosts 30 | insertafter: ^127\.0\.0\.1 *localhost 31 | line: "127.0.0.1 {{ inventory_hostname }}" 32 | owner: root 33 | group: root 34 | mode: '0644' 35 | 36 | - name: Disable cron e-mail notifications 37 | cron: 38 | name: MAILTO 39 | user: root 40 | env: yes 41 | job: "" 42 | -------------------------------------------------------------------------------- /roles/system/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - include_tasks: netplan.yml 3 | - include_tasks: essential.yml 4 | - include_tasks: user.yml 5 | - include_tasks: dotfiles.yml 6 | -------------------------------------------------------------------------------- /roles/system/tasks/netplan.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Template out the netplan file 3 | ansible.builtin.template: 4 | src: netplan.yaml 5 | dest: /etc/netplan/00-ansible.yaml 6 | owner: root 7 | group: root 8 | mode: 0644 9 | notify: Apply netplan config 10 | 11 | - meta: flush_handlers 12 | -------------------------------------------------------------------------------- /roles/system/tasks/user.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Set the name of a sudo group 3 | set_fact: 4 | sudo_group: sudo 5 | 6 | - name: Ensure the necessary groupsexists 7 | group: 8 | name: "{{ item }}" 9 | state: present 10 | loop: 11 | - "{{ username }}" 12 | - docker 13 | 14 | - name: Create a login user 15 | user: 16 | name: "{{ username }}" 17 | password: "{{ password | password_hash('sha512') }}" 18 | groups: 19 | - "{{ sudo_group }}" 20 | - docker 21 | - users 22 | state: present 23 | append: true 24 | 25 | - name: Chmod the user home directory 26 | file: 27 | path: "/home/{{ username }}" 28 | state: directory 29 | mode: 0755 30 | owner: "{{ username }}" 31 | group: "{{ username }}" 32 | recurse: yes 33 | 34 | - name: Allow '{{ sudo_group }}' group to have passwordless sudo 35 | lineinfile: 36 | path: /etc/sudoers 37 | state: present 38 | regexp: '^%{{ sudo_group }}' 39 | line: '%{{ sudo_group }} ALL=(ALL) NOPASSWD: ALL' 40 | validate: '/usr/sbin/visudo -cf %s' 41 | 42 | - name: Copy the public SSH key 43 | authorized_key: 44 | user: "{{ username }}" 45 | state: present 46 | key: "{{ ssh_public_key }}" 47 | 48 | - name: Set the default shell 49 | user: 50 | name: "{{ username }}" 51 | shell: "{{ shell }}" 52 | 53 | - name: Disable fish greeting 54 | lineinfile: 55 | path: /home/{{ username }}/.config/fish/fish_variables 56 | state: present 57 | regexp: 'fish_greeting:.+' 58 | line: 'SETUVAR fish_greeting:' 59 | create: true 60 | owner: "{{ username }}" 61 | group: "{{ username }}" 62 | mode: 0644 63 | when: '"fish" in shell' 64 | 65 | - name: Suppress login messages 66 | file: 67 | name: /home/{{ username }}/.hushlogin 68 | mode: 0644 69 | state: touch 70 | owner: "{{ username }}" 71 | group: "{{ username }}" 72 | modification_time: preserve 73 | access_time: preserve 74 | 75 | - name: Disable cron e-mail notifications 76 | cron: 77 | name: MAILTO 78 | user: "{{ username }}" 79 | env: yes 80 | job: "" 81 | -------------------------------------------------------------------------------- /roles/system/templates/netplan.yaml: -------------------------------------------------------------------------------- 1 | network: 2 | version: 2 3 | ethernets: 4 | {{ networks.lan.interface }}: 5 | dhcp4: true 6 | vlans: 7 | app: 8 | id: 4 9 | link: {{ networks.lan.interface }} 10 | addresses: [ '{{ networks.app.base }}.228/24' ] 11 | -------------------------------------------------------------------------------- /roles/tailscale/tasks/configure.yml: -------------------------------------------------------------------------------- 1 | 2 | - name: Set a list of exposed networks 3 | set_fact: 4 | tailscale_subnets: "{{ networks | selectattr('tailscale') | map(attribute='cidr') | join(',') }}" 5 | 6 | - name: Get current tailscale status 7 | changed_when: false 8 | register: tailscale_status_before 9 | ansible.builtin.command: 10 | cmd: tailscale status 11 | 12 | - name: Log in, enable tailscale and set up an exit node 13 | changed_when: false 14 | ansible.builtin.command: 15 | cmd: >- 16 | tailscale up 17 | --advertise-exit-node={{ tailscale_exit_node | default(false) | bool | lower }} 18 | --auth-key {{ tailscale_token }} 19 | --advertise-routes "{{ tailscale_subnets }}" 20 | 21 | - name: Get tailscale status after the command 22 | changed_when: tailscale_status_before.stdout != tailscale_status_after.stdout 23 | register: tailscale_status_after 24 | ansible.builtin.command: 25 | cmd: tailscale status 26 | -------------------------------------------------------------------------------- /roles/tailscale/tasks/install.yml: -------------------------------------------------------------------------------- 1 | - name: Add tailscale repository key 2 | ansible.builtin.get_url: 3 | url: "https://pkgs.tailscale.com/stable/ubuntu/{{ ansible_distribution_release | lower }}.noarmor.gpg" 4 | dest: /usr/share/keyrings/tailscale-archive-keyring.gpg 5 | owner: root 6 | group: root 7 | mode: 0644 8 | register: tailscale_key 9 | 10 | - name: Add tailscale apt repository 11 | ansible.builtin.apt_repository: 12 | repo: "deb [signed-by={{ tailscale_key.dest }}] https://pkgs.tailscale.com/stable/ubuntu {{ ansible_distribution_release | lower }} main" 13 | state: present 14 | filename: tailscale 15 | 16 | - name: Install tailscale 17 | ansible.builtin.apt: 18 | name: tailscale 19 | update_cache: yes 20 | 21 | - name: Make sure that tailscaled is enabled 22 | ansible.builtin.service: 23 | name: tailscaled 24 | state: started 25 | enabled: yes 26 | -------------------------------------------------------------------------------- /roles/tailscale/tasks/main.yml: -------------------------------------------------------------------------------- 1 | - include_tasks: install.yml 2 | - include_tasks: configure.yml 3 | -------------------------------------------------------------------------------- /run.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # 3 | # Tasks and roles for all hosts 4 | # 5 | - hosts: mona 6 | gather_facts: no 7 | 8 | pre_tasks: 9 | - import_tasks: tasks/ssh_juggle_port.yml 10 | tags: 11 | - always 12 | - port 13 | 14 | - hosts: fleet 15 | become: yes 16 | 17 | roles: 18 | # 19 | # Basics 20 | # 21 | - role: system 22 | tags: 23 | - system 24 | 25 | - role: neovim 26 | tags: 27 | - neovim 28 | 29 | - role: geerlingguy.security 30 | tags: 31 | - security 32 | 33 | - role: geerlingguy.docker 34 | tags: 35 | - docker 36 | 37 | - role: chriswayg.msmtp-mailer 38 | tags: 39 | - msmtp 40 | 41 | - name: containers 42 | tags: 43 | - containers 44 | 45 | - role: tailscale 46 | when: tailscale_enabled | default(false) 47 | tags: 48 | - tailscale 49 | -------------------------------------------------------------------------------- /tasks/repo_arch.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Set amd64 arch 3 | set_fact: 4 | repo_arch: amd64 5 | when: ansible_architecture == "x86_64" 6 | 7 | - name: Set arm64 arch 8 | set_fact: 9 | repo_arch: "{{ ansible_architecture }}" 10 | when: ansible_architecture == "arm64" 11 | -------------------------------------------------------------------------------- /tasks/ssh_juggle_port.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: SSH Port Juggle | Try connecting via SSH 3 | wait_for_connection: 4 | timeout: 5 5 | ignore_errors: true 6 | register: _ssh_port_result 7 | 8 | - name: SSH Port Juggle | Set the ansible_port to the fallback default port 9 | set_fact: 10 | ansible_ssh_port: "22" 11 | when: 12 | - _ssh_port_result is failed 13 | 14 | - name: SSH Port Juggle | Try connecting again 15 | wait_for_connection: 16 | timeout: 5 17 | ignore_errors: true 18 | register: _ssh_port_default_result 19 | when: 20 | - _ssh_port_result is failed 21 | 22 | 23 | - name: SSH Port Juggle | Set the ansible_port to the fallback default port and credentials 24 | set_fact: 25 | ansible_ssh_port: "22" 26 | ansible_ssh_user: "pi" 27 | ansible_ssh_password: "raspberry" 28 | when: 29 | - _ssh_port_result is failed 30 | - _ssh_port_default_result is failed 31 | 32 | - name: Try default credentials (for Raspberry Pi) 33 | wait_for_connection: 34 | timeout: 5 35 | ignore_errors: true 36 | register: _ssh_port_default_cred_result 37 | when: 38 | - _ssh_port_result is failed 39 | - _ssh_port_default_result is failed 40 | 41 | - name: SSH Port Juggle | Try root 42 | set_fact: 43 | ansible_ssh_port: "22" 44 | ansible_ssh_user: "root" 45 | when: 46 | - _ssh_port_result is failed 47 | - _ssh_port_default_result is failed 48 | - _ssh_port_default_cred_result is failed 49 | 50 | 51 | - name: Try root 52 | wait_for_connection: 53 | timeout: 5 54 | ignore_errors: true 55 | register: _ssh_port_default_cred_result 56 | when: 57 | - _ssh_port_result is failed 58 | - _ssh_port_default_result is failed 59 | - _ssh_port_default_cred_result is failed 60 | 61 | - name: SSH Port Juggle | Fail 62 | fail: msg="Neither the configured ansible_port {{ ansible_port }} nor the fallback port 22 were reachable" 63 | when: 64 | - _ssh_port_result is failed 65 | - _ssh_port_default_result is defined 66 | - _ssh_port_default_result is failed 67 | - _ssh_port_default_cred_result is defined 68 | - _ssh_port_default_cred_result is failed 69 | - _ssh_port_root_result is defined 70 | - _ssh_port_root_result is failed 71 | -------------------------------------------------------------------------------- /upload_iso.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: all 3 | gather_facts: yes 4 | become: no 5 | 6 | roles: 7 | - role: notthebee.ubuntu_autoinstall 8 | --------------------------------------------------------------------------------