initial commit

This commit is contained in:
2026-06-19 16:53:14 -04:00
commit 108512762f
12 changed files with 664 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAFMM1IPHOss23GMLG4SSpyXxgflbYxXzQmXsyCbQ77U ansible-control
+5
View File
@@ -0,0 +1,5 @@
# setup-ansible-service-account.sh
## Usage
`sudo setup-ansible-service-account.sh ./ansible-ed25519.pub`
@@ -0,0 +1,68 @@
#!/usr/bin/env bash
set -euo pipefail
ANSIBLE_USER="ansible"
ANSIBLE_HOME="/home/${ANSIBLE_USER}"
SSH_DIR="${ANSIBLE_HOME}/.ssh"
AUTHORIZED_KEYS="${SSH_DIR}/authorized_keys"
SUDOERS_FILE="/etc/sudoers.d/${ANSIBLE_USER}"
if [[ $# -ne 1 ]]; then
echo "Usage: $0 /path/to/ansible-control-node.pub" >&2
exit 1
fi
KEY_FILE="$1"
if [[ ! -f "$KEY_FILE" ]]; then
echo "[ERROR] Public key file not found: $KEY_FILE" >&2
exit 1
fi
PUBLIC_KEY="$(< "$KEY_FILE")"
if ! grep -Eq '^(ssh-ed25519|ssh-rsa|ecdsa-sha2-nistp256|ecdsa-sha2-nistp384|ecdsa-sha2-nistp521) [A-Za-z0-9+/=]+([[:space:]].*)?$' <<< "$PUBLIC_KEY"; then
echo "[ERROR] Invalid OpenSSH public key format" >&2
exit 1
fi
if [[ "$(id -u)" -ne 0 ]]; then
SUDO="sudo"
else
SUDO=""
fi
$SUDO true
if ! id "$ANSIBLE_USER" >/dev/null 2>&1; then
$SUDO useradd --create-home --shell /bin/bash "$ANSIBLE_USER"
fi
$SUDO passwd -l "$ANSIBLE_USER" >/dev/null
$SUDO install -d -o "$ANSIBLE_USER" -g "$ANSIBLE_USER" -m 700 "$SSH_DIR"
$SUDO touch "$AUTHORIZED_KEYS"
$SUDO chown "$ANSIBLE_USER:$ANSIBLE_USER" "$AUTHORIZED_KEYS"
$SUDO chmod 600 "$AUTHORIZED_KEYS"
if ! $SUDO grep -Fxq "$PUBLIC_KEY" "$AUTHORIZED_KEYS"; then
echo "$PUBLIC_KEY" | $SUDO tee -a "$AUTHORIZED_KEYS" >/dev/null
fi
TMP_AUTH_KEYS="$(mktemp)"
$SUDO awk 'NF && !seen[$0]++' "$AUTHORIZED_KEYS" > "$TMP_AUTH_KEYS"
$SUDO install -o "$ANSIBLE_USER" -g "$ANSIBLE_USER" -m 600 "$TMP_AUTH_KEYS" "$AUTHORIZED_KEYS"
rm -f "$TMP_AUTH_KEYS"
TMP_SUDOERS="$(mktemp)"
cat > "$TMP_SUDOERS" <<EOF
${ANSIBLE_USER} ALL=(ALL) NOPASSWD: ALL
EOF
$SUDO visudo -cf "$TMP_SUDOERS" >/dev/null
$SUDO install -o root -g root -m 0440 "$TMP_SUDOERS" "$SUDOERS_FILE"
rm -f "$TMP_SUDOERS"
$SUDO visudo -cf "$SUDOERS_FILE" >/dev/null
echo "[OK] Ansible service account configured successfully"
+14
View File
@@ -0,0 +1,14 @@
[defaults]
inventory = ./inventories/prod/hosts.yml
log_path = /var/log/ansible/ansible.log
host_key_checking = True
retry_files_enabled = False
stdout_callback = ansible.builtin.default
result_format = yaml
forks = 10
timeout = 30
[privilege_escalation]
become = True
become_method = sudo
become_ask_pass = False
+10
View File
@@ -0,0 +1,10 @@
all:
children:
ubuntu_servers:
hosts:
nbaslinv01:
ansible_host: 100.94.73.127
vars:
ansible_user: ansible
ansible_ssh_private_key_file: /home/ansible/.ssh/ansible_ed25519
ansible_python_interpreter: /usr/bin/python3
+32
View File
@@ -0,0 +1,32 @@
---
- name: Update and upgrade wolfywebserver02
hosts: wolfywebserver02
become: true
gather_facts: true
tasks:
- name: Update apt cache
ansible.builtin.apt:
update_cache: true
cache_valid_time: 3600
- name: Upgrade installed packages
ansible.builtin.apt:
upgrade: dist
- name: Remove unneeded packages
ansible.builtin.apt:
autoremove: true
- name: Check if reboot is required
ansible.builtin.stat:
path: /var/run/reboot-required
register: reboot_required
- name: Reboot server if required
ansible.builtin.reboot:
pre_reboot_delay: 60
post_reboot_delay: 60
msg: "Rebooting in 60 seconds (intiated by Ansible)"
reboot_timeout: 3600
when: reboot_required.stat.exists
+94
View File
@@ -0,0 +1,94 @@
---
- name: Patch Prod Ubuntu servers and report to Discord
hosts: prod_ubuntu_servers
become: true
gather_facts: true
vars:
discord_webhook_id: "1507643788326338630"
discord_webhook_token: "TkbDnf6naR7KxhqLDIvGJyeesq9OTglxHrLHyy3d6_1WiyI-bbzUGDeVu1yCjZN7fCdd"
tasks:
- name: Update apt cache
ansible.builtin.apt:
update_cache: true
cache_valid_time: 3600
register: apt_cache_result
- name: Upgrade installed packages
ansible.builtin.apt:
upgrade: dist
register: apt_upgrade_result
- name: Remove unneeded packages
ansible.builtin.apt:
autoremove: true
register: apt_autoremove_result
- name: Check if reboot is required
ansible.builtin.stat:
path: /var/run/reboot-required
register: reboot_required_file
- name: Reboot server if required
ansible.builtin.reboot:
pre_reboot_delay: 60
post_reboot_delay: 60
reboot_timeout: 600
msg: "Rebooting in 60 seconds (intiated by Ansible)"
when: reboot_required_file.stat.exists
register: reboot_result
- name: Build per-host summary
ansible.builtin.set_fact:
patch_summary: |
Host: {{ inventory_hostname }}
Apt Cache Updated: {{ 'Yes' if (apt_cache_result.changed | default(false)) else 'No' }}
Packages Upgraded: {{ 'Yes' if (apt_upgrade_result.changed | default(false)) else 'No' }}
Autoremove Changed: {{ 'Yes' if (apt_autoremove_result.changed | default(false)) else 'No' }}
Reboot Required: {{ 'Yes' if (reboot_required_file.stat.exists | default(false)) else 'No' }}
Reboot Performed: {{ 'Yes' if (reboot_result.changed | default(false)) else 'No' }}
- name: Send Discord report
community.general.discord:
webhook_id: "{{ discord_webhook_id }}"
webhook_token: "{{ discord_webhook_token }}"
username: "Ansible"
avatar_url: "https://opreviews.anime-pictures.net/49f/49f1117eec83be2bd5af12772a70d2e4_bp.avif"
content: |
======================================
Production Servers Update Report
Generated: {{ lookup('pipe', 'date +%Y-%m-%dT%H:%M:%SZ') }}
======================================
Total Hosts: {{ ansible_play_hosts_all | length }}
{% for host in ansible_play_hosts_all | sort %}
{{ hostvars[host].patch_summary | default('Host: ' ~ host ~ '\n No summary generated\n') }}
{% endfor %}
======================================
Summary
======================================
Hosts Requiring Reboot: {{
ansible_play_hosts_all
| select('in', ansible_play_hosts_all)
| map('extract', hostvars)
| selectattr('reboot_required_file.stat.exists', 'defined')
| selectattr('reboot_required_file.stat.exists')
| list
| length
}}
Hosts Rebooted: {{
ansible_play_hosts_all
| select('in', ansible_play_hosts_all)
| map('extract', hostvars)
| selectattr('reboot_result.changed', 'defined')
| selectattr('reboot_result.changed')
| list
| length
}}
delegate_to: localhost
run_once: true
become: false
+95
View File
@@ -0,0 +1,95 @@
---
- name: Patch Ubuntu servers and report to Discord
hosts: test_ubuntu_servers
become: true
gather_facts: true
vars:
# discord_webhook_url: "https://discord.com/api/webhooks/1447800027941437554/WyjT6AH3Pn_1Wgwah7AjMm47l2UVpZCMZ0DbMnpEr3vKPImHZkkmZDtRZrlhPl4MnW73
discord_webhook_id: "1507643788326338630"
discord_webhook_token: "TkbDnf6naR7KxhqLDIvGJyeesq9OTglxHrLHyy3d6_1WiyI-bbzUGDeVu1yCjZN7fCdd"
tasks:
- name: Update apt cache
ansible.builtin.apt:
update_cache: true
cache_valid_time: 3600
register: apt_cache_result
- name: Upgrade installed packages
ansible.builtin.apt:
upgrade: dist
register: apt_upgrade_result
- name: Remove unneeded packages
ansible.builtin.apt:
autoremove: true
register: apt_autoremove_result
- name: Check if reboot is required
ansible.builtin.stat:
path: /var/run/reboot-required
register: reboot_required_file
- name: Reboot server if required
ansible.builtin.reboot:
pre_reboot_delay: 60
post_reboot_delay: 60
reboot_timeout: 600
msg: "Rebooting in 60 seconds (intiated by Ansible)"
when: reboot_required_file.stat.exists
register: reboot_result
- name: Build per-host summary
ansible.builtin.set_fact:
patch_summary: |
Host: {{ inventory_hostname }}
Apt Cache Updated: {{ 'Yes' if (apt_cache_result.changed | default(false)) else 'No' }}
Packages Upgraded: {{ 'Yes' if (apt_upgrade_result.changed | default(false)) else 'No' }}
Autoremove Changed: {{ 'Yes' if (apt_autoremove_result.changed | default(false)) else 'No' }}
Reboot Required: {{ 'Yes' if (reboot_required_file.stat.exists | default(false)) else 'No' }}
Reboot Performed: {{ 'Yes' if (reboot_result.changed | default(false)) else 'No' }}
- name: Send Discord report
community.general.discord:
webhook_id: "{{ discord_webhook_id }}"
webhook_token: "{{ discord_webhook_token }}"
username: "Ansible"
avatar_url: "https://opreviews.anime-pictures.net/49f/49f1117eec83be2bd5af12772a70d2e4_bp.avif"
content: |
======================================
Test Servers Update Report
Generated: {{ lookup('pipe', 'date +%Y-%m-%dT%H:%M:%SZ') }}
======================================
Total Hosts: {{ ansible_play_hosts_all | length }}
{% for host in ansible_play_hosts_all | sort %}
{{ hostvars[host].patch_summary | default('Host: ' ~ host ~ '\n No summary generated\n') }}
{% endfor %}
======================================
Summary
======================================
Hosts Requiring Reboot: {{
ansible_play_hosts_all
| select('in', ansible_play_hosts_all)
| map('extract', hostvars)
| selectattr('reboot_required_file.stat.exists', 'defined')
| selectattr('reboot_required_file.stat.exists')
| list
| length
}}
Hosts Rebooted: {{
ansible_play_hosts_all
| select('in', ansible_play_hosts_all)
| map('extract', hostvars)
| selectattr('reboot_result.changed', 'defined')
| selectattr('reboot_result.changed')
| list
| length
}}
delegate_to: localhost
run_once: true
become: false
+301
View File
@@ -0,0 +1,301 @@
# Ansible Architecture for Wolfy's Server Ecosystem
## Configuration
```INI
[defaults]
inventory = ./inventories/prod/hosts.yml
log_path = /home/wolfy/log/ansible.log
host_key_checking = True
retry_files_enabled = False
stdout_callback = yaml
[privilege_escalation]
become = True
become_method = sudo
become_ask_pass = False
```
The current Ansible configuration is set to log internally and not attempt to retry upon failure (in order to prevent infinite retries on certain tasks since the proper safeties have not been put in place yet). Every server gets an `ansible` user added to it which has passwordless sudo permissions. The account is locked and has a secure password. It uses an ssh key for connecting to the servers.
## Playbooks
### apt-update-test.yml
```YAML
---
- name: Update and upgrade wolfywebserver02
hosts: wolfywebserver02
become: true
gather_facts: true
tasks:
- name: Update apt cache
ansible.builtin.apt:
update_cache: true
cache_valid_time: 3600
- name: Upgrade installed packages
ansible.builtin.apt:
upgrade: dist
- name: Remove unneeded packages
ansible.builtin.apt:
autoremove: true
```
This script is currently setup to connect to `wolfywebserver02` escalate to sudo then run `sudo apt update` then `sudo apt upgrade` then `sudo apt autoremove`.
### update-prod-servers.yml
```YAML
---
- name: Patch Prod Ubuntu servers and report to Discord
hosts: prod_ubuntu_servers
become: true
gather_facts: true
vars:
discord_webhook_id: "1507643788326338630"
discord_webhook_token: "TkbDnf6naR7KxhqLDIvGJyeesq9OTglxHrLHyy3d6_1WiyI-bbzUGDeVu1yCjZN7fCdd"
tasks:
- name: Update apt cache
ansible.builtin.apt:
update_cache: true
cache_valid_time: 3600
register: apt_cache_result
- name: Upgrade installed packages
ansible.builtin.apt:
upgrade: dist
register: apt_upgrade_result
- name: Remove unneeded packages
ansible.builtin.apt:
autoremove: true
register: apt_autoremove_result
- name: Check if reboot is required
ansible.builtin.stat:
path: /var/run/reboot-required
register: reboot_required_file
- name: Reboot server if required
ansible.builtin.reboot:
pre_reboot_delay: 60
post_reboot_delay: 60
reboot_timeout: 600
msg: "Rebooting in 60 seconds (intiated by Ansible)"
when: reboot_required_file.stat.exists
register: reboot_result
- name: Build per-host summary
ansible.builtin.set_fact:
patch_summary: |
Host: {{ inventory_hostname }}
Apt Cache Updated: {{ 'Yes' if (apt_cache_result.changed | default(false)) else 'No' }}
Packages Upgraded: {{ 'Yes' if (apt_upgrade_result.changed | default(false)) else 'No' }}
Autoremove Changed: {{ 'Yes' if (apt_autoremove_result.changed | default(false)) else 'No' }}
Reboot Required: {{ 'Yes' if (reboot_required_file.stat.exists | default(false)) else 'No' }}
Reboot Performed: {{ 'Yes' if (reboot_result.changed | default(false)) else 'No' }}
- name: Send Discord report
community.general.discord:
webhook_id: "{{ discord_webhook_id }}"
webhook_token: "{{ discord_webhook_token }}"
username: "Ansible"
avatar_url: "https://opreviews.anime-pictures.net/49f/49f1117eec83be2bd5af12772a70d2e4_bp.avif"
content: |
========================================
Test Servers Update Report
Generated: {{ lookup('pipe', 'date -u +%Y-%m-%dT%H:%M:%SZ') }}
========================================
Total Hosts: {{ ansible_play_hosts_all | length }}
{% for host in ansible_play_hosts_all | sort %}
{{ hostvars[host].patch_summary | default('Host: ' ~ host ~ '\n No summary generated\n') }}
{% endfor %}
========================================
Summary
========================================
Hosts Requiring Reboot: {{
ansible_play_hosts_all
| select('in', ansible_play_hosts_all)
| map('extract', hostvars)
| selectattr('reboot_required_file.stat.exists', 'defined')
| selectattr('reboot_required_file.stat.exists')
| list
| length
}}
Hosts Rebooted: {{
ansible_play_hosts_all
| select('in', ansible_play_hosts_all)
| map('extract', hostvars)
| selectattr('reboot_result.changed', 'defined')
| selectattr('reboot_result.changed')
| list
| length
}}
delegate_to: localhost
run_once: true
become: false
```
This playbook is designed to connect all `prod_ubuntu_servers` hosts and run update, upgrade, autoremove, and restart if necessary then send out a webhook with details on how each step went on each server.
## Run
### update-prod-servers.sh
```bash
#!/bin/bash
set -euo pipefail
LOG_DIR="/home/wolfy/log/ansible-prod"
mkdir -p "$LOG_DIR"
TIMESTAMP="$(date '+%Y-%m-%d_%H-%M-%S')"
LOG_FILE="$LOG_DIR/ubuntu-prod-update-$TIMESTAMP.log"
exec >>"$LOG_FILE" 2>&1
echo "========================================"
echo "Prod patching run started: $(date -Is)"
echo "========================================"
cd /home/wolfy/ansible
/home/wolfy/ansible-env/bin/ansible-playbook playbooks/update-prod-servers.yml
echo
echo "========================================"
echo "Prod patching run completed: $(date -Is)"
echo "========================================"
```
This wrapper script is ran by `update-prod-servers.service` a systemd service which is triggered by `update-prod-servers.timer` a systemd timer. This is designed to trigger every Monday at 7:30 EST and update all production ubuntu servers using `update-prod-servers.yml`.
#### update-prod-servers.service
```ini
[Unit]
Description=Run weekly production patching playbook
Wants=network-online.target
After=network-online.target
[Service]
Type=oneshot
User=wolfy
Group=wolfy
ExecStart=/home/wolfy/ansible/run/update-prod-servers.sh
WorkingDirectory=/home/wolfy/ansible
```
#### update-prod-servers.timer
```ini
[Unit]
Description=Weekly production patching timer
[Timer]
OnCalendar=Mon *-*-* 07:30:00
Persistent=true
Unit=update-prod-servers.service
[Install]
WantedBy=timers.target
```
## Inventory
### wolfywebserver02
- Hostname: wolfywebserver02
- OS: Ubuntu 24.04.4 LTS
- VM Host: wolfyvmserver
- Ansible Status: Ready
- Ansible Category: ubuntu_servers
This server is currently being used for running audiobookshelf so that my friends can access the server and listen to audiobooks on their phone or computer using an app or the webui. The friends are networked via Tailscale.
### wolfyminecraftserver
- Hostname: wolfyminecraftserver
- OS: Ubuntu 24.04.4 LTS
- VM Host: wolfyvmserver
- Ansible Status: Ready
- Ansible Category: ubuntu_servers
This server is currently being used to host MoreModsReloaded server for Rockman and his friends. It is running Crafty Controller to manage the Minecraft Server and provide an admin web UI so that Rockman and a few admins can make changes to the server if they need to. Playit service is used to tunnel a port on the server to a playit cloud server that reverse proxies outside users to my server without exposing my IP. Tailscale is used to allow Rockman and Waluu to access the Crafty Controller web UI.
### wolfydiscordbot01
- Hostname: wolfydiscordbot01
- OS: Ubuntu 24.04.4 LTS
- VM Host: wolfyvmserver
- Ansible Status: Ready
- Ansible Category: ubuntu_servers
TO DO
## Notes
### Setting up Ansible Access
#### Bootstrap Script
`.bootstrap/` contains a script that can be used to setup ansible on new servers.
#### Debian/Ubuntu
1. On the control node generate a dedicated key
```bash
ssh-keygen -t ed25519 -f ~/.ssh/ansible-ed25519 -C "ansible-control"
```
2. Create the ansible user and give it sudo permissions
```bash
# For Password
openssl rand -base64 16
# Create account and give sudo permissions
sudo adduser ansible
sudo usermod -aG sudo ansible
```
3. Allow passwordless sudo for the ansible account
```bash
sudo visudo
```
Add:
```ini
ansible ALL=(ALL) NOPASSWD: ALL
```
4. On the control node copy the ssh key over to the target server
```bash
ssh-copy-id -i ~/.ssh/ansible-ed25519.pub ansible@server.tailnet.ts.net
```
Test:
```bash
ssh ansible@server.tailnet.ts.net
```
5. Lock the ansible account to prevent login attempts
```bash
sudo passwd -l ansible
```
### To-Do
+23
View File
@@ -0,0 +1,23 @@
#!/bin/bash
set -euo pipefail
LOG_DIR="/home/wolfy/log/ansible-prod"
mkdir -p "$LOG_DIR"
TIMESTAMP="$(date '+%Y-%m-%d_%H-%M-%S')"
LOG_FILE="$LOG_DIR/ubuntu-prod-update-$TIMESTAMP.log"
exec >>"$LOG_FILE" 2>&1
echo "========================================"
echo "Prod patching run started: $(date -Is)"
echo "========================================"
cd /home/wolfy/ansible
/home/wolfy/ansible-env/bin/ansible-playbook playbooks/update-prod-servers.yml
echo
echo "========================================"
echo "Prod patching run completed: $(date -Is)"
echo "========================================"
+11
View File
@@ -0,0 +1,11 @@
[Unit]
Description=Run weekly production patching playbook
Wants=network-online.target
After=network-online.target
[Service]
Type=oneshot
User=wolfy
Group=wolfy
ExecStart=/home/wolfy/ansible/run/update-prod-servers.sh
WorkingDirectory=/home/wolfy/ansible
+10
View File
@@ -0,0 +1,10 @@
[Unit]
Description=Weekly production patching timer
[Timer]
OnCalendar=Mon *-*-* 07:30:00
Persistent=true
Unit=update-prod-servers.service
[Install]
WantedBy=timers.target