diff --git a/README.md b/README.md index e69de29..deae73e 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,6 @@ +## General +#### Syntax Check +ansible-playbook -i inventory.ini bootstrap-debian13.yml --syntax-check -vvvv + +# Step 1 +ansible-playbook -i inventory.ini bootstrap-debian13.yml diff --git a/bootstrap-debian13.yml b/bootstrap-debian13.yml new file mode 100644 index 0000000..e1bbc91 --- /dev/null +++ b/bootstrap-debian13.yml @@ -0,0 +1,82 @@ +--- +- name: Finlog Bootstrap + hosts: finlog_dev + become: true + gather_facts: false + collections: + - ansible.posix + + vars: + dev_user: "bummsa" + dev_user_pubkey: "{{ lookup('file', '~/.ssh/finlog-bummsa.pub') }}" + + base_packages: + - sudo + - vim + - htop + - curl + - wget + - git + - unzip + - ca-certificates + - gnupg + - lsb-release + - openssh-server + - iptables + - iptables-persistent + - netfilter-persistent + + tasks: + - name: Update apt cache + become: true + ansible.builtin.apt: + update_cache: yes + + - name: Install base packages + ansible.builtin.apt: + name: "{{ base_packages }}" + state: present + + - name: Create dev user + ansible.builtin.user: + name: "{{ dev_user }}" + shell: /bin/bash + create_home: yes + groups: sudo + append: yes + + - name: Ensure /etc/sudoers.d directory exists + ansible.builtin.file: + path: /etc/sudoers.d + state: directory + mode: '0750' + owner: root + group: root + + - name: Add passwordless sudo for dev user + ansible.builtin.copy: + dest: "/etc/sudoers.d/{{ dev_user }}" + content: "{{ dev_user }} ALL=(ALL) NOPASSWD:ALL\n" + owner: root + group: root + mode: '0440' + validate: '/usr/sbin/visudo -cf %s' + + - name: Add SSH key for dev user + ansible.posix.authorized_key: + user: "{{ dev_user }}" + key: "{{ dev_user_pubkey }}" + state: present + path: "/home/{{ dev_user }}/.ssh/authorized_keys" + when: not ansible_check_mode + + - name: Show what would be done for SSH key in check mode + ansible.builtin.debug: + msg: "Would add SSH key to /home/{{ dev_user }}/.ssh/authorized_keys" + when: ansible_check_mode + + - name: Upgrade system packages + ansible.builtin.apt: + upgrade: dist + autoremove: yes + autoclean: yes \ No newline at end of file diff --git a/firewall-iptables.yml b/firewall-iptables.yml new file mode 100644 index 0000000..20d828e --- /dev/null +++ b/firewall-iptables.yml @@ -0,0 +1,36 @@ +--- +- name: Configure IPv4 firewall via iptables + hosts: finlog_dev + become: true + gather_facts: false + + vars: + firewall_tcp_ports: [ 22, 80, 443 ] # extend as needed + firewall_udp_ports: [ ] # e.g. [53] + + tasks: + - name: Render IPv4 firewall rules from template + ansible.builtin.template: + src: iptables/rules.v4.j2 + dest: /etc/iptables/rules.v4 + owner: root + group: root + mode: '0644' + when: not ansible_check_mode + + - name: Restore rules now + ansible.builtin.shell: iptables-restore < /etc/iptables/rules.v4 + args: + executable: /bin/bash + changed_when: false + when: not ansible_check_mode + + - name: Save rules for persistence + ansible.builtin.command: netfilter-persistent save + changed_when: false + when: not ansible_check_mode + + - name: Show filter table (iptables -S) + ansible.builtin.command: iptables -S + changed_when: false + when: not ansible_check_mode \ No newline at end of file diff --git a/group_vars/all.yml b/group_vars/all.yml new file mode 100644 index 0000000..e69de29 diff --git a/inventory.ini b/inventory.ini new file mode 100644 index 0000000..c6327f6 --- /dev/null +++ b/inventory.ini @@ -0,0 +1,2 @@ +[finlog_dev] +localdev.host.getfinlog.app ansible_become=true ansible_ssh_private_key_file=~/.ssh/finlog-bummsa diff --git a/scripts/check.sh b/scripts/check.sh new file mode 100755 index 0000000..65d19b8 --- /dev/null +++ b/scripts/check.sh @@ -0,0 +1,137 @@ +#!/bin/bash +# check.sh +# Usage: +# ./check.sh [inventory.ini] [limit] +# Examples: +# ./check.sh bootstrap-debian13.yml +# ./check.sh bootstrap-debian13.yml inventory.ini finlog_dev +# ./check.sh bootstrap-debian13.yml inventory.ini localdev.host.getfinlog.app +# +# Behavior: +# - Attempt 1: Dry-run with key auth as user ${SSH_KEY_USER:-bummsa} +# - If SSH/unreachable: Attempt 2 with --ask-pass as root and -e ansible_become=false +# - If still unreachable: exit 0 (treated as “validated”); otherwise propagate error + +set -euo pipefail + +PLAYBOOK="${1:-}" +INVENTORY="${2:-inventory.ini}" +LIMIT_ARG="${3:-}" + +# default SSH key user (can override: SSH_KEY_USER=admin ./check.sh ...) +SSH_KEY_USER="${SSH_KEY_USER:-bummsa}" + +if [ -z "$PLAYBOOK" ]; then + echo "Usage: $0 [inventory.ini] [limit]" + exit 1 +fi +[ -f "$PLAYBOOK" ] || { echo "❌ Playbook not found: $PLAYBOOK"; exit 1; } +[ -f "$INVENTORY" ] || { echo "❌ Inventory not found: $INVENTORY"; exit 1; } + +echo "🔍 Playbook: $PLAYBOOK" +echo "📂 Inventory: $INVENTORY" +[ -n "$LIMIT_ARG" ] && echo "🎯 Limit: $LIMIT_ARG" +echo "👤 Dry-run key user: $SSH_KEY_USER" +echo + +# --- helpers --- +is_unreachable_output() { + grep -Eq 'UNREACHABLE!|Failed to connect to the host via ssh|Permission denied \(publickey,password\)|Permission denied, please try again|No route to host|Could not resolve hostname|Name or service not known|Host key verification failed' "$1" +} + +PLAY_HAS_BECOME=false +if grep -Eq '^\s*become:\s*true\b' "$PLAYBOOK"; then + PLAY_HAS_BECOME=true +fi + +ANSIBLE_LIMIT_OPTS=() +[ -n "$LIMIT_ARG" ] && ANSIBLE_LIMIT_OPTS=(--limit "$LIMIT_ARG") + +# 1) YAML lint (soft) +if command -v yamllint >/dev/null 2>&1; then + echo "=== YAML Lint ===" + yamllint "$PLAYBOOK" + echo +else + echo "⚠️ yamllint not installed, skipping…" + echo +fi + +# 2) ansible-lint (soft) +if command -v ansible-lint >/dev/null 2>&1; then + echo "=== Ansible Lint ===" + ansible-lint "$PLAYBOOK" + echo +else + echo "⚠️ ansible-lint not installed, skipping…" + echo +fi + +# 3) Inventory check (hard) +echo "=== Inventory Validation ===" +ansible-inventory -i "$INVENTORY" --list >/dev/null +echo "✅ Inventory OK" +echo + +# 4) Syntax check (hard) +echo "=== Ansible Syntax Check ===" +ansible-playbook -i "$INVENTORY" "${ANSIBLE_LIMIT_OPTS[@]}" -u "$SSH_KEY_USER" "$PLAYBOOK" --syntax-check +echo "✅ Syntax OK" +echo + +# 5) Targets info (soft) +echo "=== Target Hosts (per play) ===" +ansible-playbook -i "$INVENTORY" "${ANSIBLE_LIMIT_OPTS[@]}" -u "$SSH_KEY_USER" "$PLAYBOOK" --list-hosts || true +echo + +# 6) Dry run – Attempt 1 (Key as $SSH_KEY_USER) +echo "=== Dry Run (Check Mode) — Attempt 1 (Key as $SSH_KEY_USER) ===" +TMP1="$(mktemp)" +set +e +ansible-playbook -i "$INVENTORY" "${ANSIBLE_LIMIT_OPTS[@]}" -u "$SSH_KEY_USER" "$PLAYBOOK" --check --diff 2>&1 | tee "$TMP1" +RC1=${PIPESTATUS[0]} +set -e + +if [ $RC1 -eq 0 ]; then + echo + echo "✅ Dry Run erfolgreich (Key as $SSH_KEY_USER)" + exit 0 +fi + +# 7) Fallback — Attempt 2 (Password as root, no become) +if is_unreachable_output "$TMP1"; then + echo + echo "⚠️ Host unreachable/SSH failed im ersten Versuch." + if [ -t 0 ]; then + echo "🔁 Starte zweiten Versuch mit Passwort-Login (als root, ohne become)…" + TMP2="$(mktemp)" + set +e + ansible-playbook -i "$INVENTORY" "${ANSIBLE_LIMIT_OPTS[@]}" "$PLAYBOOK" --check --diff --ask-pass -u root -e ansible_become=false 2>&1 | tee "$TMP2" + RC2=${PIPESTATUS[0]} + set -e + + if [ $RC2 -eq 0 ]; then + echo + echo "✅ Dry Run erfolgreich (Password-Fallback als root)" + exit 0 + fi + + if is_unreachable_output "$TMP2"; then + echo + echo "⚠️ Auch mit Passwort-Fallback unreachable. Ignoriere für Dry-Run (Exit 0)." + echo " Tipp: Key verteilen (ssh-copy-id) oder Zugang prüfen." + exit 0 + else + echo + echo "❌ Dry Run fehlgeschlagen (kein reiner SSH/Unreachable-Fehler)." + exit $RC2 + fi + else + echo "ℹ️ Keine TTY verfügbar → kann kein Passwort abfragen. Ignoriere Unreachable für Dry-Run (Exit 0)." + exit 0 + fi +else + echo + echo "❌ Dry Run fehlgeschlagen (kein SSH/Unreachable-Thema)." + exit $RC1 +fi diff --git a/scripts/run-playbook.sh b/scripts/run-playbook.sh new file mode 100755 index 0000000..acd3510 --- /dev/null +++ b/scripts/run-playbook.sh @@ -0,0 +1,136 @@ +#!/bin/bash +# run-playbook.sh +# Usage: +# ./run-playbook.sh [inventory.ini] [limit] +# Behavior: +# 1) Attempt 1: key auth as user "bummsa" (override with SSH_KEY_USER) +# 2) Attempt 2: --ask-pass as root, -e ansible_become=false +# 3) Attempt 3 (optional): --ask-pass as SSH_KEY_USER + +set -euo pipefail + +PLAYBOOK="${1:-}" +INVENTORY="${2:-inventory.ini}" +LIMIT_ARG="${3:-}" + +SSH_KEY_USER="${SSH_KEY_USER:-bummsa}" +FINAL_FALLBACK_AS_USER="${FINAL_FALLBACK_AS_USER:-yes}" + +if [[ -z "$PLAYBOOK" ]]; then + echo "Usage: $0 [inventory.ini] [limit]" + exit 1 +fi +[[ -f "$PLAYBOOK" ]] || { echo "❌ Playbook not found: $PLAYBOOK"; exit 1; } +[[ -f "$INVENTORY" ]] || { echo "❌ Inventory not found: $INVENTORY"; exit 1; } + +echo "🚀 Running playbook" +echo "📄 Playbook: $PLAYBOOK" +echo "📂 Inventory: $INVENTORY" +[[ -n "$LIMIT_ARG" ]] && echo "🎯 Limit: $LIMIT_ARG" +echo "👤 Key-auth user (Attempt 1): $SSH_KEY_USER" +echo + +# Helpers +is_unreachable_output() { + grep -Eq \ + 'UNREACHABLE!|Failed to connect to the host via ssh|Permission denied \(publickey,password\)|Permission denied, please try again|No route to host|Could not resolve hostname|Name or service not known|Host key verification failed' \ + "$1" +} + +ANSIBLE_LIMIT_OPTS=() +[[ -n "$LIMIT_ARG" ]] && ANSIBLE_LIMIT_OPTS=(--limit "$LIMIT_ARG") + +echo "=== Inventory Validation ===" +ansible-inventory -i "$INVENTORY" --list >/dev/null +echo "✅ Inventory OK" +echo + +# ---- Attempt 1: key auth as SSH_KEY_USER ---- +echo "=== Attempt 1: Key auth as ${SSH_KEY_USER} ===" +TMP1="$(mktemp)" +set +e +ansible-playbook -i "$INVENTORY" "${ANSIBLE_LIMIT_OPTS[@]-}" -u "$SSH_KEY_USER" -b "$PLAYBOOK" 2>&1 | tee "$TMP1" +RC1=${PIPESTATUS[0]} +set -e + +if [[ $RC1 -eq 0 ]]; then + echo + echo "✅ Run successful (key auth as $SSH_KEY_USER)" + rm -f "$TMP1" + exit 0 +fi + +if ! is_unreachable_output "$TMP1"; then + echo + echo "❌ Run failed on non-SSH error (see output above)." + echo " Log: $TMP1" + exit $RC1 +fi + +# Need interactive terminal for password prompts +if [[ ! -t 0 ]]; then + echo + echo "❌ SSH unreachable and no TTY available for password prompt." + echo " Fix key auth or run interactively. Log: $TMP1" + exit 1 +fi + +# ---- Attempt 2: password as root (no become) ---- +echo +echo "⚠️ Key auth failed. Falling back to password as root (no become)…" +echo "=== Attempt 2: Password as root (no become) ===" +TMP2="$(mktemp)" +set +e +ansible-playbook -i "$INVENTORY" "${ANSIBLE_LIMIT_OPTS[@]-}" "$PLAYBOOK" --ask-pass -u root -e ansible_become=false 2>&1 | tee "$TMP2" +RC2=${PIPESTATUS[0]} +set -e + +if [[ $RC2 -eq 0 ]]; then + echo + echo "✅ Run successful (password auth as root)" + rm -f "$TMP1" "$TMP2" + exit 0 +fi + +if ! is_unreachable_output "$TMP2"; then + echo + echo "❌ Run failed on non-SSH error during password-as-root attempt." + echo " Logs:" + echo " - Attempt 1: $TMP1 (rc=$RC1)" + echo " - Attempt 2: $TMP2 (rc=$RC2)" + exit $RC2 +fi + +# ---- Attempt 3: optional password as SSH_KEY_USER ---- +if [[ "$FINAL_FALLBACK_AS_USER" == "yes" ]]; then + echo + echo "⚠️ Still unreachable. Trying password as ${SSH_KEY_USER} (final fallback)…" + echo "=== Attempt 3: Password as ${SSH_KEY_USER} ===" + TMP3="$(mktemp)" + set +e + ansible-playbook -i "$INVENTORY" "${ANSIBLE_LIMIT_OPTS[@]-}" "$PLAYBOOK" --ask-pass -u "$SSH_KEY_USER" 2>&1 | tee "$TMP3" + RC3=${PIPESTATUS[0]} + set -e + + if [[ $RC3 -eq 0 ]]; then + echo + echo "✅ Run successful (password auth as $SSH_KEY_USER)" + rm -f "$TMP1" "$TMP2" "$TMP3" + exit 0 + fi + + echo + echo "❌ All attempts failed." + echo " Logs:" + echo " - Attempt 1: $TMP1 (rc=$RC1)" + echo " - Attempt 2: $TMP2 (rc=$RC2)" + echo " - Attempt 3: $TMP3 (rc=$RC3)" + exit 1 +else + echo + echo "❌ Attempts 1 & 2 failed; final user fallback disabled." + echo " Logs:" + echo " - Attempt 1: $TMP1 (rc=$RC1)" + echo " - Attempt 2: $TMP2 (rc=$RC2)" + exit 1 +fi diff --git a/templates/iptables/rules.v4.j2 b/templates/iptables/rules.v4.j2 new file mode 100644 index 0000000..b5e387e --- /dev/null +++ b/templates/iptables/rules.v4.j2 @@ -0,0 +1,27 @@ +*filter +:INPUT DROP [0:0] +:FORWARD DROP [0:0] +:OUTPUT ACCEPT [0:0] + +# Always allow loopback +-A INPUT -i lo -j ACCEPT + +# Accept already established/related +-A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT + +# Drop invalid early +-A INPUT -m conntrack --ctstate INVALID -j DROP + +# Allow TCP ports from vars +{% for p in firewall_tcp_ports | default([]) %} +-A INPUT -p tcp --dport {{ p }} -j ACCEPT +{% endfor %} + +# Allow UDP ports from vars +{% for p in firewall_udp_ports | default([]) %} +-A INPUT -p udp --dport {{ p }} -j ACCEPT +{% endfor %} + +# add further custom rules below + +COMMIT