created role for bootstrapping a Linode VPS via API or a LAN homeserver

This commit is contained in:
2026-05-26 16:04:41 -04:00
parent c3e1f95350
commit ba5aa5e8d4
16 changed files with 782 additions and 0 deletions

View File

@@ -0,0 +1,125 @@
#SPDX-License-Identifier: MIT-0
---
# tasks file for roles/init-vps
- name: Creating prerequisite directory tree
ansible.builtin.file:
path: "{{ ansible_facts['user_dir'] }}/.local/bin"
recurse: true
state: directory
- name: Creating prerequisite directory tree
ansible.builtin.file:
path: "{{ ansible_facts['user_dir'] }}/downloads/archives/released"
recurse: true
state: directory
- name: Installing Linux software
when: ansible_facts["system"] == "Linux"
block:
- name: Installing software using Debian package manager
when: ansible_facts["os_family"] == "Debian"
become: true
block:
- name: Registering a package signing key
when: item.key is defined and item.key_path is defined
ansible.builtin.get_url:
url: "{{ item.key }}"
dest: "{{ item.key_path | default('/etc/apt/keyrings/') }}"
owner: root
group: root
mode: "644"
force: true
backup: true
loop: "{{ pkgs.mngr.core + pkgs.mngr.userspace | rejectattr('key', 'search', '\\.deb$') }}"
- name: Installing a package signing key
when: item.key is defined
ansible.builtin.apt:
deb: "{{ item.key }}"
state: present
loop: "{{ pkgs.mngr.core + pkgs.mngr.userspace | selectattr('key', 'search', '\\.deb$') }}"
- name: Registering a package source
when: item.src_entry is defined and item.src_path is defined
ansible.builtin.copy:
content: "{{ item.src_entry }}"
dest: "{{ item.src_path }}"
owner: root
group: root
mode: "644"
force: true
backup: true
loop: "{{ pkgs.mngr.core + pkgs.mngr.userspace }}"
- name: Installing a local package in managed node
when: item.uri is defined
ansible.builtin.apt:
deb: "{{ item.uri }}"
update_cache: true
state: present
notify: "{{ item.name }}"
loop: "{{ pkgs.mngr.core + pkgs.mngr.userspace | selectattr('uri', 'search', '\\.deb$') }}"
- name: Installing a package
when: item.name is defined and item.uri is undefined
ansible.builtin.package:
name: "{{ item.name }}"
update_cache: true
state: latest
notify: "{{ item.name }}" # @TODO create corresponding roles/init-vps handlers
loop: "{{ pkgs.mngr.core + pkgs.mngr.userspace | rejectattr('uri', 'search', '\\.deb$') }}"
tags:
- get_mngr_pkgs
- name: Installing software by executing installation shell scripts
when: item.src is defined
block:
- name: Acquiring installation shell script
ansible.builtin.get_url:
url: "{{ item.src }}"
dest: "{{ ansible_facts['user_dir'] }}/.local/bin/{{ item.name }}-install.sh"
force: true
backup: true
mode: "744"
loop: "{{ pkgs.script.core + pkgs.script.userspace }}"
register: install_scripts
- name: Executing a shell-scripted installation process
become: true
ansible.builtin.shell:
cmd: "{{ item.dest }}"
notify: "{{ (pkgs.script.core + pkgs.script.userspace)[idx].name }}"
loop: "{{ install_scripts.results }}"
loop_control:
index_var: idx
tags:
- get_script_pkgs
# @TODO complete below block task
- name: Installing software by building it from source archives
block:
- name: Acquiring software source archive
ansible.builtin.get_url:
url: "{{ item.src }}"
dest: "{{ ansible_facts['user_dir'] }}/downloads/archives/"
force: true
backup: true
mode: "644"
loop: "{{ pkgs.archive.core + pkgs.archive.userspace }}"
register: archived_builds
- name: Unarchiving software build archive
ansible.builtin.unarchive:
src: "{{ item.dest }}"
remote_src: true
dest: "{{ ansible_facts['user_dir'] }}/downloads/archives/released/{{ (pkgs.archive.core + pkgs.archive.userspace)[idx].name }}/"
notify: "{{ (pkgs.archive.core + pkgs.archive.userspace)[idx].name }}"
loop: "{{ archived_builds.results }}"
loop_control:
index_var: idx
tags:
- get_archive_pkgs
- name: Installing software from source git repositories
block:
- name: Clone git bare repository
ansible.builtin.git:
repo: "{{ item.src }}"
dest: "{{ ansible_facts['user_dir'] }}/repos/.foreign/{{ item.name }}"
version: "{{ item.branch }}"
clone: true
single_branch: true
notify: "{{ item.name }}"
loop: "{{ pkgs.git_repos.core + pkgs.git_repos.userspace }}"
register: installation_repos
tags:
- get_git_pkgs

View File

@@ -0,0 +1,41 @@
#SPDX-License-Identifier: MIT-0
---
# tasks file for roles/init-vps
# @TODO complete below tasks
- name: Checking whether administrative login used
when: ansible_facts["user_id"] not in (admins | map(attribute="username") | list)
ansible.builtin.fail:
msg: Administrative user does not exist on managed node
- name: Prohibiting SSH root login
when: harden
become: true
ansible.builtin.copy:
src: sshd_config.d/denyroot.conf
dest: /etc/ssh/sshd_config.d/denyroot.conf
owner: root
group: root
mode: "644"
force: true
backup: true
validate: "sshd -t %s"
- name: Create groups for FTP services
when: "'internal-sftp' in item.service or 'proftpd' in item.service or 'vsftpd' in item.service"
become: true
ansible.builtin.group:
name: "{{ item.username }}"
system: true
state: present
loop: "{{ sys_users }}"
register: ftp_groups
- name: Configuring SFTP for FTP group
become: true
ansible.builtin.template:
src: sshd_config.d/sftp.conf.j2
dest: /etc/ssh/sshd_config.d/sftp.conf
owner: root
group: root
mode: "644"
force: true
backup: true
validate: "sshd -t %s"
register: configured_sftp

View File

@@ -0,0 +1,443 @@
#SPDX-License-Identifier: MIT-0
---
# tasks file for roles/init-vps
# @NOTE server deployment method is based on task tags compiled herein
# @TODO review 'loop' task attribute return values and make compliant changes
- name: Finding SSH public keys for root
ansible.builtin.find:
paths: "{{ cnode_homedir | default('/home/' ~ ansible_user ~ '/.ssh') }}" # @TODO define 'cnode_homedir' in playbook
patterns: "{{ ['^'] | product(keys) | map('join') | list }}"
file_type: file
use_regex: true
register: ssh_keypairs
- name: Bootstrapping VPS
block:
- name: Creating VPS via Linode VPS service API
block:
- name: Creating the VPS
linode.cloud.instance:
api_token: "{{ token }}"
label: "{{ instance }}"
type: g6-standard-2
image: "{{ operating_system }}"
disk_encryption: enabled
region: "{{ origin }}"
private_ip: true
root_pass: "{{ password }}"
authorized_keys: "{{ ssh_keypairs.files | selectattr('path', 'search', '\\.pub$') | map(attribute='path') | map('lookup', 'file') | list }}"
state: present
register: new_instance
- name: Waiting for that VPS to come online
delegate_to: "{{ new_instance.instance[ip_pref][0] }}"
delegate_facts: true
ansible.builtin.wait_for_connection:
delay: 20
timeout: 300
vars:
ansible_ssh_private_key_file: "{{ chosen_privkey | default(ssh_keypairs.files | rejectattr('path', 'search', '\\.pub$') | map(attribute='path') | list | random) }}" # @TODO define 'chosen_privkey'in playbook
ansible_user: root
- name: Checking if that VPS has required operating system
delegate_to: "{{ new_instance.instance[ip_pref][0] }}"
delegate_facts: true
when: ansible_facts["system"] != "Linux"
ansible.builtin.fail:
msg: Unsupported operating system found
vars:
ansible_ssh_private_key_file: "{{ chosen_privkey | default(ssh_keypairs.files | rejectattr('path', 'search', '\\.pub$') | map(attribute='path') | list | random) }}" # @TODO define 'chosen_privkey'in playbook
ansible_user: root
- name: Checking if that VPS has required Linux distro
delegate_to: "{{ new_instance.instance[ip_pref][0] }}"
delegate_facts: true
when: ansible_facts["system"] == "Linux" and ansible_facts["os_family"] != "Debian"
ansible.builtin.fail:
msg: Unsupported Linux distro found
vars:
ansible_ssh_private_key_file: "{{ chosen_privkey | default(ssh_keypairs.files | rejectattr('path', 'search', '\\.pub$') | map(attribute='path') | list | random) }}" # @TODO define 'chosen_privkey'in playbook
ansible_user: root
tags:
- linode
tags:
- vps
- name: Bootstrapping homeserver
block:
- name: Installing operating system or distro in server
when: operating_system is defined
block:
- name: Creating a server
block: []
tags:
- unimplemented
- name: Waiting for that VPS to come online
delegate_to: "{{ hostvars[instance]['ansible_default_' ~ ip_pref].address }}"
delegate_facts: true
ansible.builtin.wait_for_connection:
delay: 20
timeout: 300
vars:
ansible_user: root
- name: Checking if that server has required operating system
delegate_to: "{{ hostvars[instance]['ansible_default_' ~ ip_pref].address }}"
delegate_facts: true
when: ansible_facts["system"] != "Linux"
ansible.builtin.fail:
msg: Unsupported operating system found
vars:
ansible_user: root
- name: Checking if that server has required Linux distro
delegate_to: "{{ hostvars[instance]['ansible_default_' ~ ip_pref].address }}"
delegate_facts: true
when: ansible_facts["system"] == "Linux" and ansible_facts["os_family"] != "Debian"
ansible.builtin.fail:
msg: Unsupported Linux distro found
vars:
ansible_user: root
- name: Providing authorized keys for server root account
delegate_to: "{{ hostvars[instance]['ansible_default_' ~ ip_pref].address }}"
delegate_facts: true
ansible.posix.authorized_key:
user: "{{ ansible_facts['user_id'] }}"
key: "{{ lookup('file', item) }}"
state: present
vars:
ansible_root: root
loop: "{{ ssh_keypairs.files | selectattr('path', 'search', '\\.pub$') | map(attribute='path') | list }}"
tags:
- lan
- name: Starting SSH hardening
when: harden
delegate_facts: true
block:
- name: Hardening SSH service for the Linode VPS
delegate_to: "{{ new_instance.instance[ip_pref][0] }}"
ansible.builtin.copy:
src: sshd_config.d/harden.conf
dest: /etc/ssh/sshd_config.d/harden.conf
owner: root
group: root
mode: "644"
force: true
backup: true
validate: "sshd -t %s"
vars:
ansible_ssh_private_key_file: "{{ chosen_privkey | default(ssh_keypairs.files | rejectattr('path', 'search', '\\.pub$') | map(attribute='path') | list | random) }}" # @TODO define 'chosen_privkey'in playbook
ansible_user: root
register: ssh_hardened
tags:
- linode
- name: Hardening SSH service for the server
delegate_to: "{{ hostvars[instance]['ansible_default_' ~ ip_pref].address }}"
ansible.builtin.copy:
src: sshd_config.d/harden.conf
dest: /etc/ssh/sshd_config.d/harden.conf
owner: root
group: root
mode: "644"
force: true
backup: true
validate: "sshd -t %s"
vars:
ansible_ssh_private_key_file: "{{ chosen_privkey | default(ssh_keypairs.files | rejectattr('path', 'search', '\\.pub$') | map(attribute='path') | list | random) }}" # @TODO define 'chosen_privkey'in playbook
ansible_user: root
register: ssh_hardened
tags:
- lan
- name: Starting user and group creation for SSH access
block:
- name: Creating group remote for managing SSH access
delegate_facts: true
block:
- name: In the Linode VPS
delegate_to: "{{ new_instance.instance[ip_pref][0] }}"
ansible.builtin.group:
name: remote
system: true
state: present
vars:
ansible_ssh_private_key_file: "{{ chosen_privkey | default(ssh_keypairs.files | rejectattr('path', 'search', '\\.pub$') | map(attribute='path') | list | random) }}" # @TODO define 'chosen_privkey'in playbook
ansible_user: root
register: remote_group
tags:
- linode
- name: In the server
delegate_to: "{{ hostvars[instance]['ansible_default_' ~ ip_pref].address }}"
ansible.builtin.group:
name: remote
system: true
state: present
vars:
ansible_ssh_private_key_file: "{{ chosen_privkey | default(ssh_keypairs.files | rejectattr('path', 'search', '\\.pub$') | map(attribute='path') | list | random) }}" # @TODO define 'chosen_privkey'in playbook
ansible_user: root
register: remote_group
tags:
- lan
- name: Creating an administrative user
delegate_facts: true
when: ansible_facts["system"] == "Linux"
block:
- name: In the Linode VPS
delegate_to: "{{ new_instance.instance[ip_pref][0] }}"
ansible.builtin.user:
name: "{{ item.username }}"
comment: administrator
group: "{{ item.username }}"
groups:
- "{{ remote_group.name }}"
- sudo # @NOTE used by Debian
append: true
generate_ssh_key: true
create_home: true
password: "{{ item.password }}"
shell: "/bin/bash"
vars:
ansible_ssh_private_key_file: "{{ chosen_privkey | default(ssh_keypairs.files | rejectattr('path', 'search', '\\.pub$') | map(attribute='path') | list | random) }}" # @TODO define 'chosen_privkey'in playbook
ansible_user: root
loop: "{{ admins }}"
register: admin_users
tags:
- linode
- name: In the server
delegate_to: "{{ hostvars[instance]['ansible_default_' ~ ip_pref].address }}"
ansible.builtin.user:
name: "{{ item.username }}"
comment: administrator
group: "{{ item.username }}"
groups:
- "{{ remote_group.name }}"
- sudo # @NOTE used by Debian
append: true
generate_ssh_key: true
create_home: true
password: "{{ item.password }}"
shell: "/bin/bash"
vars:
ansible_ssh_private_key_file: "{{ chosen_privkey | default(ssh_keypairs.files | rejectattr('path', 'search', '\\.pub$') | map(attribute='path') | list | random) }}" # @TODO define 'chosen_privkey'in playbook
ansible_user: root
loop: "{{ admins }}"
register: admin_users
tags:
- lan
- name: Finding SSH public keys for an administrative user
when: item.username in (admin_users.results | map(attribute="name") | list)
ansible.builtin.find:
paths: "{{ cnode_homedir | default('/home/' ~ ansible_user ~ '/.ssh') }}" # @TODO define 'cnode_homedir' in playbook
patterns: "{{ ['^'] | product(item.keys) | map('join') | list }}"
file_type: file
use_regex: true
loop: "{{ admins }}"
register: admin_ssh_keypairs
- name: Authorizing SSH public key for an administrative user
delegate_facts: true
block:
- name: In the Linode VPS
delegate_to: "{{ new_instance.instance[ip_pref][0] }}"
ansible.posix.authorized_key:
user: "{{ admin_users.results[idx] }}"
key: "{{ admin_ssh_keypairs.results[idx].files | selectattr('path', 'search', '\\.pub$') | map(attribute='path') | map('lookup', 'file') | list | map('join','\n') }}"
state: present
vars:
ansible_ssh_private_key_file: "{{ chosen_privkey | default(ssh_keypairs.files | rejectattr('path', 'search', '\\.pub$') | map(attribute='path') | list | random) }}" # @TODO define 'chosen_privkey'in playbook
ansible_user: root
loop: "{{ admin_users.results }}"
loop_control:
index_var: idx
register: ssh_authorizations
tags:
- linode
- name: In the server
delegate_to: "{{ hostvars[instance]['ansible_default_' ~ ip_pref].address }}"
ansible.posix.authorized_key:
user: "{{ admin_users.results[idx] }}"
key: "{{ admin_ssh_keypairs.results[idx].files | selectattr('path', 'search', '\\.pub$') | map(attribute='path') | map('lookup', 'file') | list | map('join','\n') }}"
state: present
vars:
ansible_ssh_private_key_file: "{{ chosen_privkey | default(ssh_keypairs.files | rejectattr('path', 'search', '\\.pub$') | map(attribute='path') | list | random) }}" # @TODO define 'chosen_privkey'in playbook
ansible_user: root
loop: "{{ admin_users.results }}"
loop_control:
index_var: idx
register: ssh_authorizations
tags:
- lan
- name: Allowing sole SSH access to users in group remote
delegate_facts: true
block:
- name: In Linode VPS
delegate_to: "{{ new_instance.instance[ip_pref][0] }}"
ansible.builtin.template:
src: sshd_config.d/allowance.conf.j2 # @TODO create corresponding role template file
dest: /etc/ssh/sshd_config.d/allowance.conf
owner: root
group: root
mode: "644"
force: true
backup: true
validate: "sshd -t %s"
vars:
ansible_ssh_private_key_file: "{{ chosen_privkey | default(ssh_keypairs.files | rejectattr('path', 'search', '\\.pub$') | map(attribute='path') | list | random) }}" # @TODO define 'chosen_privkey'in playbook
ansible_user: root
register: ssh_gatekept
tags:
- linode
- name: In Linode VPS
delegate_to: "{{ hostvars[instance]['ansible_default_' ~ ip_pref].address }}"
ansible.builtin.template:
src: sshd_config.d/allowance.conf.j2 # @TODO create corresponding role template file
dest: /etc/ssh/sshd_config.d/allowance.conf
owner: root
group: root
mode: "644"
force: true
backup: true
validate: "sshd -t %s"
vars:
ansible_ssh_private_key_file: "{{ chosen_privkey | default(ssh_keypairs.files | rejectattr('path', 'search', '\\.pub$') | map(attribute='path') | list | random) }}" # @TODO define 'chosen_privkey'in playbook
ansible_user: root
register: ssh_gatekept
tags:
- lan
- name: Setting approved SSH authentication procedures
when: harden
delegate_facts: true
block:
- name: In the Linode VPS
delegate_to: "{{ new_instance.instance[ip_pref][0] }}"
ansible.builtin.copy:
src: sshd_config.d/auth.conf
dest: /etc/ssh/sshd_config.d/auth.conf
owner: root
group: root
mode: "644"
force: true
backup: true
validate: "sshd -t %s"
vars:
ansible_ssh_private_key_file: "{{ chosen_privkey | default(ssh_keypairs.files | rejectattr('path', 'search', '\\.pub$') | map(attribute='path') | list | random) }}" # @TODO define 'chosen_privkey'in playbook
ansible_user: root
register: ssh_authenticator
tags:
- linode
- name: In the server
delegate_to: "{{ hostvars[instance]['ansible_default_' ~ ip_pref].address }}"
ansible.builtin.copy:
src: sshd_config.d/auth.conf
dest: /etc/ssh/sshd_config.d/auth.conf
owner: root
group: root
mode: "644"
force: true
backup: true
validate: "sshd -t %s"
vars:
ansible_ssh_private_key_file: "{{ chosen_privkey | default(ssh_keypairs.files | rejectattr('path', 'search', '\\.pub$') | map(attribute='path') | list | random) }}" # @TODO define 'chosen_privkey'in playbook
ansible_user: root
register: ssh_authenticator
tags:
- lan
- name: Installing core packages
delegate_facts: true
block:
- name: In the Linode VPS
delegate_to: "{{ new_instance.instance[ip_pref][0] }}"
block:
- name: Registering a package signing key
when: ansible_facts["os_family"] == "Debian" and item.key is defined and item.key_path is defined
ansible.builtin.get_url:
url: "{{ item.key }}"
dest: "{{ item.key_path | default('/etc/apt/keyrings/') }}"
owner: root
group: root
mode: "644"
force: true
backup: true
loop: "{{ pkgs.mngr.core | rejectattr('key', 'search', '\\.deb$') }}"
- name: Installing a package signing key
when: ansible_facts["os_family"] == "Debian" and item.key is defined
ansible.builtin.apt:
deb: "{{ item.key }}"
state: latest
loop: "{{ pkgs.mngr.core | selectattr('key', 'search', '\\.deb$') }}"
- name: Registering a package source
when: ansible_facts["os_family"] == "Debian" and item.src_entry is defined and item.src_path is defined
ansible.builtin.copy:
content: "{{ item.src_entry }}"
dest: "{{ item.src_path }}"
owner: root
group: root
mode: "644"
force: true
backup: true
loop: "{{ pkgs.mngr.core }}"
- name: Installing a local package in managed node
when: ansible_facts["os_family"] == "Debian" and item.uri is defined
ansible.builtin.apt:
deb: "{{ item.uri }}"
update_cache: true
state: latest
loop: "{{ pkgs.mngr.core | selectattr('uri', 'search', '\\.deb$') }}"
- name: Installing a package
when: item.name is defined and item.uri is undefined
ansible.builtin.package:
name: "{{ item.name }}"
update_cache: true
state: latest
notify: "{{ item.name }}" # @TODO create corresponding roles/init-vps handlers
loop: "{{ pkgs.mngr.core| rejectattr('uri', 'search', '\\.deb$') }}"
vars:
ansible_ssh_private_key_file: "{{ chosen_privkey | default(ssh_keypairs.files | rejectattr('path', 'search', '\\.pub$') | map(attribute='path') | list | random) }}" # @TODO define 'chosen_privkey'in playbook
ansible_user: root
tags:
- linode
- name: In the server
delegate_to: "{{ hostvars[instance]['ansible_default_' ~ ip_pref].address }}"
block:
- name: Registering a package signing key
when: ansible_facts["os_family"] == "Debian" and item.key is defined and item.key_path is defined
ansible.builtin.get_url:
url: "{{ item.key }}"
dest: "{{ item.key_path | default('/etc/apt/keyrings/') }}"
owner: root
group: root
mode: "644"
force: true
backup: true
loop: "{{ pkgs.mngr.core | rejectattr('key', 'search', '\\.deb$') }}"
- name: Installing a package signing key
when: ansible_facts["os_family"] == "Debian" and item.key is defined
ansible.builtin.apt:
deb: "{{ item.key }}"
state: latest
loop: "{{ pkgs.mngr.core | selectattr('key', 'search', '\\.deb$') }}"
- name: Registering a package source
when: ansible_facts["os_family"] == "Debian" and item.src_entry is defined and item.src_path is defined
ansible.builtin.copy:
content: "{{ item.src_entry }}"
dest: "{{ item.src_path }}"
owner: root
group: root
mode: "644"
force: true
backup: true
loop: "{{ pkgs.mngr.core }}"
- name: Installing a local package in managed node
when: ansible_facts["os_family"] == "Debian" and item.uri is defined
ansible.builtin.apt:
deb: "{{ item.uri }}"
update_cache: true
state: latest
notify: "{{ item.name }}"
loop: "{{ pkgs.mngr.core | selectattr('uri', 'search', '\\.deb$') }}"
- name: Installing a package
when: item.name is defined and item.uri is undefined
ansible.builtin.package:
name: "{{ item.name }}"
update_cache: true
state: latest
notify: "{{ item.name }}" # @TODO create corresponding roles/init-vps handlers
loop: "{{ pkgs.mngr.core| rejectattr('uri', 'search', '\\.deb$') }}"
vars:
ansible_ssh_private_key_file: "{{ chosen_privkey | default(ssh_keypairs.files | rejectattr('path', 'search', '\\.pub$') | map(attribute='path') | list | random) }}" # @TODO define 'chosen_privkey'in playbook
ansible_user: root
tags:
- lan
tags:
- get_pkgs