Phantom v2: dual-mode architecture, security hardening, deployment management
- Dual-mode operation: standalone + c2itall integrated (env var detection)
- SSH keys moved to ~/.ssh/c2deploy_ph-{id} with per-deployment known_hosts
- Ansible output streaming with filtered console + full log capture
- Deployment management menu: discover, SSH, teardown existing deployments
- Cert setup script (setup-cert.sh) deployed to servers for post-DNS LE certs
- Matrix hardening: unique secrets, SSRF protection, rate limits, nginx security headers
- Base hardening: fail2ban systemd backend (Debian 12), SSH limits, nginx jails
- Add-matrix-user helper script deployed to all Matrix servers
- .env support for standalone credential storage
- Config key rename: deploy_id → deployment_id (with backward compat)
- Provider cleanup playbooks for teardown
- Test suite with 50 tests
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
hosts: all
|
||||
become: true
|
||||
vars:
|
||||
ssh_port: "{{ ssh_port | default(22) }}"
|
||||
_ssh_port: "{{ ssh_port | default('22') }}"
|
||||
ssh_allow_password: false
|
||||
|
||||
tasks:
|
||||
@@ -51,7 +51,7 @@
|
||||
- name: Allow SSH through UFW
|
||||
ufw:
|
||||
rule: allow
|
||||
port: "{{ ssh_port }}"
|
||||
port: "{{ _ssh_port }}"
|
||||
proto: tcp
|
||||
|
||||
- name: Enable UFW
|
||||
@@ -59,18 +59,45 @@
|
||||
state: enabled
|
||||
|
||||
# ─── Fail2ban ───────────────────────────────────────────────────────
|
||||
- name: Configure fail2ban SSH jail
|
||||
- name: Configure fail2ban jails
|
||||
copy:
|
||||
dest: /etc/fail2ban/jail.local
|
||||
content: |
|
||||
[sshd]
|
||||
enabled = true
|
||||
port = {{ ssh_port }}
|
||||
filter = sshd
|
||||
logpath = /var/log/auth.log
|
||||
maxretry = 5
|
||||
[DEFAULT]
|
||||
bantime = 3600
|
||||
findtime = 600
|
||||
maxretry = 5
|
||||
banaction = ufw
|
||||
|
||||
[sshd]
|
||||
enabled = true
|
||||
port = {{ _ssh_port }}
|
||||
filter = sshd
|
||||
backend = systemd
|
||||
maxretry = 3
|
||||
bantime = 7200
|
||||
|
||||
[nginx-http-auth]
|
||||
enabled = true
|
||||
port = http,https
|
||||
filter = nginx-http-auth
|
||||
logpath = /var/log/nginx/error.log
|
||||
maxretry = 3
|
||||
|
||||
[nginx-botsearch]
|
||||
enabled = true
|
||||
port = http,https
|
||||
filter = nginx-botsearch
|
||||
logpath = /var/log/nginx/access.log
|
||||
maxretry = 5
|
||||
|
||||
[nginx-limit-req]
|
||||
enabled = true
|
||||
port = http,https
|
||||
filter = nginx-limit-req
|
||||
logpath = /var/log/nginx/error.log
|
||||
maxretry = 5
|
||||
bantime = 3600
|
||||
mode: "0644"
|
||||
notify: restart fail2ban
|
||||
|
||||
@@ -103,6 +130,41 @@
|
||||
line: "X11Forwarding no"
|
||||
notify: restart sshd
|
||||
|
||||
- name: Set SSH MaxAuthTries
|
||||
lineinfile:
|
||||
path: /etc/ssh/sshd_config
|
||||
regexp: "^#?MaxAuthTries"
|
||||
line: "MaxAuthTries 3"
|
||||
notify: restart sshd
|
||||
|
||||
- name: Set SSH ClientAliveInterval
|
||||
lineinfile:
|
||||
path: /etc/ssh/sshd_config
|
||||
regexp: "^#?ClientAliveInterval"
|
||||
line: "ClientAliveInterval 300"
|
||||
notify: restart sshd
|
||||
|
||||
- name: Set SSH ClientAliveCountMax
|
||||
lineinfile:
|
||||
path: /etc/ssh/sshd_config
|
||||
regexp: "^#?ClientAliveCountMax"
|
||||
line: "ClientAliveCountMax 2"
|
||||
notify: restart sshd
|
||||
|
||||
- name: Set SSH MaxStartups
|
||||
lineinfile:
|
||||
path: /etc/ssh/sshd_config
|
||||
regexp: "^#?MaxStartups"
|
||||
line: "MaxStartups 10:30:100"
|
||||
notify: restart sshd
|
||||
|
||||
- name: Set SSH LoginGraceTime
|
||||
lineinfile:
|
||||
path: /etc/ssh/sshd_config
|
||||
regexp: "^#?LoginGraceTime"
|
||||
line: "LoginGraceTime 60"
|
||||
notify: restart sshd
|
||||
|
||||
# ─── Automatic Security Updates ─────────────────────────────────────
|
||||
- name: Enable unattended upgrades for security
|
||||
copy:
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env bash
|
||||
# Phantom — Matrix User Registration
|
||||
# Creates a new user on this Synapse homeserver
|
||||
set -euo pipefail
|
||||
|
||||
SERVER_NAME=$(grep '^server_name:' /etc/matrix-synapse/homeserver.yaml | awk '{print $2}' | tr -d '"')
|
||||
SECRET=$(grep 'registration_shared_secret:' /etc/matrix-synapse/homeserver.yaml | awk '{print $2}' | tr -d '"')
|
||||
|
||||
echo "============================================"
|
||||
echo " Matrix — Add User"
|
||||
echo " Server: ${SERVER_NAME}"
|
||||
echo "============================================"
|
||||
echo ""
|
||||
|
||||
register_new_matrix_user \
|
||||
--shared-secret "${SECRET}" \
|
||||
http://localhost:8008
|
||||
@@ -0,0 +1,120 @@
|
||||
#!/usr/bin/env bash
|
||||
# Phantom — Let's Encrypt Certificate Setup
|
||||
# Deployed by phantom to {{ ansible_host | default('this server') }}
|
||||
# Run this script after DNS has propagated to this server's IP.
|
||||
set -euo pipefail
|
||||
|
||||
DOMAIN="{{ _tls_domain }}"
|
||||
SERVER_IP=$(hostname -I | awk '{print $1}')
|
||||
|
||||
echo "============================================"
|
||||
echo " Phantom — Certificate Setup"
|
||||
echo "============================================"
|
||||
echo " Domain: ${DOMAIN}"
|
||||
echo " Server IP: ${SERVER_IP}"
|
||||
echo ""
|
||||
|
||||
# ── DNS Validation ──────────────────────────────────────────────────────
|
||||
echo "[*] Checking DNS resolution for ${DOMAIN}..."
|
||||
DNS_IP=$(dig +short "${DOMAIN}" @1.1.1.1 2>/dev/null | tail -1)
|
||||
|
||||
if [ -z "${DNS_IP}" ]; then
|
||||
echo "[-] DNS lookup failed — ${DOMAIN} does not resolve."
|
||||
echo " Point your DNS A record to: ${SERVER_IP}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "${DNS_IP}" != "${SERVER_IP}" ]; then
|
||||
echo "[-] DNS mismatch!"
|
||||
echo " ${DOMAIN} resolves to: ${DNS_IP}"
|
||||
echo " This server's IP: ${SERVER_IP}"
|
||||
echo " Update your DNS A record to point to ${SERVER_IP}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[+] DNS OK — ${DOMAIN} → ${DNS_IP}"
|
||||
|
||||
# ── HTTP Reachability Check ─────────────────────────────────────────────
|
||||
echo "[*] Verifying HTTP reachability..."
|
||||
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "http://${DOMAIN}/" --max-time 10 2>/dev/null || echo "000")
|
||||
|
||||
if [ "${HTTP_CODE}" = "000" ]; then
|
||||
echo "[!] Warning: HTTP request to ${DOMAIN} failed (timeout or connection refused)"
|
||||
echo " Certbot may fail if port 80 is not reachable."
|
||||
read -rp " Continue anyway? [y/N] " CONT
|
||||
[ "${CONT}" = "y" ] || exit 1
|
||||
else
|
||||
echo "[+] HTTP reachable (status ${HTTP_CODE})"
|
||||
fi
|
||||
|
||||
# ── Certbot ─────────────────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo "[*] Running certbot for ${DOMAIN}..."
|
||||
|
||||
# Check if nginx is running — use webroot if so, standalone otherwise
|
||||
if systemctl is-active --quiet nginx 2>/dev/null; then
|
||||
echo " Using webroot mode (nginx running)"
|
||||
mkdir -p /var/www/certbot
|
||||
certbot certonly --webroot -w /var/www/certbot \
|
||||
-d "${DOMAIN}" \
|
||||
--non-interactive \
|
||||
--agree-tos \
|
||||
{% if certbot_email is defined and certbot_email %}
|
||||
--email "{{ certbot_email }}" \
|
||||
{% else %}
|
||||
--register-unsafely-without-email \
|
||||
{% endif %}
|
||||
--force-renewal
|
||||
else
|
||||
echo " Using standalone mode (nginx not running)"
|
||||
certbot certonly --standalone \
|
||||
-d "${DOMAIN}" \
|
||||
--non-interactive \
|
||||
--agree-tos \
|
||||
{% if certbot_email is defined and certbot_email %}
|
||||
--email "{{ certbot_email }}" \
|
||||
{% else %}
|
||||
--register-unsafely-without-email \
|
||||
{% endif %}
|
||||
--force-renewal
|
||||
fi
|
||||
|
||||
if [ ! -f "/etc/letsencrypt/live/${DOMAIN}/fullchain.pem" ]; then
|
||||
echo "[-] Certbot failed — certificate not found."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[+] Certificate obtained successfully."
|
||||
|
||||
# ── Update nginx to use LE cert ─────────────────────────────────────────
|
||||
echo "[*] Updating nginx configuration..."
|
||||
|
||||
SELF_CERT="/etc/ssl/certs/${DOMAIN}-selfsigned.crt"
|
||||
SELF_KEY="/etc/ssl/private/${DOMAIN}-selfsigned.key"
|
||||
LE_CERT="/etc/letsencrypt/live/${DOMAIN}/fullchain.pem"
|
||||
LE_KEY="/etc/letsencrypt/live/${DOMAIN}/privkey.pem"
|
||||
|
||||
# Replace self-signed cert paths with LE cert paths in all nginx configs
|
||||
for CONF in /etc/nginx/sites-enabled/* /etc/nginx/conf.d/*; do
|
||||
[ -f "${CONF}" ] || continue
|
||||
if grep -q "${SELF_CERT}" "${CONF}" 2>/dev/null; then
|
||||
sed -i "s|${SELF_CERT}|${LE_CERT}|g" "${CONF}"
|
||||
sed -i "s|${SELF_KEY}|${LE_KEY}|g" "${CONF}"
|
||||
echo " Updated: ${CONF}"
|
||||
fi
|
||||
done
|
||||
|
||||
# Test and reload nginx
|
||||
if nginx -t 2>/dev/null; then
|
||||
systemctl reload nginx
|
||||
echo "[+] nginx reloaded with Let's Encrypt certificate."
|
||||
else
|
||||
echo "[-] nginx config test failed — check configuration manually."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "============================================"
|
||||
echo " Certificate setup complete!"
|
||||
echo " Domain: https://${DOMAIN}"
|
||||
echo "============================================"
|
||||
@@ -0,0 +1,59 @@
|
||||
---
|
||||
# Shared TLS setup: tries certbot, falls back to self-signed
|
||||
# Include with: include_tasks: ../common/tls_setup.yml
|
||||
# Required vars: _tls_domain (the domain to get a cert for)
|
||||
|
||||
- name: Attempt TLS certificate from Let's Encrypt
|
||||
command: >
|
||||
certbot certonly --webroot -w /var/www/certbot
|
||||
-d {{ _tls_domain }}
|
||||
--non-interactive
|
||||
--agree-tos
|
||||
{% if certbot_email is defined and certbot_email %}
|
||||
--email {{ certbot_email }}
|
||||
{% else %}
|
||||
--register-unsafely-without-email
|
||||
{% endif %}
|
||||
args:
|
||||
creates: "/etc/letsencrypt/live/{{ _tls_domain }}/fullchain.pem"
|
||||
register: _certbot_result
|
||||
ignore_errors: true
|
||||
|
||||
- name: Generate self-signed certificate (fallback)
|
||||
command: >
|
||||
openssl req -x509 -nodes -days 365 -newkey rsa:4096
|
||||
-keyout /etc/ssl/private/{{ _tls_domain }}-selfsigned.key
|
||||
-out /etc/ssl/certs/{{ _tls_domain }}-selfsigned.crt
|
||||
-subj "/CN={{ _tls_domain }}"
|
||||
args:
|
||||
creates: "/etc/ssl/certs/{{ _tls_domain }}-selfsigned.crt"
|
||||
when: _certbot_result is failed
|
||||
|
||||
- name: Set cert paths (certbot)
|
||||
set_fact:
|
||||
_ssl_cert: "/etc/letsencrypt/live/{{ _tls_domain }}/fullchain.pem"
|
||||
_ssl_key: "/etc/letsencrypt/live/{{ _tls_domain }}/privkey.pem"
|
||||
when: _certbot_result is not failed
|
||||
|
||||
- name: Set cert paths (self-signed)
|
||||
set_fact:
|
||||
_ssl_cert: "/etc/ssl/certs/{{ _tls_domain }}-selfsigned.crt"
|
||||
_ssl_key: "/etc/ssl/private/{{ _tls_domain }}-selfsigned.key"
|
||||
when: _certbot_result is failed
|
||||
|
||||
- name: Warn about self-signed certificate
|
||||
debug:
|
||||
msg: "Using self-signed TLS cert. Point DNS to this server and run: certbot certonly --webroot -w /var/www/certbot -d {{ _tls_domain }}"
|
||||
when: _certbot_result is failed
|
||||
|
||||
- name: Create Tools directory
|
||||
file:
|
||||
path: /root/Tools
|
||||
state: directory
|
||||
mode: "0755"
|
||||
|
||||
- name: Deploy cert setup script
|
||||
template:
|
||||
src: ../common/templates/setup-cert.sh.j2
|
||||
dest: /root/Tools/setup-cert.sh
|
||||
mode: "0700"
|
||||
Reference in New Issue
Block a user