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:
@@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2026 ghost_protocol contributors
|
Copyright (c) 2026 n0mad1k
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
+1
-1
@@ -40,7 +40,7 @@ The **Covert SD Card Tool** is a Python script designed to automate the process
|
|||||||
1. **Clone the Repository or Download the Script:**
|
1. **Clone the Repository or Download the Script:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/YOUR_USERNAME/ghost_protocol.git
|
git clone https://github.com/n0mad1k/ghost_protocol.git
|
||||||
cd ghost_protocol/covert_sd
|
cd ghost_protocol/covert_sd
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
+11
-10
@@ -8,12 +8,13 @@ PROFILE_NAME="default"
|
|||||||
|
|
||||||
# ─── TOR SETTINGS ──────────────────────────────────────────────────────────────
|
# ─── TOR SETTINGS ──────────────────────────────────────────────────────────────
|
||||||
TOR_CIRCUIT_ROTATION="30"
|
TOR_CIRCUIT_ROTATION="30"
|
||||||
TOR_BLACKLIST=""
|
TOR_BLACKLIST="us,gb,ca,au,nz"
|
||||||
TOR_STRICT_NODES="1"
|
TOR_STRICT_NODES="0"
|
||||||
TOR_ISOLATION="1"
|
TOR_ISOLATION="1"
|
||||||
TOR_PADDING="1"
|
TOR_PADDING="1"
|
||||||
TOR_SOCKS_PORT="9050"
|
TOR_SOCKS_PORT="9050"
|
||||||
TOR_DNS_PORT="5353"
|
TOR_DNS_PORT="5353"
|
||||||
|
TOR_TRANS_PORT="9040"
|
||||||
TOR_NUM_GUARDS="3"
|
TOR_NUM_GUARDS="3"
|
||||||
TOR_SAFE_LOGGING="1"
|
TOR_SAFE_LOGGING="1"
|
||||||
|
|
||||||
@@ -36,14 +37,14 @@ MAC_VENDOR_SPOOF=""
|
|||||||
|
|
||||||
# ─── HOSTNAME ──────────────────────────────────────────────────────────────────
|
# ─── HOSTNAME ──────────────────────────────────────────────────────────────────
|
||||||
# Pattern: desktop | random | custom
|
# Pattern: desktop | random | custom
|
||||||
HOSTNAME_PATTERN="desktop"
|
HOSTNAME_PATTERN="random"
|
||||||
HOSTNAME_CUSTOM_PREFIX=""
|
HOSTNAME_CUSTOM_PREFIX="workstation"
|
||||||
|
|
||||||
# ─── SYSTEM HARDENING ──────────────────────────────────────────────────────────
|
# ─── SYSTEM HARDENING ──────────────────────────────────────────────────────────
|
||||||
HARDEN_IPV6="1"
|
HARDEN_IPV6="1"
|
||||||
HARDEN_SWAP="1"
|
HARDEN_SWAP="1"
|
||||||
HARDEN_CORE_DUMPS="1"
|
HARDEN_CORE_DUMPS="1"
|
||||||
HARDEN_CLIPBOARD_CLEAR="0"
|
HARDEN_CLIPBOARD_CLEAR="1"
|
||||||
HARDEN_SCREEN_LOCK="1"
|
HARDEN_SCREEN_LOCK="1"
|
||||||
HARDEN_SCREEN_LOCK_TIMEOUT="300"
|
HARDEN_SCREEN_LOCK_TIMEOUT="300"
|
||||||
HARDEN_TIMEZONE_SPOOF="0"
|
HARDEN_TIMEZONE_SPOOF="0"
|
||||||
@@ -53,10 +54,10 @@ HARDEN_LOCALE_VALUE="en_US.UTF-8"
|
|||||||
|
|
||||||
# ─── LEAK PREVENTION ──────────────────────────────────────────────────────────
|
# ─── LEAK PREVENTION ──────────────────────────────────────────────────────────
|
||||||
LEAK_WEBRTC_BLOCK="1"
|
LEAK_WEBRTC_BLOCK="1"
|
||||||
LEAK_USB_BLOCK="0"
|
LEAK_USB_BLOCK="1"
|
||||||
|
|
||||||
# ─── MONITORING ────────────────────────────────────────────────────────────────
|
# ─── MONITORING ────────────────────────────────────────────────────────────────
|
||||||
MONITOR_PROCESSES="0"
|
MONITOR_PROCESSES="1"
|
||||||
MONITOR_LOG_ROTATION="1"
|
MONITOR_LOG_ROTATION="1"
|
||||||
LOG_ROTATION_HOURS="4"
|
LOG_ROTATION_HOURS="4"
|
||||||
|
|
||||||
@@ -85,12 +86,12 @@ WIPE_METHOD="auto"
|
|||||||
|
|
||||||
# ─── DEPLOYMENT LEVEL ─────────────────────────────────────────────────────────
|
# ─── DEPLOYMENT LEVEL ─────────────────────────────────────────────────────────
|
||||||
# Level: bare-metal-standard | bare-metal-paranoid | cloud-normal | cloud-paranoid
|
# Level: bare-metal-standard | bare-metal-paranoid | cloud-normal | cloud-paranoid
|
||||||
DEPLOYMENT_LEVEL="bare-metal-standard"
|
DEPLOYMENT_LEVEL="bare-metal"
|
||||||
|
|
||||||
# ─── TERMINAL BANNER ──────────────────────────────────────────────────────────
|
# ─── TERMINAL BANNER ──────────────────────────────────────────────────────────
|
||||||
# Mode: compact | full | auto | off
|
# Mode: compact | full | auto | off
|
||||||
OPSEC_BANNER="compact"
|
OPSEC_BANNER="full"
|
||||||
|
|
||||||
# ─── WIDGET THEME ────────────────────────────────────────────────────────────
|
# ─── WIDGET THEME ────────────────────────────────────────────────────────────
|
||||||
# Theme: default | aurora | ember | slate | cyberpunk | frost | terminal
|
# Theme: default | aurora | ember | slate | cyberpunk | frost | terminal
|
||||||
WIDGET_THEME="default"
|
WIDGET_THEME="apt"
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# Phantom — Provider Credentials
|
||||||
|
# Copy to .env and fill in your values.
|
||||||
|
# These are loaded automatically in standalone mode.
|
||||||
|
# Env vars take priority over values here.
|
||||||
|
|
||||||
|
# Linode
|
||||||
|
LINODE_TOKEN=
|
||||||
|
LINODE_REGION=us-east
|
||||||
|
LINODE_PLAN=g6-nanode-1
|
||||||
|
|
||||||
|
# AWS
|
||||||
|
AWS_ACCESS_KEY_ID=
|
||||||
|
AWS_SECRET_ACCESS_KEY=
|
||||||
|
AWS_REGION=us-east-1
|
||||||
|
AWS_INSTANCE_TYPE=t3.micro
|
||||||
+9
-5
@@ -8,12 +8,12 @@ Deploy self-hosted privacy infrastructure with a single command. Supports cloud
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Matrix + Element | Ready | Encrypted messaging homeserver with web client |
|
| Matrix + Element | Ready | Encrypted messaging homeserver with web client |
|
||||||
| WireGuard VPN | Ready | Private VPN server with client config generation |
|
| WireGuard VPN | Ready | Private VPN server with client config generation |
|
||||||
| Pi-hole DNS | Ready | Ad-blocking DNS server (Docker-based) |
|
| Pi-hole DNS | Ready | Ad-blocking DNS server (native) |
|
||||||
|
| Nextcloud | Ready | Self-hosted file sync and collaboration |
|
||||||
|
| Vaultwarden | Ready | Self-hosted Bitwarden password manager |
|
||||||
|
| Jellyfin | Ready | Self-hosted media server |
|
||||||
|
| Mail-in-a-Box | Ready | Self-hosted email |
|
||||||
| All-in-One | Ready | Multiple services on one server with nginx reverse proxy |
|
| All-in-One | Ready | Multiple services on one server with nginx reverse proxy |
|
||||||
| Nextcloud | Stub | Self-hosted file sync and collaboration |
|
|
||||||
| Vaultwarden | Stub | Self-hosted Bitwarden password manager |
|
|
||||||
| Jellyfin | Stub | Self-hosted media server |
|
|
||||||
| Mail-in-a-Box | Stub | Self-hosted email |
|
|
||||||
|
|
||||||
## Deployment Targets
|
## Deployment Targets
|
||||||
|
|
||||||
@@ -87,6 +87,10 @@ phantom/
|
|||||||
│ ├── matrix/ # Synapse + Element
|
│ ├── matrix/ # Synapse + Element
|
||||||
│ ├── vpn/ # WireGuard
|
│ ├── vpn/ # WireGuard
|
||||||
│ ├── dns/ # Pi-hole
|
│ ├── dns/ # Pi-hole
|
||||||
|
│ ├── cloud/ # Nextcloud
|
||||||
|
│ ├── vault/ # Vaultwarden
|
||||||
|
│ ├── media/ # Jellyfin
|
||||||
|
│ ├── email/ # Mail-in-a-Box
|
||||||
│ └── all_in_one/ # Multi-service composer
|
│ └── all_in_one/ # Multi-service composer
|
||||||
├── providers/ # Cloud provisioning
|
├── providers/ # Cloud provisioning
|
||||||
└── logs/ # Deployment artifacts
|
└── logs/ # Deployment artifacts
|
||||||
|
|||||||
@@ -15,9 +15,9 @@ SERVICES = [
|
|||||||
("matrix", "Matrix + Element", "Encrypted messaging"),
|
("matrix", "Matrix + Element", "Encrypted messaging"),
|
||||||
("vpn", "WireGuard VPN", "Private VPN server"),
|
("vpn", "WireGuard VPN", "Private VPN server"),
|
||||||
("dns", "Pi-hole DNS", "Ad-blocking DNS"),
|
("dns", "Pi-hole DNS", "Ad-blocking DNS"),
|
||||||
("cloud", "Nextcloud", "File sync (stub)"),
|
("cloud", "Nextcloud", "File sync"),
|
||||||
("vault", "Vaultwarden", "Password manager (stub)"),
|
("vault", "Vaultwarden", "Password manager"),
|
||||||
("media", "Jellyfin", "Media server (stub)"),
|
("media", "Jellyfin", "Media server"),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -66,18 +66,6 @@ def gather_config(config):
|
|||||||
print(f" {RED}No services selected.{RESET}")
|
print(f" {RED}No services selected.{RESET}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Warn about stub services
|
|
||||||
STUB_SERVICES = {"cloud", "vault", "media", "email"}
|
|
||||||
stubs_selected = [s for s in selected_services if s in STUB_SERVICES]
|
|
||||||
if stubs_selected:
|
|
||||||
stub_labels = {s[0]: s[1] for s in SERVICES}
|
|
||||||
print(f"\n {YELLOW}Warning: The following services are stubs (playbook not yet implemented):{RESET}")
|
|
||||||
for s in stubs_selected:
|
|
||||||
print(f" {YELLOW}- {stub_labels.get(s, s)}{RESET}")
|
|
||||||
proceed = input(f" {YELLOW}Continue anyway? [y/N]:{RESET} ").strip().lower()
|
|
||||||
if proceed != "y":
|
|
||||||
return None
|
|
||||||
|
|
||||||
config["services"] = selected_services
|
config["services"] = selected_services
|
||||||
config["all_in_one"] = True
|
config["all_in_one"] = True
|
||||||
|
|
||||||
@@ -116,7 +104,7 @@ def gather_config(config):
|
|||||||
if config is None:
|
if config is None:
|
||||||
return None
|
return None
|
||||||
except ImportError:
|
except ImportError:
|
||||||
pass # Stub module, skip
|
pass # Module not yet loaded, skip
|
||||||
# Restore base domain
|
# Restore base domain
|
||||||
config["domain"] = base_domain
|
config["domain"] = base_domain
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""Nextcloud deployment module (stub — playbook TODO)."""
|
"""Nextcloud deployment module — PHP-FPM + MariaDB + Redis + nginx."""
|
||||||
|
|
||||||
CYAN = "\033[38;5;51m"
|
CYAN = "\033[38;5;51m"
|
||||||
WHITE = "\033[38;5;255m"
|
WHITE = "\033[38;5;255m"
|
||||||
@@ -9,7 +9,7 @@ RESET = "\033[0m"
|
|||||||
def gather_config(config):
|
def gather_config(config):
|
||||||
"""Gather Nextcloud configuration."""
|
"""Gather Nextcloud configuration."""
|
||||||
print(f"\n{CYAN} ┌─ Nextcloud Configuration ─────────────────────────┐{RESET}")
|
print(f"\n{CYAN} ┌─ Nextcloud Configuration ─────────────────────────┐{RESET}")
|
||||||
print(f" {CYAN}│{RESET} {GREY}Note: Playbook coming soon{RESET}")
|
print(f" {CYAN}│{RESET} {GREY}PHP-FPM + MariaDB + Redis + nginx{RESET}")
|
||||||
|
|
||||||
config["domain"] = config.get("domain") or input(
|
config["domain"] = config.get("domain") or input(
|
||||||
f" {CYAN}│{RESET} Domain (e.g. cloud.example.com): "
|
f" {CYAN}│{RESET} Domain (e.g. cloud.example.com): "
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""Mail-in-a-Box deployment module (stub — playbook TODO)."""
|
"""Mail-in-a-Box deployment module — native installer script."""
|
||||||
|
|
||||||
CYAN = "\033[38;5;51m"
|
CYAN = "\033[38;5;51m"
|
||||||
WHITE = "\033[38;5;255m"
|
WHITE = "\033[38;5;255m"
|
||||||
@@ -9,7 +9,7 @@ RESET = "\033[0m"
|
|||||||
def gather_config(config):
|
def gather_config(config):
|
||||||
"""Gather Mail-in-a-Box configuration."""
|
"""Gather Mail-in-a-Box configuration."""
|
||||||
print(f"\n{CYAN} ┌─ Mail-in-a-Box Configuration ─────────────────────┐{RESET}")
|
print(f"\n{CYAN} ┌─ Mail-in-a-Box Configuration ─────────────────────┐{RESET}")
|
||||||
print(f" {CYAN}│{RESET} {GREY}Note: Playbook coming soon{RESET}")
|
print(f" {CYAN}│{RESET} {GREY}Complete mail stack — manages its own nginx/TLS/DNS{RESET}")
|
||||||
|
|
||||||
config["domain"] = config.get("domain") or input(
|
config["domain"] = config.get("domain") or input(
|
||||||
f" {CYAN}│{RESET} Mail domain (e.g. mail.example.com): "
|
f" {CYAN}│{RESET} Mail domain (e.g. mail.example.com): "
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ def gather_config(config):
|
|||||||
if config["domain"].startswith("matrix.") else config["domain"]
|
if config["domain"].startswith("matrix.") else config["domain"]
|
||||||
|
|
||||||
config["matrix_signing_key"] = secrets.token_hex(32)
|
config["matrix_signing_key"] = secrets.token_hex(32)
|
||||||
|
config["matrix_form_secret"] = secrets.token_hex(32)
|
||||||
|
config["matrix_macaroon_secret"] = secrets.token_hex(32)
|
||||||
|
|
||||||
print(f" {CYAN}└─────────────────────────────────────────────────────┘{RESET}")
|
print(f" {CYAN}└─────────────────────────────────────────────────────┘{RESET}")
|
||||||
return config
|
return config
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""Jellyfin media server deployment module (stub — playbook TODO)."""
|
"""Jellyfin media server deployment module — official apt repo + nginx."""
|
||||||
|
|
||||||
CYAN = "\033[38;5;51m"
|
CYAN = "\033[38;5;51m"
|
||||||
WHITE = "\033[38;5;255m"
|
WHITE = "\033[38;5;255m"
|
||||||
@@ -9,7 +9,7 @@ RESET = "\033[0m"
|
|||||||
def gather_config(config):
|
def gather_config(config):
|
||||||
"""Gather Jellyfin configuration."""
|
"""Gather Jellyfin configuration."""
|
||||||
print(f"\n{CYAN} ┌─ Jellyfin Configuration ──────────────────────────┐{RESET}")
|
print(f"\n{CYAN} ┌─ Jellyfin Configuration ──────────────────────────┐{RESET}")
|
||||||
print(f" {CYAN}│{RESET} {GREY}Note: Playbook coming soon{RESET}")
|
print(f" {CYAN}│{RESET} {GREY}Official apt repo + nginx reverse proxy{RESET}")
|
||||||
|
|
||||||
config["domain"] = config.get("domain") or input(
|
config["domain"] = config.get("domain") or input(
|
||||||
f" {CYAN}│{RESET} Domain (e.g. media.example.com): "
|
f" {CYAN}│{RESET} Domain (e.g. media.example.com): "
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""Vaultwarden (Bitwarden) deployment module (stub — playbook TODO)."""
|
"""Vaultwarden (Bitwarden-compatible) deployment module — binary + systemd + nginx."""
|
||||||
|
|
||||||
import secrets
|
import secrets
|
||||||
|
|
||||||
@@ -11,7 +11,7 @@ RESET = "\033[0m"
|
|||||||
def gather_config(config):
|
def gather_config(config):
|
||||||
"""Gather Vaultwarden configuration."""
|
"""Gather Vaultwarden configuration."""
|
||||||
print(f"\n{CYAN} ┌─ Vaultwarden Configuration ───────────────────────┐{RESET}")
|
print(f"\n{CYAN} ┌─ Vaultwarden Configuration ───────────────────────┐{RESET}")
|
||||||
print(f" {CYAN}│{RESET} {GREY}Note: Playbook coming soon{RESET}")
|
print(f" {CYAN}│{RESET} {GREY}Pre-built binary + systemd + nginx{RESET}")
|
||||||
|
|
||||||
config["domain"] = config.get("domain") or input(
|
config["domain"] = config.get("domain") or input(
|
||||||
f" {CYAN}│{RESET} Domain (e.g. vault.example.com): "
|
f" {CYAN}│{RESET} Domain (e.g. vault.example.com): "
|
||||||
|
|||||||
+721
-118
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,7 @@
|
|||||||
#
|
#
|
||||||
# NOTE: This playbook includes task files directly rather than
|
# NOTE: This playbook includes task files directly rather than
|
||||||
# importing full playbooks, to allow conditional composition.
|
# importing full playbooks, to allow conditional composition.
|
||||||
|
# NOTE: Email (MIAB) is excluded — it manages its own nginx/DNS/TLS.
|
||||||
|
|
||||||
- name: All-in-One Privacy Server
|
- name: All-in-One Privacy Server
|
||||||
hosts: all
|
hosts: all
|
||||||
@@ -41,6 +42,8 @@
|
|||||||
- "443"
|
- "443"
|
||||||
|
|
||||||
# ─── Deploy Individual Services (task files, not full playbooks) ──
|
# ─── Deploy Individual Services (task files, not full playbooks) ──
|
||||||
|
|
||||||
|
# Matrix
|
||||||
- name: Deploy Matrix — Synapse
|
- name: Deploy Matrix — Synapse
|
||||||
include_tasks: "{{ playbook_dir }}/../matrix/tasks/synapse.yml"
|
include_tasks: "{{ playbook_dir }}/../matrix/tasks/synapse.yml"
|
||||||
when: "'matrix' in selected_services"
|
when: "'matrix' in selected_services"
|
||||||
@@ -53,6 +56,14 @@
|
|||||||
include_tasks: "{{ playbook_dir }}/../matrix/tasks/nginx.yml"
|
include_tasks: "{{ playbook_dir }}/../matrix/tasks/nginx.yml"
|
||||||
when: "'matrix' in selected_services"
|
when: "'matrix' in selected_services"
|
||||||
|
|
||||||
|
- name: Deploy Matrix user creation script
|
||||||
|
template:
|
||||||
|
src: "{{ playbook_dir }}/../common/templates/add-matrix-user.sh.j2"
|
||||||
|
dest: /root/Tools/add-matrix-user.sh
|
||||||
|
mode: "0700"
|
||||||
|
when: "'matrix' in selected_services"
|
||||||
|
|
||||||
|
# WireGuard VPN
|
||||||
- name: Deploy WireGuard — Install
|
- name: Deploy WireGuard — Install
|
||||||
include_tasks: "{{ playbook_dir }}/../vpn/tasks/install.yml"
|
include_tasks: "{{ playbook_dir }}/../vpn/tasks/install.yml"
|
||||||
when: "'vpn' in selected_services"
|
when: "'vpn' in selected_services"
|
||||||
@@ -61,6 +72,7 @@
|
|||||||
include_tasks: "{{ playbook_dir }}/../vpn/tasks/configure.yml"
|
include_tasks: "{{ playbook_dir }}/../vpn/tasks/configure.yml"
|
||||||
when: "'vpn' in selected_services"
|
when: "'vpn' in selected_services"
|
||||||
|
|
||||||
|
# Pi-hole DNS
|
||||||
- name: Deploy Pi-hole — Install
|
- name: Deploy Pi-hole — Install
|
||||||
include_tasks: "{{ playbook_dir }}/../dns/tasks/install.yml"
|
include_tasks: "{{ playbook_dir }}/../dns/tasks/install.yml"
|
||||||
when: "'dns' in selected_services"
|
when: "'dns' in selected_services"
|
||||||
@@ -69,11 +81,44 @@
|
|||||||
include_tasks: "{{ playbook_dir }}/../dns/tasks/configure.yml"
|
include_tasks: "{{ playbook_dir }}/../dns/tasks/configure.yml"
|
||||||
when: "'dns' in selected_services"
|
when: "'dns' in selected_services"
|
||||||
|
|
||||||
# Stub services — print notice
|
# Nextcloud
|
||||||
- name: Notice for stub services
|
- name: Deploy Nextcloud — Install
|
||||||
debug:
|
include_tasks: "{{ playbook_dir }}/../cloud/tasks/install.yml"
|
||||||
msg: "Service '{{ item }}' playbook not yet implemented — skipping"
|
when: "'cloud' in selected_services"
|
||||||
loop: "{{ selected_services | select('in', ['cloud', 'vault', 'media', 'email']) | list }}"
|
|
||||||
|
- name: Deploy Nextcloud — Configure
|
||||||
|
include_tasks: "{{ playbook_dir }}/../cloud/tasks/configure.yml"
|
||||||
|
when: "'cloud' in selected_services"
|
||||||
|
|
||||||
|
- name: Deploy Nextcloud — Nginx vhost
|
||||||
|
include_tasks: "{{ playbook_dir }}/../cloud/tasks/nginx.yml"
|
||||||
|
when: "'cloud' in selected_services"
|
||||||
|
|
||||||
|
# Vaultwarden
|
||||||
|
- name: Deploy Vaultwarden — Install
|
||||||
|
include_tasks: "{{ playbook_dir }}/../vault/tasks/install.yml"
|
||||||
|
when: "'vault' in selected_services"
|
||||||
|
|
||||||
|
- name: Deploy Vaultwarden — Configure
|
||||||
|
include_tasks: "{{ playbook_dir }}/../vault/tasks/configure.yml"
|
||||||
|
when: "'vault' in selected_services"
|
||||||
|
|
||||||
|
- name: Deploy Vaultwarden — Nginx vhost
|
||||||
|
include_tasks: "{{ playbook_dir }}/../vault/tasks/nginx.yml"
|
||||||
|
when: "'vault' in selected_services"
|
||||||
|
|
||||||
|
# Jellyfin
|
||||||
|
- name: Deploy Jellyfin — Install
|
||||||
|
include_tasks: "{{ playbook_dir }}/../media/tasks/install.yml"
|
||||||
|
when: "'media' in selected_services"
|
||||||
|
|
||||||
|
- name: Deploy Jellyfin — Configure
|
||||||
|
include_tasks: "{{ playbook_dir }}/../media/tasks/configure.yml"
|
||||||
|
when: "'media' in selected_services"
|
||||||
|
|
||||||
|
- name: Deploy Jellyfin — Nginx vhost
|
||||||
|
include_tasks: "{{ playbook_dir }}/../media/tasks/nginx.yml"
|
||||||
|
when: "'media' in selected_services"
|
||||||
|
|
||||||
handlers:
|
handlers:
|
||||||
- name: reload nginx
|
- name: reload nginx
|
||||||
@@ -86,6 +131,36 @@
|
|||||||
name: matrix-synapse
|
name: matrix-synapse
|
||||||
state: restarted
|
state: restarted
|
||||||
|
|
||||||
|
- name: restart php-fpm
|
||||||
|
service:
|
||||||
|
name: php8.2-fpm
|
||||||
|
state: restarted
|
||||||
|
|
||||||
|
- name: restart mariadb
|
||||||
|
service:
|
||||||
|
name: mariadb
|
||||||
|
state: restarted
|
||||||
|
|
||||||
|
- name: restart redis
|
||||||
|
service:
|
||||||
|
name: redis-server
|
||||||
|
state: restarted
|
||||||
|
|
||||||
|
- name: restart vaultwarden
|
||||||
|
service:
|
||||||
|
name: vaultwarden
|
||||||
|
state: restarted
|
||||||
|
|
||||||
|
- name: restart jellyfin
|
||||||
|
service:
|
||||||
|
name: jellyfin
|
||||||
|
state: restarted
|
||||||
|
|
||||||
|
- name: restart pihole-FTL
|
||||||
|
service:
|
||||||
|
name: pihole-FTL
|
||||||
|
state: restarted
|
||||||
|
|
||||||
- name: restart sshd
|
- name: restart sshd
|
||||||
service:
|
service:
|
||||||
name: sshd
|
name: sshd
|
||||||
|
|||||||
@@ -1,11 +1,42 @@
|
|||||||
---
|
---
|
||||||
# Cloud deployment — Coming soon
|
# Nextcloud deployment — PHP-FPM + MariaDB + Redis + nginx
|
||||||
# This is a stub playbook. Full implementation planned.
|
|
||||||
|
|
||||||
- name: Deploy Cloud Server
|
- name: Deploy Nextcloud Server
|
||||||
hosts: all
|
hosts: all
|
||||||
become: true
|
become: true
|
||||||
|
vars:
|
||||||
|
nextcloud_domain: "{{ domain }}"
|
||||||
|
nextcloud_admin: "{{ cloud_admin_user | default('admin') }}"
|
||||||
|
nextcloud_storage_gb: "{{ cloud_storage_gb | default('10') }}"
|
||||||
|
target_host: "{{ target_host | default('localhost') }}"
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
- name: Placeholder
|
- name: Include Nextcloud installation
|
||||||
debug:
|
include_tasks: tasks/install.yml
|
||||||
msg: "Cloud playbook not yet implemented. Check phantom/README.md for status."
|
|
||||||
|
- name: Include Nextcloud configuration
|
||||||
|
include_tasks: tasks/configure.yml
|
||||||
|
|
||||||
|
- name: Include Nextcloud nginx reverse proxy
|
||||||
|
include_tasks: tasks/nginx.yml
|
||||||
|
|
||||||
|
handlers:
|
||||||
|
- name: reload nginx
|
||||||
|
service:
|
||||||
|
name: nginx
|
||||||
|
state: reloaded
|
||||||
|
|
||||||
|
- name: restart php-fpm
|
||||||
|
service:
|
||||||
|
name: php8.2-fpm
|
||||||
|
state: restarted
|
||||||
|
|
||||||
|
- name: restart mariadb
|
||||||
|
service:
|
||||||
|
name: mariadb
|
||||||
|
state: restarted
|
||||||
|
|
||||||
|
- name: restart redis
|
||||||
|
service:
|
||||||
|
name: redis-server
|
||||||
|
state: restarted
|
||||||
|
|||||||
@@ -0,0 +1,180 @@
|
|||||||
|
---
|
||||||
|
# Nextcloud database, app, and service configuration
|
||||||
|
|
||||||
|
- name: Generate MariaDB password for Nextcloud
|
||||||
|
command: openssl rand -base64 24
|
||||||
|
register: nc_db_password_gen
|
||||||
|
args:
|
||||||
|
creates: /var/www/nextcloud/.db_password
|
||||||
|
|
||||||
|
- name: Save DB password
|
||||||
|
copy:
|
||||||
|
content: "{{ nc_db_password_gen.stdout }}"
|
||||||
|
dest: /var/www/nextcloud/.db_password
|
||||||
|
owner: www-data
|
||||||
|
group: www-data
|
||||||
|
mode: "0600"
|
||||||
|
when: nc_db_password_gen.changed
|
||||||
|
|
||||||
|
- name: Read saved DB password
|
||||||
|
slurp:
|
||||||
|
src: /var/www/nextcloud/.db_password
|
||||||
|
register: nc_db_password_file
|
||||||
|
|
||||||
|
- name: Set DB password fact
|
||||||
|
set_fact:
|
||||||
|
nc_db_pass: "{{ (nc_db_password_file.content | b64decode).strip() }}"
|
||||||
|
|
||||||
|
- name: Create Nextcloud MariaDB database
|
||||||
|
mysql_db:
|
||||||
|
name: nextcloud
|
||||||
|
state: present
|
||||||
|
encoding: utf8mb4
|
||||||
|
collation: utf8mb4_general_ci
|
||||||
|
login_unix_socket: /var/run/mysqld/mysqld.sock
|
||||||
|
|
||||||
|
- name: Create Nextcloud MariaDB user
|
||||||
|
mysql_user:
|
||||||
|
name: nextcloud
|
||||||
|
password: "{{ nc_db_pass }}"
|
||||||
|
priv: "nextcloud.*:ALL"
|
||||||
|
host: localhost
|
||||||
|
state: present
|
||||||
|
login_unix_socket: /var/run/mysqld/mysqld.sock
|
||||||
|
|
||||||
|
- name: Generate admin password
|
||||||
|
command: openssl rand -base64 16
|
||||||
|
register: nc_admin_password_gen
|
||||||
|
args:
|
||||||
|
creates: /var/www/nextcloud/.admin_password
|
||||||
|
|
||||||
|
- name: Save admin password
|
||||||
|
copy:
|
||||||
|
content: "{{ nc_admin_password_gen.stdout }}"
|
||||||
|
dest: /var/www/nextcloud/.admin_password
|
||||||
|
owner: www-data
|
||||||
|
group: www-data
|
||||||
|
mode: "0600"
|
||||||
|
when: nc_admin_password_gen.changed
|
||||||
|
|
||||||
|
- name: Read saved admin password
|
||||||
|
slurp:
|
||||||
|
src: /var/www/nextcloud/.admin_password
|
||||||
|
register: nc_admin_password_file
|
||||||
|
|
||||||
|
- name: Configure PHP-FPM pool for Nextcloud
|
||||||
|
copy:
|
||||||
|
dest: /etc/php/8.2/fpm/pool.d/nextcloud.conf
|
||||||
|
content: |
|
||||||
|
[nextcloud]
|
||||||
|
user = www-data
|
||||||
|
group = www-data
|
||||||
|
listen = /run/php/php8.2-fpm-nextcloud.sock
|
||||||
|
listen.owner = www-data
|
||||||
|
listen.group = www-data
|
||||||
|
pm = dynamic
|
||||||
|
pm.max_children = 16
|
||||||
|
pm.start_servers = 4
|
||||||
|
pm.min_spare_servers = 2
|
||||||
|
pm.max_spare_servers = 8
|
||||||
|
pm.max_requests = 500
|
||||||
|
env[HOSTNAME] = $HOSTNAME
|
||||||
|
env[TMP] = /tmp
|
||||||
|
env[TMPDIR] = /tmp
|
||||||
|
env[TEMP] = /tmp
|
||||||
|
php_value[upload_max_filesize] = 10G
|
||||||
|
php_value[post_max_size] = 10G
|
||||||
|
php_value[memory_limit] = 512M
|
||||||
|
php_value[max_execution_time] = 3600
|
||||||
|
php_value[max_input_time] = 3600
|
||||||
|
php_value[opcache.enable] = 1
|
||||||
|
php_value[opcache.memory_consumption] = 128
|
||||||
|
php_value[opcache.interned_strings_buffer] = 16
|
||||||
|
php_value[opcache.max_accelerated_files] = 10000
|
||||||
|
php_value[opcache.revalidate_freq] = 1
|
||||||
|
mode: "0644"
|
||||||
|
notify: restart php-fpm
|
||||||
|
|
||||||
|
- name: Flush handlers to restart PHP-FPM
|
||||||
|
meta: flush_handlers
|
||||||
|
|
||||||
|
- name: Install Nextcloud via occ
|
||||||
|
become_user: www-data
|
||||||
|
command: >
|
||||||
|
php /var/www/nextcloud/occ maintenance:install
|
||||||
|
--database mysql
|
||||||
|
--database-name nextcloud
|
||||||
|
--database-user nextcloud
|
||||||
|
--database-pass "{{ nc_db_pass }}"
|
||||||
|
--admin-user "{{ nextcloud_admin }}"
|
||||||
|
--admin-pass "{{ (nc_admin_password_file.content | b64decode).strip() }}"
|
||||||
|
--data-dir /var/www/nextcloud/data
|
||||||
|
args:
|
||||||
|
creates: /var/www/nextcloud/config/config.php
|
||||||
|
|
||||||
|
- name: Set trusted domain
|
||||||
|
become_user: www-data
|
||||||
|
command: "php /var/www/nextcloud/occ config:system:set trusted_domains 0 --value={{ nextcloud_domain }}"
|
||||||
|
changed_when: true
|
||||||
|
|
||||||
|
- name: Set overwrite.cli.url
|
||||||
|
become_user: www-data
|
||||||
|
command: "php /var/www/nextcloud/occ config:system:set overwrite.cli.url --value=https://{{ nextcloud_domain }}"
|
||||||
|
changed_when: true
|
||||||
|
|
||||||
|
- name: Configure Redis caching
|
||||||
|
become_user: www-data
|
||||||
|
command: "php /var/www/nextcloud/occ config:system:set {{ item.key }} --value={{ item.value }} {{ item.type | default('') }}"
|
||||||
|
loop:
|
||||||
|
- { key: "memcache.local", value: "\\OC\\Memcache\\APCu" }
|
||||||
|
- { key: "memcache.distributed", value: "\\OC\\Memcache\\Redis" }
|
||||||
|
- { key: "memcache.locking", value: "\\OC\\Memcache\\Redis" }
|
||||||
|
- { key: "redis host", value: "localhost" }
|
||||||
|
- { key: "redis port", value: "6379", type: "--type=integer" }
|
||||||
|
changed_when: true
|
||||||
|
|
||||||
|
- name: Set default phone region
|
||||||
|
become_user: www-data
|
||||||
|
command: "php /var/www/nextcloud/occ config:system:set default_phone_region --value=US"
|
||||||
|
changed_when: true
|
||||||
|
|
||||||
|
- name: Deploy Nextcloud cron systemd timer
|
||||||
|
copy:
|
||||||
|
dest: /etc/systemd/system/nextcloud-cron.service
|
||||||
|
content: |
|
||||||
|
[Unit]
|
||||||
|
Description=Nextcloud cron.php
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
User=www-data
|
||||||
|
ExecStart=/usr/bin/php /var/www/nextcloud/cron.php
|
||||||
|
Type=oneshot
|
||||||
|
mode: "0644"
|
||||||
|
|
||||||
|
- name: Deploy Nextcloud cron timer
|
||||||
|
copy:
|
||||||
|
dest: /etc/systemd/system/nextcloud-cron.timer
|
||||||
|
content: |
|
||||||
|
[Unit]
|
||||||
|
Description=Run Nextcloud cron.php every 5 minutes
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnBootSec=5min
|
||||||
|
OnUnitActiveSec=5min
|
||||||
|
Unit=nextcloud-cron.service
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
|
mode: "0644"
|
||||||
|
|
||||||
|
- name: Enable Nextcloud cron timer
|
||||||
|
systemd:
|
||||||
|
name: nextcloud-cron.timer
|
||||||
|
state: started
|
||||||
|
enabled: true
|
||||||
|
daemon_reload: true
|
||||||
|
|
||||||
|
- name: Display admin credentials
|
||||||
|
debug:
|
||||||
|
msg: "Nextcloud admin credentials saved to /var/www/nextcloud/.admin_password — access at https://{{ nextcloud_domain }}"
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
---
|
||||||
|
# Nextcloud package and dependency installation
|
||||||
|
|
||||||
|
- name: Install Nextcloud dependencies
|
||||||
|
apt:
|
||||||
|
name:
|
||||||
|
- php8.2-fpm
|
||||||
|
- php8.2-xml
|
||||||
|
- php8.2-mbstring
|
||||||
|
- php8.2-gd
|
||||||
|
- php8.2-curl
|
||||||
|
- php8.2-zip
|
||||||
|
- php8.2-intl
|
||||||
|
- php8.2-mysql
|
||||||
|
- php8.2-redis
|
||||||
|
- php8.2-apcu
|
||||||
|
- php8.2-imagick
|
||||||
|
- php8.2-bcmath
|
||||||
|
- php8.2-gmp
|
||||||
|
- mariadb-server
|
||||||
|
- redis-server
|
||||||
|
- nginx
|
||||||
|
- certbot
|
||||||
|
- python3-certbot-nginx
|
||||||
|
- unzip
|
||||||
|
- curl
|
||||||
|
- ca-certificates
|
||||||
|
state: present
|
||||||
|
update_cache: true
|
||||||
|
|
||||||
|
- name: Enable and start MariaDB
|
||||||
|
service:
|
||||||
|
name: mariadb
|
||||||
|
state: started
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
- name: Enable and start Redis
|
||||||
|
service:
|
||||||
|
name: redis-server
|
||||||
|
state: started
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
- name: Enable and start PHP-FPM
|
||||||
|
service:
|
||||||
|
name: php8.2-fpm
|
||||||
|
state: started
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
- name: Get latest Nextcloud version
|
||||||
|
uri:
|
||||||
|
url: https://download.nextcloud.com/server/releases/latest.tar.bz2.sha256
|
||||||
|
return_content: true
|
||||||
|
register: nc_checksum_response
|
||||||
|
|
||||||
|
- name: Parse Nextcloud checksum
|
||||||
|
set_fact:
|
||||||
|
nc_checksum: "{{ nc_checksum_response.content.split()[0] }}"
|
||||||
|
|
||||||
|
- name: Download Nextcloud tarball
|
||||||
|
get_url:
|
||||||
|
url: https://download.nextcloud.com/server/releases/latest.tar.bz2
|
||||||
|
dest: /tmp/nextcloud.tar.bz2
|
||||||
|
checksum: "sha256:{{ nc_checksum }}"
|
||||||
|
mode: "0644"
|
||||||
|
|
||||||
|
- name: Extract Nextcloud
|
||||||
|
unarchive:
|
||||||
|
src: /tmp/nextcloud.tar.bz2
|
||||||
|
dest: /var/www/
|
||||||
|
remote_src: true
|
||||||
|
creates: /var/www/nextcloud/index.php
|
||||||
|
|
||||||
|
- name: Set Nextcloud ownership
|
||||||
|
file:
|
||||||
|
path: /var/www/nextcloud
|
||||||
|
state: directory
|
||||||
|
owner: www-data
|
||||||
|
group: www-data
|
||||||
|
recurse: true
|
||||||
|
|
||||||
|
- name: Create Nextcloud data directory
|
||||||
|
file:
|
||||||
|
path: /var/www/nextcloud/data
|
||||||
|
state: directory
|
||||||
|
owner: www-data
|
||||||
|
group: www-data
|
||||||
|
mode: "0770"
|
||||||
|
|
||||||
|
- name: Allow HTTP/HTTPS through UFW
|
||||||
|
ufw:
|
||||||
|
rule: allow
|
||||||
|
port: "{{ item }}"
|
||||||
|
proto: tcp
|
||||||
|
loop:
|
||||||
|
- "80"
|
||||||
|
- "443"
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
---
|
||||||
|
# Nginx reverse proxy for Nextcloud
|
||||||
|
|
||||||
|
- name: Create certbot webroot
|
||||||
|
file:
|
||||||
|
path: /var/www/certbot
|
||||||
|
state: directory
|
||||||
|
owner: www-data
|
||||||
|
group: www-data
|
||||||
|
mode: "0755"
|
||||||
|
|
||||||
|
- name: Deploy HTTP-only nginx config (for certbot)
|
||||||
|
copy:
|
||||||
|
dest: /etc/nginx/sites-available/nextcloud
|
||||||
|
content: |
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name {{ nextcloud_domain }};
|
||||||
|
location /.well-known/acme-challenge/ { root /var/www/certbot; }
|
||||||
|
location / { return 200 'phantom setup in progress'; add_header Content-Type text/plain; }
|
||||||
|
}
|
||||||
|
mode: "0644"
|
||||||
|
notify: reload nginx
|
||||||
|
|
||||||
|
- name: Enable Nextcloud nginx site
|
||||||
|
file:
|
||||||
|
src: /etc/nginx/sites-available/nextcloud
|
||||||
|
dest: /etc/nginx/sites-enabled/nextcloud
|
||||||
|
state: link
|
||||||
|
notify: reload nginx
|
||||||
|
|
||||||
|
- name: Flush handlers to apply HTTP config
|
||||||
|
meta: flush_handlers
|
||||||
|
|
||||||
|
- name: Setup TLS (certbot with self-signed fallback)
|
||||||
|
include_tasks: ../../common/tls_setup.yml
|
||||||
|
vars:
|
||||||
|
_tls_domain: "{{ nextcloud_domain }}"
|
||||||
|
|
||||||
|
- name: Deploy SSL nginx config
|
||||||
|
copy:
|
||||||
|
dest: /etc/nginx/sites-available/nextcloud
|
||||||
|
content: |
|
||||||
|
upstream php-handler {
|
||||||
|
server unix:/run/php/php8.2-fpm-nextcloud.sock;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name {{ nextcloud_domain }};
|
||||||
|
location /.well-known/acme-challenge/ { root /var/www/certbot; }
|
||||||
|
location / { return 301 https://$host$request_uri; }
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 443 ssl http2;
|
||||||
|
server_name {{ nextcloud_domain }};
|
||||||
|
|
||||||
|
ssl_certificate {{ _ssl_cert }};
|
||||||
|
ssl_certificate_key {{ _ssl_key }};
|
||||||
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
|
ssl_ciphers HIGH:!aNULL:!MD5;
|
||||||
|
ssl_prefer_server_ciphers on;
|
||||||
|
|
||||||
|
server_tokens off;
|
||||||
|
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
|
||||||
|
add_header X-Content-Type-Options nosniff always;
|
||||||
|
add_header X-Frame-Options SAMEORIGIN always;
|
||||||
|
add_header X-Robots-Tag "noindex, nofollow" always;
|
||||||
|
add_header Referrer-Policy no-referrer always;
|
||||||
|
|
||||||
|
client_max_body_size 10G;
|
||||||
|
client_body_timeout 3600s;
|
||||||
|
fastcgi_buffers 64 4K;
|
||||||
|
|
||||||
|
root /var/www/nextcloud;
|
||||||
|
|
||||||
|
location = /.well-known/carddav { return 301 $scheme://$host/remote.php/dav; }
|
||||||
|
location = /.well-known/caldav { return 301 $scheme://$host/remote.php/dav; }
|
||||||
|
location = /.well-known/webfinger { return 301 $scheme://$host/index.php/.well-known/webfinger; }
|
||||||
|
location = /.well-known/nodeinfo { return 301 $scheme://$host/index.php/.well-known/nodeinfo; }
|
||||||
|
|
||||||
|
location / { rewrite ^ /index.php; }
|
||||||
|
|
||||||
|
location ~ ^\/(?:build|tests|config|lib|3rdparty|templates|data)\/ { deny all; }
|
||||||
|
location ~ ^\/(?:\.|autotest|occ|issue|indie|db_|console) { deny all; }
|
||||||
|
|
||||||
|
location ~ ^\/(?:index|remote|public|cron|core\/ajax\/update|status|ocs\/v[12]|updater\/.+|ocs-provider\/.+)\.php(?:$|\/) {
|
||||||
|
fastcgi_split_path_info ^(.+?\.php)(\/.*|)$;
|
||||||
|
set $path_info $fastcgi_path_info;
|
||||||
|
try_files $fastcgi_script_name =404;
|
||||||
|
include fastcgi_params;
|
||||||
|
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||||
|
fastcgi_param PATH_INFO $path_info;
|
||||||
|
fastcgi_param HTTPS on;
|
||||||
|
fastcgi_param modHeadersAvailable true;
|
||||||
|
fastcgi_param front_controller_active true;
|
||||||
|
fastcgi_pass php-handler;
|
||||||
|
fastcgi_intercept_errors on;
|
||||||
|
fastcgi_request_buffering off;
|
||||||
|
fastcgi_read_timeout 3600;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ ^\/(?:updater|ocs-provider)(?:$|\/) { try_files $uri/ =404; index index.php; }
|
||||||
|
location ~ \.(?:css|js|woff2?|svg|gif|map)$ { try_files $uri /index.php$request_uri; add_header Cache-Control "public, max-age=15778463"; access_log off; }
|
||||||
|
location ~ \.(?:png|html|ttf|ico|jpg|jpeg|bcmap|mp4|webm)$ { try_files $uri /index.php$request_uri; access_log off; }
|
||||||
|
}
|
||||||
|
mode: "0644"
|
||||||
|
notify: reload nginx
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
hosts: all
|
hosts: all
|
||||||
become: true
|
become: true
|
||||||
vars:
|
vars:
|
||||||
ssh_port: "{{ ssh_port | default(22) }}"
|
_ssh_port: "{{ ssh_port | default('22') }}"
|
||||||
ssh_allow_password: false
|
ssh_allow_password: false
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
@@ -51,7 +51,7 @@
|
|||||||
- name: Allow SSH through UFW
|
- name: Allow SSH through UFW
|
||||||
ufw:
|
ufw:
|
||||||
rule: allow
|
rule: allow
|
||||||
port: "{{ ssh_port }}"
|
port: "{{ _ssh_port }}"
|
||||||
proto: tcp
|
proto: tcp
|
||||||
|
|
||||||
- name: Enable UFW
|
- name: Enable UFW
|
||||||
@@ -59,18 +59,45 @@
|
|||||||
state: enabled
|
state: enabled
|
||||||
|
|
||||||
# ─── Fail2ban ───────────────────────────────────────────────────────
|
# ─── Fail2ban ───────────────────────────────────────────────────────
|
||||||
- name: Configure fail2ban SSH jail
|
- name: Configure fail2ban jails
|
||||||
copy:
|
copy:
|
||||||
dest: /etc/fail2ban/jail.local
|
dest: /etc/fail2ban/jail.local
|
||||||
content: |
|
content: |
|
||||||
[sshd]
|
[DEFAULT]
|
||||||
enabled = true
|
|
||||||
port = {{ ssh_port }}
|
|
||||||
filter = sshd
|
|
||||||
logpath = /var/log/auth.log
|
|
||||||
maxretry = 5
|
|
||||||
bantime = 3600
|
bantime = 3600
|
||||||
findtime = 600
|
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"
|
mode: "0644"
|
||||||
notify: restart fail2ban
|
notify: restart fail2ban
|
||||||
|
|
||||||
@@ -103,6 +130,41 @@
|
|||||||
line: "X11Forwarding no"
|
line: "X11Forwarding no"
|
||||||
notify: restart sshd
|
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 ─────────────────────────────────────
|
# ─── Automatic Security Updates ─────────────────────────────────────
|
||||||
- name: Enable unattended upgrades for security
|
- name: Enable unattended upgrades for security
|
||||||
copy:
|
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"
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
# Pi-hole DNS server deployment (Docker-based)
|
# Pi-hole DNS server deployment (native installer)
|
||||||
|
|
||||||
- name: Deploy Pi-hole DNS Server
|
- name: Deploy Pi-hole DNS Server
|
||||||
hosts: all
|
hosts: all
|
||||||
@@ -16,3 +16,9 @@
|
|||||||
|
|
||||||
- name: Include Pi-hole configuration
|
- name: Include Pi-hole configuration
|
||||||
include_tasks: tasks/configure.yml
|
include_tasks: tasks/configure.yml
|
||||||
|
|
||||||
|
handlers:
|
||||||
|
- name: restart pihole-FTL
|
||||||
|
service:
|
||||||
|
name: pihole-FTL
|
||||||
|
state: restarted
|
||||||
|
|||||||
@@ -1,66 +1,66 @@
|
|||||||
---
|
---
|
||||||
# Pi-hole Docker configuration and launch
|
# Pi-hole native configuration
|
||||||
|
|
||||||
- name: Create Pi-hole directories
|
|
||||||
file:
|
|
||||||
path: "{{ item }}"
|
|
||||||
state: directory
|
|
||||||
mode: "0755"
|
|
||||||
loop:
|
|
||||||
- /opt/pihole
|
|
||||||
- /opt/pihole/etc-pihole
|
|
||||||
- /opt/pihole/etc-dnsmasq.d
|
|
||||||
|
|
||||||
- name: Generate Pi-hole admin password
|
- name: Generate Pi-hole admin password
|
||||||
shell: "openssl rand -base64 16"
|
command: openssl rand -base64 16
|
||||||
register: pihole_password
|
register: pihole_password_gen
|
||||||
args:
|
args:
|
||||||
creates: /opt/pihole/.password
|
creates: /etc/pihole/.phantom_password
|
||||||
|
|
||||||
- name: Save admin password
|
- name: Save admin password
|
||||||
copy:
|
copy:
|
||||||
content: "{{ pihole_password.stdout }}"
|
content: "{{ pihole_password_gen.stdout }}"
|
||||||
dest: /opt/pihole/.password
|
dest: /etc/pihole/.phantom_password
|
||||||
mode: "0600"
|
mode: "0600"
|
||||||
when: pihole_password.changed
|
when: pihole_password_gen.changed
|
||||||
|
|
||||||
- name: Deploy Pi-hole Docker Compose
|
- name: Read saved password
|
||||||
copy:
|
slurp:
|
||||||
dest: /opt/pihole/docker-compose.yml
|
src: /etc/pihole/.phantom_password
|
||||||
content: |
|
register: pihole_password_file
|
||||||
services:
|
|
||||||
pihole:
|
|
||||||
container_name: pihole
|
|
||||||
image: pihole/pihole:latest
|
|
||||||
ports:
|
|
||||||
- "53:53/tcp"
|
|
||||||
- "53:53/udp"
|
|
||||||
- "80:80/tcp"
|
|
||||||
environment:
|
|
||||||
TZ: UTC
|
|
||||||
WEBPASSWORD_FILE: /run/secrets/webpassword
|
|
||||||
PIHOLE_DNS_: "{{ pihole_upstream }}"
|
|
||||||
DNSSEC: "true"
|
|
||||||
QUERY_LOGGING: "false"
|
|
||||||
volumes:
|
|
||||||
- /opt/pihole/etc-pihole:/etc/pihole
|
|
||||||
- /opt/pihole/etc-dnsmasq.d:/etc/dnsmasq.d
|
|
||||||
secrets:
|
|
||||||
- webpassword
|
|
||||||
restart: unless-stopped
|
|
||||||
dns:
|
|
||||||
- 127.0.0.1
|
|
||||||
- 9.9.9.9
|
|
||||||
secrets:
|
|
||||||
webpassword:
|
|
||||||
file: /opt/pihole/.password
|
|
||||||
mode: "0644"
|
|
||||||
|
|
||||||
- name: Start Pi-hole
|
- name: Set Pi-hole admin password
|
||||||
shell: cd /opt/pihole && docker compose up -d
|
command: "pihole -a -p {{ (pihole_password_file.content | b64decode).strip() }}"
|
||||||
args:
|
changed_when: true
|
||||||
creates: /opt/pihole/etc-pihole/pihole-FTL.db
|
|
||||||
|
|
||||||
- name: Display admin password
|
- name: Set upstream DNS servers
|
||||||
|
command: "pihole -a setdns {{ pihole_upstream | replace(';', ' ') }}"
|
||||||
|
changed_when: true
|
||||||
|
|
||||||
|
- name: Configure blocklist level
|
||||||
|
block:
|
||||||
|
- name: Update gravity (standard blocklist)
|
||||||
|
command: pihole -g
|
||||||
|
changed_when: true
|
||||||
|
when: pihole_blocklist == "standard"
|
||||||
|
|
||||||
|
- name: Add aggressive blocklists
|
||||||
|
lineinfile:
|
||||||
|
path: /etc/pihole/adlists.list
|
||||||
|
line: "{{ item }}"
|
||||||
|
create: true
|
||||||
|
mode: "0644"
|
||||||
|
loop:
|
||||||
|
- "https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts"
|
||||||
|
- "https://raw.githubusercontent.com/hagezi/dns-blocklists/main/adblock/pro.txt"
|
||||||
|
when: pihole_blocklist == "aggressive"
|
||||||
|
notify: update gravity
|
||||||
|
|
||||||
|
- name: Update gravity after blocklist changes
|
||||||
|
command: pihole -g
|
||||||
|
changed_when: true
|
||||||
|
when: pihole_blocklist == "aggressive"
|
||||||
|
|
||||||
|
- name: Enable DNSSEC
|
||||||
|
command: pihole -a dnssec on
|
||||||
|
changed_when: true
|
||||||
|
|
||||||
|
- name: Ensure pihole-FTL is running
|
||||||
|
service:
|
||||||
|
name: pihole-FTL
|
||||||
|
state: started
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
- name: Display admin credentials
|
||||||
debug:
|
debug:
|
||||||
msg: "Pi-hole admin password: {{ pihole_password.stdout | default('(see /opt/pihole/.password)') }}"
|
msg: "Pi-hole admin password saved to /etc/pihole/.phantom_password — access admin at http://{{ ansible_default_ipv4.address | default('server_ip') }}/admin"
|
||||||
|
|||||||
@@ -1,40 +1,17 @@
|
|||||||
---
|
---
|
||||||
# Pi-hole installation via Docker
|
# Pi-hole native installation (no Docker)
|
||||||
|
|
||||||
- name: Install Docker prerequisites
|
- name: Install Pi-hole prerequisites
|
||||||
apt:
|
apt:
|
||||||
name:
|
name:
|
||||||
- apt-transport-https
|
|
||||||
- ca-certificates
|
|
||||||
- curl
|
- curl
|
||||||
- gnupg
|
- git
|
||||||
- lsb-release
|
- iproute2
|
||||||
|
- whiptail
|
||||||
|
- ca-certificates
|
||||||
|
- cron
|
||||||
state: present
|
state: present
|
||||||
|
update_cache: true
|
||||||
- name: Add Docker GPG key
|
|
||||||
apt_key:
|
|
||||||
url: https://download.docker.com/linux/{{ ansible_distribution | lower }}/gpg
|
|
||||||
state: present
|
|
||||||
|
|
||||||
- name: Add Docker repository
|
|
||||||
apt_repository:
|
|
||||||
repo: "deb https://download.docker.com/linux/{{ ansible_distribution | lower }} {{ ansible_distribution_release }} stable"
|
|
||||||
state: present
|
|
||||||
|
|
||||||
- name: Install Docker
|
|
||||||
apt:
|
|
||||||
name:
|
|
||||||
- docker-ce
|
|
||||||
- docker-ce-cli
|
|
||||||
- containerd.io
|
|
||||||
- docker-compose-plugin
|
|
||||||
state: present
|
|
||||||
|
|
||||||
- name: Enable Docker service
|
|
||||||
service:
|
|
||||||
name: docker
|
|
||||||
state: started
|
|
||||||
enabled: true
|
|
||||||
|
|
||||||
- name: Stop systemd-resolved (conflicts with Pi-hole on port 53)
|
- name: Stop systemd-resolved (conflicts with Pi-hole on port 53)
|
||||||
service:
|
service:
|
||||||
@@ -43,7 +20,7 @@
|
|||||||
enabled: false
|
enabled: false
|
||||||
failed_when: false
|
failed_when: false
|
||||||
|
|
||||||
- name: Set DNS fallback
|
- name: Set DNS fallback for installation
|
||||||
copy:
|
copy:
|
||||||
dest: /etc/resolv.conf
|
dest: /etc/resolv.conf
|
||||||
content: |
|
content: |
|
||||||
@@ -51,6 +28,45 @@
|
|||||||
nameserver 1.1.1.1
|
nameserver 1.1.1.1
|
||||||
mode: "0644"
|
mode: "0644"
|
||||||
|
|
||||||
|
- name: Create Pi-hole config directory
|
||||||
|
file:
|
||||||
|
path: /etc/pihole
|
||||||
|
state: directory
|
||||||
|
mode: "0755"
|
||||||
|
|
||||||
|
- name: Deploy setupVars.conf for unattended install
|
||||||
|
copy:
|
||||||
|
dest: /etc/pihole/setupVars.conf
|
||||||
|
content: |
|
||||||
|
PIHOLE_INTERFACE={{ ansible_default_ipv4.interface | default('eth0') }}
|
||||||
|
PIHOLE_DNS_1={{ pihole_upstream.split(';')[0] | default('9.9.9.9') }}
|
||||||
|
PIHOLE_DNS_2={{ pihole_upstream.split(';')[1] | default('149.112.112.112') }}
|
||||||
|
QUERY_LOGGING=false
|
||||||
|
INSTALL_WEB_SERVER=true
|
||||||
|
INSTALL_WEB_INTERFACE=true
|
||||||
|
LIGHTTPD_ENABLED=false
|
||||||
|
CACHE_SIZE=10000
|
||||||
|
DNS_FQDN_REQUIRED=true
|
||||||
|
DNS_BOGUS_PRIV=true
|
||||||
|
DNSSEC=true
|
||||||
|
BLOCKING_ENABLED=true
|
||||||
|
WEBPASSWORD=
|
||||||
|
mode: "0644"
|
||||||
|
|
||||||
|
- name: Clone Pi-hole repository
|
||||||
|
git:
|
||||||
|
repo: https://github.com/pi-hole/pi-hole.git
|
||||||
|
dest: /opt/pihole-install
|
||||||
|
version: master
|
||||||
|
depth: 1
|
||||||
|
|
||||||
|
- name: Run Pi-hole installer (unattended)
|
||||||
|
command: bash /opt/pihole-install/automated\ install/basic-install.sh --unattended
|
||||||
|
args:
|
||||||
|
creates: /usr/local/bin/pihole
|
||||||
|
environment:
|
||||||
|
PIHOLE_SKIP_OS_CHECK: "true"
|
||||||
|
|
||||||
- name: Allow DNS through UFW
|
- name: Allow DNS through UFW
|
||||||
ufw:
|
ufw:
|
||||||
rule: allow
|
rule: allow
|
||||||
@@ -59,4 +75,14 @@
|
|||||||
loop:
|
loop:
|
||||||
- { port: "53", proto: "tcp" }
|
- { port: "53", proto: "tcp" }
|
||||||
- { port: "53", proto: "udp" }
|
- { port: "53", proto: "udp" }
|
||||||
- { port: "80", proto: "tcp" }
|
|
||||||
|
- name: Allow Pi-hole admin from private networks only
|
||||||
|
ufw:
|
||||||
|
rule: allow
|
||||||
|
port: "80"
|
||||||
|
proto: tcp
|
||||||
|
from_ip: "{{ item }}"
|
||||||
|
loop:
|
||||||
|
- "10.0.0.0/8"
|
||||||
|
- "172.16.0.0/12"
|
||||||
|
- "192.168.0.0/16"
|
||||||
|
|||||||
@@ -1,11 +1,18 @@
|
|||||||
---
|
---
|
||||||
# Email deployment — Coming soon
|
# Mail-in-a-Box deployment — native installer script
|
||||||
# This is a stub playbook. Full implementation planned.
|
# MIAB manages its own nginx, TLS, DNS, and mail stack
|
||||||
|
|
||||||
- name: Deploy Email Server
|
- name: Deploy Mail-in-a-Box Email Server
|
||||||
hosts: all
|
hosts: all
|
||||||
become: true
|
become: true
|
||||||
|
vars:
|
||||||
|
miab_domain: "{{ domain }}"
|
||||||
|
miab_first_user: "{{ email_first_user }}"
|
||||||
|
target_host: "{{ target_host | default('localhost') }}"
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
- name: Placeholder
|
- name: Include Mail-in-a-Box installation
|
||||||
debug:
|
include_tasks: tasks/install.yml
|
||||||
msg: "Email playbook not yet implemented. Check phantom/README.md for status."
|
|
||||||
|
- name: Include Mail-in-a-Box configuration
|
||||||
|
include_tasks: tasks/configure.yml
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
---
|
||||||
|
# Mail-in-a-Box non-interactive installation
|
||||||
|
|
||||||
|
- name: Generate admin password
|
||||||
|
command: openssl rand -base64 16
|
||||||
|
register: miab_admin_password
|
||||||
|
args:
|
||||||
|
creates: /root/.miab_admin_password
|
||||||
|
|
||||||
|
- name: Save admin password
|
||||||
|
copy:
|
||||||
|
content: "{{ miab_admin_password.stdout }}"
|
||||||
|
dest: /root/.miab_admin_password
|
||||||
|
mode: "0600"
|
||||||
|
when: miab_admin_password.changed
|
||||||
|
|
||||||
|
- name: Read saved admin password
|
||||||
|
slurp:
|
||||||
|
src: /root/.miab_admin_password
|
||||||
|
register: miab_password_file
|
||||||
|
|
||||||
|
- name: Clone Mail-in-a-Box repository
|
||||||
|
git:
|
||||||
|
repo: https://github.com/mail-in-a-box/mailinabox.git
|
||||||
|
dest: /opt/mailinabox
|
||||||
|
version: main
|
||||||
|
depth: 1
|
||||||
|
|
||||||
|
- name: Run Mail-in-a-Box installer (non-interactive)
|
||||||
|
command: bash /opt/mailinabox/setup/start.sh
|
||||||
|
args:
|
||||||
|
creates: /etc/mailinabox.conf
|
||||||
|
environment:
|
||||||
|
NONINTERACTIVE: "1"
|
||||||
|
PRIMARY_HOSTNAME: "{{ miab_domain }}"
|
||||||
|
EMAIL_ADDR: "{{ miab_first_user }}"
|
||||||
|
EMAIL_PW: "{{ (miab_password_file.content | b64decode).strip() }}"
|
||||||
|
timeout: 600
|
||||||
|
|
||||||
|
- name: Display admin credentials
|
||||||
|
debug:
|
||||||
|
msg: >
|
||||||
|
Mail-in-a-Box installed. Admin: {{ miab_first_user }}
|
||||||
|
Password saved to /root/.miab_admin_password
|
||||||
|
Web admin: https://{{ miab_domain }}/admin
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
---
|
||||||
|
# Mail-in-a-Box prerequisites and environment setup
|
||||||
|
|
||||||
|
- name: Verify Ubuntu distribution
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- ansible_distribution == "Ubuntu"
|
||||||
|
fail_msg: "Mail-in-a-Box requires Ubuntu. Detected: {{ ansible_distribution }}"
|
||||||
|
|
||||||
|
- name: Stop conflicting nginx (MIAB manages its own)
|
||||||
|
service:
|
||||||
|
name: nginx
|
||||||
|
state: stopped
|
||||||
|
enabled: false
|
||||||
|
failed_when: false
|
||||||
|
|
||||||
|
- name: Set system hostname for MIAB
|
||||||
|
hostname:
|
||||||
|
name: "{{ miab_domain }}"
|
||||||
|
|
||||||
|
- name: Update /etc/hostname
|
||||||
|
copy:
|
||||||
|
content: "{{ miab_domain }}\n"
|
||||||
|
dest: /etc/hostname
|
||||||
|
mode: "0644"
|
||||||
|
|
||||||
|
- name: Ensure hostname in /etc/hosts
|
||||||
|
lineinfile:
|
||||||
|
path: /etc/hosts
|
||||||
|
regexp: '^127\.0\.1\.1'
|
||||||
|
line: "127.0.1.1 {{ miab_domain }}"
|
||||||
|
|
||||||
|
- name: Allow mail-related ports through UFW
|
||||||
|
ufw:
|
||||||
|
rule: allow
|
||||||
|
port: "{{ item.port }}"
|
||||||
|
proto: "{{ item.proto }}"
|
||||||
|
loop:
|
||||||
|
- { port: "25", proto: "tcp" }
|
||||||
|
- { port: "587", proto: "tcp" }
|
||||||
|
- { port: "993", proto: "tcp" }
|
||||||
|
- { port: "465", proto: "tcp" }
|
||||||
|
- { port: "80", proto: "tcp" }
|
||||||
|
- { port: "443", proto: "tcp" }
|
||||||
|
- { port: "53", proto: "tcp" }
|
||||||
|
- { port: "53", proto: "udp" }
|
||||||
|
- { port: "4190", proto: "tcp" }
|
||||||
|
|
||||||
|
- name: Install prerequisites
|
||||||
|
apt:
|
||||||
|
name:
|
||||||
|
- curl
|
||||||
|
- ca-certificates
|
||||||
|
- git
|
||||||
|
state: present
|
||||||
|
update_cache: true
|
||||||
@@ -26,6 +26,18 @@
|
|||||||
- name: Include nginx reverse proxy tasks
|
- name: Include nginx reverse proxy tasks
|
||||||
include_tasks: tasks/nginx.yml
|
include_tasks: tasks/nginx.yml
|
||||||
|
|
||||||
|
- name: Create Tools directory
|
||||||
|
file:
|
||||||
|
path: /root/Tools
|
||||||
|
state: directory
|
||||||
|
mode: "0755"
|
||||||
|
|
||||||
|
- name: Deploy Matrix user creation script
|
||||||
|
template:
|
||||||
|
src: ../common/templates/add-matrix-user.sh.j2
|
||||||
|
dest: /root/Tools/add-matrix-user.sh
|
||||||
|
mode: "0700"
|
||||||
|
|
||||||
handlers:
|
handlers:
|
||||||
- name: restart synapse
|
- name: restart synapse
|
||||||
service:
|
service:
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
---
|
---
|
||||||
# Nginx reverse proxy for Matrix + Element
|
# Nginx reverse proxy for Matrix + Element
|
||||||
# Two-phase: HTTP-only first for certbot, then full SSL config
|
|
||||||
|
|
||||||
- name: Create certbot webroot
|
- name: Create certbot webroot
|
||||||
file:
|
file:
|
||||||
@@ -17,15 +16,8 @@
|
|||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
server_name {{ matrix_domain }};
|
server_name {{ matrix_domain }};
|
||||||
|
location /.well-known/acme-challenge/ { root /var/www/certbot; }
|
||||||
location /.well-known/acme-challenge/ {
|
location / { return 200 'phantom setup in progress'; add_header Content-Type text/plain; }
|
||||||
root /var/www/certbot;
|
|
||||||
}
|
|
||||||
|
|
||||||
location / {
|
|
||||||
return 200 'phantom setup in progress';
|
|
||||||
add_header Content-Type text/plain;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
mode: "0644"
|
mode: "0644"
|
||||||
notify: reload nginx
|
notify: reload nginx
|
||||||
@@ -40,35 +32,38 @@
|
|||||||
- name: Flush handlers to apply HTTP config
|
- name: Flush handlers to apply HTTP config
|
||||||
meta: flush_handlers
|
meta: flush_handlers
|
||||||
|
|
||||||
- name: Obtain TLS certificate
|
- name: Setup TLS (certbot with self-signed fallback)
|
||||||
command: >
|
include_tasks: ../../common/tls_setup.yml
|
||||||
certbot certonly --webroot -w /var/www/certbot
|
vars:
|
||||||
-d {{ matrix_domain }}
|
_tls_domain: "{{ matrix_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/{{ matrix_domain }}/fullchain.pem"
|
|
||||||
|
|
||||||
- name: Deploy full SSL nginx config
|
- name: Disable server tokens in nginx
|
||||||
|
lineinfile:
|
||||||
|
path: /etc/nginx/nginx.conf
|
||||||
|
regexp: "^\\s*#?\\s*server_tokens"
|
||||||
|
line: "\tserver_tokens off;"
|
||||||
|
insertafter: "sendfile on;"
|
||||||
|
notify: reload nginx
|
||||||
|
|
||||||
|
- name: Remove default nginx site
|
||||||
|
file:
|
||||||
|
path: /etc/nginx/sites-enabled/default
|
||||||
|
state: absent
|
||||||
|
notify: reload nginx
|
||||||
|
|
||||||
|
- name: Deploy hardened SSL nginx config
|
||||||
copy:
|
copy:
|
||||||
dest: /etc/nginx/sites-available/matrix
|
dest: /etc/nginx/sites-available/matrix
|
||||||
content: |
|
content: |
|
||||||
|
# Rate limiting zones
|
||||||
|
limit_req_zone $binary_remote_addr zone=matrix_login:10m rate=3r/m;
|
||||||
|
limit_req_zone $binary_remote_addr zone=matrix_general:10m rate=10r/s;
|
||||||
|
|
||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
server_name {{ matrix_domain }};
|
server_name {{ matrix_domain }};
|
||||||
|
location /.well-known/acme-challenge/ { root /var/www/certbot; }
|
||||||
location /.well-known/acme-challenge/ {
|
location / { return 301 https://$host$request_uri; }
|
||||||
root /var/www/certbot;
|
|
||||||
}
|
|
||||||
|
|
||||||
location / {
|
|
||||||
return 301 https://$host$request_uri;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
server {
|
server {
|
||||||
@@ -76,24 +71,51 @@
|
|||||||
listen 8448 ssl http2;
|
listen 8448 ssl http2;
|
||||||
server_name {{ matrix_domain }};
|
server_name {{ matrix_domain }};
|
||||||
|
|
||||||
ssl_certificate /etc/letsencrypt/live/{{ matrix_domain }}/fullchain.pem;
|
# TLS
|
||||||
ssl_certificate_key /etc/letsencrypt/live/{{ matrix_domain }}/privkey.pem;
|
ssl_certificate {{ _ssl_cert }};
|
||||||
|
ssl_certificate_key {{ _ssl_key }};
|
||||||
ssl_protocols TLSv1.2 TLSv1.3;
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
ssl_ciphers HIGH:!aNULL:!MD5;
|
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305;
|
||||||
ssl_prefer_server_ciphers on;
|
ssl_prefer_server_ciphers off;
|
||||||
|
ssl_session_timeout 1d;
|
||||||
|
ssl_session_cache shared:SSL:10m;
|
||||||
|
ssl_session_tickets off;
|
||||||
|
|
||||||
|
# Security headers
|
||||||
|
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
|
||||||
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||||
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
|
add_header X-XSS-Protection "1; mode=block" always;
|
||||||
|
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||||
|
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
|
||||||
|
|
||||||
client_max_body_size 50m;
|
client_max_body_size 50m;
|
||||||
|
|
||||||
# Synapse
|
# Block Synapse admin API from external access
|
||||||
location ~* ^(\/_matrix|\/_synapse\/client) {
|
location /_synapse/admin {
|
||||||
|
return 403;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Login endpoint — strict rate limit
|
||||||
|
location /_matrix/client/v3/login {
|
||||||
|
limit_req zone=matrix_login burst=3 nodelay;
|
||||||
proxy_pass http://127.0.0.1:8008;
|
proxy_pass http://127.0.0.1:8008;
|
||||||
proxy_set_header X-Forwarded-For $remote_addr;
|
proxy_set_header X-Forwarded-For $remote_addr;
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
}
|
}
|
||||||
|
|
||||||
# .well-known delegation
|
# Matrix client + federation API
|
||||||
|
location ~* ^(\/_matrix|\/_synapse\/client) {
|
||||||
|
limit_req zone=matrix_general burst=20 nodelay;
|
||||||
|
proxy_pass http://127.0.0.1:8008;
|
||||||
|
proxy_set_header X-Forwarded-For $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_read_timeout 600s;
|
||||||
|
proxy_send_timeout 600s;
|
||||||
|
}
|
||||||
|
|
||||||
location /.well-known/matrix/server {
|
location /.well-known/matrix/server {
|
||||||
return 200 '{"m.server": "{{ matrix_domain }}:443"}';
|
return 200 '{"m.server": "{{ matrix_domain }}:443"}';
|
||||||
add_header Content-Type application/json;
|
add_header Content-Type application/json;
|
||||||
@@ -105,10 +127,11 @@
|
|||||||
add_header Access-Control-Allow-Origin *;
|
add_header Access-Control-Allow-Origin *;
|
||||||
}
|
}
|
||||||
|
|
||||||
# Element Web (if enabled)
|
# Element Web
|
||||||
location / {
|
location / {
|
||||||
root /var/www/element;
|
root /var/www/element;
|
||||||
try_files $uri $uri/ /index.html;
|
try_files $uri $uri/ /index.html;
|
||||||
|
add_header Content-Security-Policy "frame-ancestors 'self'" always;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mode: "0644"
|
mode: "0644"
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
---
|
---
|
||||||
# Synapse homeserver installation and configuration
|
# Synapse homeserver installation and configuration
|
||||||
|
# Uses SQLite3 (matches c2itall chat-server pattern — no PostgreSQL needed)
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
apt:
|
apt:
|
||||||
@@ -7,24 +8,25 @@
|
|||||||
- python3
|
- python3
|
||||||
- python3-pip
|
- python3-pip
|
||||||
- python3-venv
|
- python3-venv
|
||||||
- libpq-dev
|
|
||||||
- postgresql
|
|
||||||
- postgresql-contrib
|
|
||||||
- nginx
|
- nginx
|
||||||
- certbot
|
- certbot
|
||||||
- python3-certbot-nginx
|
- python3-certbot-nginx
|
||||||
|
- curl
|
||||||
|
- wget
|
||||||
|
- gnupg
|
||||||
|
- lsb-release
|
||||||
state: present
|
state: present
|
||||||
|
|
||||||
- name: Add Matrix Synapse apt repository key
|
- name: Add Matrix Synapse GPG key and repository
|
||||||
apt_key:
|
shell: |
|
||||||
url: https://packages.matrix.org/debian/matrix-org-archive-keyring.gpg
|
wget -qO /usr/share/keyrings/matrix-org-archive-keyring.gpg https://packages.matrix.org/debian/matrix-org-archive-keyring.gpg
|
||||||
state: present
|
echo "deb [signed-by=/usr/share/keyrings/matrix-org-archive-keyring.gpg] https://packages.matrix.org/debian/ $(lsb_release -cs) main" | tee /etc/apt/sources.list.d/matrix-org.list
|
||||||
|
args:
|
||||||
|
creates: /etc/apt/sources.list.d/matrix-org.list
|
||||||
|
|
||||||
- name: Add Matrix Synapse apt repository
|
- name: Update apt cache after adding Matrix repo
|
||||||
apt_repository:
|
apt:
|
||||||
repo: "deb https://packages.matrix.org/debian/ {{ ansible_distribution_release }} main"
|
update_cache: true
|
||||||
state: present
|
|
||||||
filename: matrix-org
|
|
||||||
|
|
||||||
- name: Install Synapse
|
- name: Install Synapse
|
||||||
apt:
|
apt:
|
||||||
@@ -33,22 +35,17 @@
|
|||||||
environment:
|
environment:
|
||||||
DEBIAN_FRONTEND: noninteractive
|
DEBIAN_FRONTEND: noninteractive
|
||||||
|
|
||||||
- name: Create PostgreSQL database
|
- name: Set correct ownership for Matrix directories
|
||||||
become_user: postgres
|
file:
|
||||||
postgresql_db:
|
path: "{{ item }}"
|
||||||
name: synapse
|
state: directory
|
||||||
encoding: UTF-8
|
owner: matrix-synapse
|
||||||
lc_collate: C
|
group: matrix-synapse
|
||||||
lc_ctype: C
|
mode: "0750"
|
||||||
template: template0
|
recurse: true
|
||||||
|
loop:
|
||||||
- name: Create PostgreSQL user
|
- /var/lib/matrix-synapse
|
||||||
become_user: postgres
|
- /var/log/matrix-synapse
|
||||||
postgresql_user:
|
|
||||||
name: synapse
|
|
||||||
password: "{{ matrix_signing_key[:32] }}"
|
|
||||||
db: synapse
|
|
||||||
priv: ALL
|
|
||||||
|
|
||||||
- name: Deploy Synapse homeserver config
|
- name: Deploy Synapse homeserver config
|
||||||
template:
|
template:
|
||||||
@@ -59,6 +56,11 @@
|
|||||||
mode: "0640"
|
mode: "0640"
|
||||||
notify: restart synapse
|
notify: restart synapse
|
||||||
|
|
||||||
|
- name: Clear conflicting conf.d configurations
|
||||||
|
shell: |
|
||||||
|
rm -f /etc/matrix-synapse/conf.d/*.yaml 2>/dev/null || true
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
- name: Allow Matrix federation port through UFW
|
- name: Allow Matrix federation port through UFW
|
||||||
ufw:
|
ufw:
|
||||||
rule: allow
|
rule: allow
|
||||||
@@ -77,23 +79,25 @@
|
|||||||
- name: Enable and start Synapse
|
- name: Enable and start Synapse
|
||||||
service:
|
service:
|
||||||
name: matrix-synapse
|
name: matrix-synapse
|
||||||
state: started
|
state: restarted
|
||||||
enabled: true
|
enabled: true
|
||||||
|
|
||||||
- name: Wait for Synapse to be ready
|
- name: Wait for Synapse to be ready
|
||||||
wait_for:
|
wait_for:
|
||||||
port: 8008
|
port: 8008
|
||||||
host: 127.0.0.1
|
host: 127.0.0.1
|
||||||
timeout: 30
|
delay: 5
|
||||||
|
timeout: 60
|
||||||
|
|
||||||
- name: Create admin user
|
- name: Create admin user
|
||||||
command: >
|
shell: |
|
||||||
register_new_matrix_user
|
/opt/venvs/matrix-synapse/bin/register_new_matrix_user \
|
||||||
-u {{ matrix_admin }}
|
-c /etc/matrix-synapse/homeserver.yaml \
|
||||||
-p {{ matrix_admin_pass }}
|
-u {{ matrix_admin }} \
|
||||||
-a
|
-p {{ matrix_admin_pass }} \
|
||||||
-c /etc/matrix-synapse/homeserver.yaml
|
-a
|
||||||
http://localhost:8008
|
|
||||||
register: admin_create
|
register: admin_create
|
||||||
failed_when: false
|
failed_when:
|
||||||
changed_when: admin_create.rc == 0
|
- admin_create.rc != 0
|
||||||
|
- "'User ID already taken' not in admin_create.stderr"
|
||||||
|
changed_when: "'User ID already taken' not in admin_create.stderr"
|
||||||
|
|||||||
@@ -2,43 +2,83 @@
|
|||||||
## Generated by phantom
|
## Generated by phantom
|
||||||
|
|
||||||
server_name: "{{ matrix_server_name }}"
|
server_name: "{{ matrix_server_name }}"
|
||||||
pid_file: /run/matrix-synapse.pid
|
public_baseurl: "https://{{ matrix_domain }}/"
|
||||||
|
pid_file: /var/run/matrix-synapse.pid
|
||||||
|
|
||||||
listeners:
|
listeners:
|
||||||
- port: 8008
|
- port: 8008
|
||||||
tls: false
|
tls: false
|
||||||
type: http
|
type: http
|
||||||
x_forwarded: true
|
x_forwarded: true
|
||||||
|
bind_addresses:
|
||||||
|
- '127.0.0.1'
|
||||||
resources:
|
resources:
|
||||||
- names: [client, federation]
|
- names: [client, federation]
|
||||||
compress: false
|
compress: false
|
||||||
|
|
||||||
database:
|
database:
|
||||||
name: psycopg2
|
name: sqlite3
|
||||||
args:
|
args:
|
||||||
user: synapse
|
database: /var/lib/matrix-synapse/homeserver.db
|
||||||
password: "{{ matrix_signing_key[:32] }}"
|
|
||||||
database: synapse
|
|
||||||
host: localhost
|
|
||||||
cp_min: 5
|
|
||||||
cp_max: 10
|
|
||||||
|
|
||||||
log_config: "/etc/matrix-synapse/log.yaml"
|
log_config: "/etc/matrix-synapse/log.yaml"
|
||||||
media_store_path: /var/lib/matrix-synapse/media
|
media_store_path: /var/lib/matrix-synapse/media
|
||||||
signing_key_path: "/etc/matrix-synapse/{{ matrix_server_name }}.signing.key"
|
signing_key_path: "/etc/matrix-synapse/homeserver.signing.key"
|
||||||
|
|
||||||
|
registration_shared_secret: "{{ matrix_signing_key }}"
|
||||||
enable_registration: {{ matrix_registration_enabled | lower }}
|
enable_registration: {{ matrix_registration_enabled | lower }}
|
||||||
{% if not matrix_registration_enabled %}
|
{% if not matrix_registration_enabled %}
|
||||||
enable_registration_without_verification: false
|
enable_registration_without_verification: false
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
allow_guest_access: false
|
||||||
|
|
||||||
suppress_key_server_warning: true
|
form_secret: "{{ matrix_form_secret | default(matrix_signing_key[:28]) }}"
|
||||||
|
macaroon_secret_key: "{{ matrix_macaroon_secret | default(matrix_signing_key[:30]) }}"
|
||||||
|
|
||||||
trusted_key_servers:
|
trusted_key_servers:
|
||||||
- server_name: "matrix.org"
|
- server_name: "matrix.org"
|
||||||
|
|
||||||
max_upload_size: 50M
|
# Rate limiting
|
||||||
|
rc_message:
|
||||||
|
per_second: 0.2
|
||||||
|
burst_count: 10
|
||||||
|
rc_login:
|
||||||
|
address:
|
||||||
|
per_second: 0.17
|
||||||
|
burst_count: 3
|
||||||
|
account:
|
||||||
|
per_second: 0.17
|
||||||
|
burst_count: 3
|
||||||
|
rc_registration:
|
||||||
|
per_second: 0.17
|
||||||
|
burst_count: 3
|
||||||
|
rc_admin_redaction:
|
||||||
|
per_second: 1
|
||||||
|
burst_count: 50
|
||||||
|
|
||||||
|
# Privacy / access control
|
||||||
|
require_auth_for_profile_requests: true
|
||||||
|
limit_profile_requests_to_users_who_share_rooms: true
|
||||||
|
allow_public_rooms_without_auth: false
|
||||||
|
allow_public_rooms_over_federation: false
|
||||||
|
|
||||||
|
# Media
|
||||||
|
enable_media_repo: true
|
||||||
|
max_upload_size: 50M
|
||||||
|
max_image_pixels: 32M
|
||||||
|
|
||||||
|
# Telemetry
|
||||||
|
enable_metrics: false
|
||||||
|
report_stats: false
|
||||||
|
|
||||||
|
# Session / auth hardening
|
||||||
|
session_lifetime: 24h
|
||||||
|
bcrypt_rounds: 12
|
||||||
|
|
||||||
|
# Federation TLS
|
||||||
|
federation_client_minimum_tls_version: 1.2
|
||||||
|
|
||||||
|
# URL preview with SSRF protection
|
||||||
url_preview_enabled: true
|
url_preview_enabled: true
|
||||||
url_preview_ip_range_blacklist:
|
url_preview_ip_range_blacklist:
|
||||||
- '127.0.0.0/8'
|
- '127.0.0.0/8'
|
||||||
@@ -46,8 +86,9 @@ url_preview_ip_range_blacklist:
|
|||||||
- '172.16.0.0/12'
|
- '172.16.0.0/12'
|
||||||
- '192.168.0.0/16'
|
- '192.168.0.0/16'
|
||||||
- '100.64.0.0/10'
|
- '100.64.0.0/10'
|
||||||
- '192.0.0.0/24'
|
|
||||||
- '169.254.0.0/16'
|
- '169.254.0.0/16'
|
||||||
- '::1/128'
|
- '::1/128'
|
||||||
- 'fe80::/10'
|
- 'fe80::/10'
|
||||||
- 'fc00::/7'
|
- 'fc00::/7'
|
||||||
|
|
||||||
|
suppress_key_server_warning: true
|
||||||
|
|||||||
@@ -1,11 +1,31 @@
|
|||||||
---
|
---
|
||||||
# Media deployment — Coming soon
|
# Jellyfin media server deployment (native package from official repo)
|
||||||
# This is a stub playbook. Full implementation planned.
|
|
||||||
|
|
||||||
- name: Deploy Media Server
|
- name: Deploy Jellyfin Media Server
|
||||||
hosts: all
|
hosts: all
|
||||||
become: true
|
become: true
|
||||||
|
vars:
|
||||||
|
jellyfin_domain: "{{ domain }}"
|
||||||
|
jellyfin_library: "{{ media_library_path | default('/srv/media') }}"
|
||||||
|
target_host: "{{ target_host | default('localhost') }}"
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
- name: Placeholder
|
- name: Include Jellyfin installation
|
||||||
debug:
|
include_tasks: tasks/install.yml
|
||||||
msg: "Media playbook not yet implemented. Check phantom/README.md for status."
|
|
||||||
|
- name: Include Jellyfin configuration
|
||||||
|
include_tasks: tasks/configure.yml
|
||||||
|
|
||||||
|
- name: Include Jellyfin nginx reverse proxy
|
||||||
|
include_tasks: tasks/nginx.yml
|
||||||
|
|
||||||
|
handlers:
|
||||||
|
- name: reload nginx
|
||||||
|
service:
|
||||||
|
name: nginx
|
||||||
|
state: reloaded
|
||||||
|
|
||||||
|
- name: restart jellyfin
|
||||||
|
service:
|
||||||
|
name: jellyfin
|
||||||
|
state: restarted
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
---
|
||||||
|
# Jellyfin service configuration
|
||||||
|
|
||||||
|
- name: Create media library directory
|
||||||
|
file:
|
||||||
|
path: "{{ jellyfin_library }}"
|
||||||
|
state: directory
|
||||||
|
owner: jellyfin
|
||||||
|
group: jellyfin
|
||||||
|
mode: "0755"
|
||||||
|
|
||||||
|
- name: Ensure Jellyfin binds to localhost only
|
||||||
|
lineinfile:
|
||||||
|
path: /etc/jellyfin/network.xml
|
||||||
|
regexp: '<LocalNetworkAddresses>'
|
||||||
|
line: ' <LocalNetworkAddresses><string>127.0.0.1</string></LocalNetworkAddresses>'
|
||||||
|
create: true
|
||||||
|
mode: "0644"
|
||||||
|
notify: restart jellyfin
|
||||||
|
failed_when: false
|
||||||
|
|
||||||
|
- name: Enable and start Jellyfin service
|
||||||
|
service:
|
||||||
|
name: jellyfin
|
||||||
|
state: started
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
- name: Wait for Jellyfin to be ready
|
||||||
|
wait_for:
|
||||||
|
port: 8096
|
||||||
|
host: 127.0.0.1
|
||||||
|
delay: 5
|
||||||
|
timeout: 60
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
---
|
||||||
|
# Jellyfin installation from official apt repository
|
||||||
|
|
||||||
|
- name: Install prerequisites
|
||||||
|
apt:
|
||||||
|
name:
|
||||||
|
- curl
|
||||||
|
- gnupg
|
||||||
|
- ca-certificates
|
||||||
|
- nginx
|
||||||
|
- certbot
|
||||||
|
- python3-certbot-nginx
|
||||||
|
state: present
|
||||||
|
update_cache: true
|
||||||
|
|
||||||
|
- name: Add Jellyfin GPG signing key
|
||||||
|
ansible.builtin.get_url:
|
||||||
|
url: https://repo.jellyfin.org/jellyfin_team.gpg.key
|
||||||
|
dest: /usr/share/keyrings/jellyfin-archive-keyring.asc
|
||||||
|
mode: "0644"
|
||||||
|
|
||||||
|
- name: Add Jellyfin apt repository
|
||||||
|
apt_repository:
|
||||||
|
repo: "deb [signed-by=/usr/share/keyrings/jellyfin-archive-keyring.asc] https://repo.jellyfin.org/{{ ansible_distribution | lower }} {{ ansible_distribution_release }} main"
|
||||||
|
state: present
|
||||||
|
filename: jellyfin
|
||||||
|
|
||||||
|
- name: Install Jellyfin
|
||||||
|
apt:
|
||||||
|
name: jellyfin
|
||||||
|
state: present
|
||||||
|
update_cache: true
|
||||||
|
|
||||||
|
- name: Allow HTTP/HTTPS through UFW
|
||||||
|
ufw:
|
||||||
|
rule: allow
|
||||||
|
port: "{{ item }}"
|
||||||
|
proto: tcp
|
||||||
|
loop:
|
||||||
|
- "80"
|
||||||
|
- "443"
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
---
|
||||||
|
# Nginx reverse proxy for Jellyfin
|
||||||
|
|
||||||
|
- name: Create certbot webroot
|
||||||
|
file:
|
||||||
|
path: /var/www/certbot
|
||||||
|
state: directory
|
||||||
|
owner: www-data
|
||||||
|
group: www-data
|
||||||
|
mode: "0755"
|
||||||
|
|
||||||
|
- name: Deploy HTTP-only nginx config (for certbot)
|
||||||
|
copy:
|
||||||
|
dest: /etc/nginx/sites-available/jellyfin
|
||||||
|
content: |
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name {{ jellyfin_domain }};
|
||||||
|
location /.well-known/acme-challenge/ { root /var/www/certbot; }
|
||||||
|
location / { return 200 'phantom setup in progress'; add_header Content-Type text/plain; }
|
||||||
|
}
|
||||||
|
mode: "0644"
|
||||||
|
notify: reload nginx
|
||||||
|
|
||||||
|
- name: Enable Jellyfin nginx site
|
||||||
|
file:
|
||||||
|
src: /etc/nginx/sites-available/jellyfin
|
||||||
|
dest: /etc/nginx/sites-enabled/jellyfin
|
||||||
|
state: link
|
||||||
|
notify: reload nginx
|
||||||
|
|
||||||
|
- name: Flush handlers to apply HTTP config
|
||||||
|
meta: flush_handlers
|
||||||
|
|
||||||
|
- name: Setup TLS (certbot with self-signed fallback)
|
||||||
|
include_tasks: ../../common/tls_setup.yml
|
||||||
|
vars:
|
||||||
|
_tls_domain: "{{ jellyfin_domain }}"
|
||||||
|
|
||||||
|
- name: Deploy SSL nginx config
|
||||||
|
copy:
|
||||||
|
dest: /etc/nginx/sites-available/jellyfin
|
||||||
|
content: |
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name {{ jellyfin_domain }};
|
||||||
|
location /.well-known/acme-challenge/ { root /var/www/certbot; }
|
||||||
|
location / { return 301 https://$host$request_uri; }
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 443 ssl http2;
|
||||||
|
server_name {{ jellyfin_domain }};
|
||||||
|
|
||||||
|
ssl_certificate {{ _ssl_cert }};
|
||||||
|
ssl_certificate_key {{ _ssl_key }};
|
||||||
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
|
ssl_ciphers HIGH:!aNULL:!MD5;
|
||||||
|
ssl_prefer_server_ciphers on;
|
||||||
|
|
||||||
|
server_tokens off;
|
||||||
|
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
|
||||||
|
add_header X-Content-Type-Options nosniff always;
|
||||||
|
add_header X-Frame-Options SAMEORIGIN always;
|
||||||
|
|
||||||
|
client_max_body_size 100m;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://127.0.0.1:8096;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
}
|
||||||
|
|
||||||
|
location /socket {
|
||||||
|
proxy_pass http://127.0.0.1:8096;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mode: "0644"
|
||||||
|
notify: reload nginx
|
||||||
@@ -1,11 +1,32 @@
|
|||||||
---
|
---
|
||||||
# Vault deployment — Coming soon
|
# Vaultwarden (Bitwarden-compatible) deployment — pre-built binary + systemd
|
||||||
# This is a stub playbook. Full implementation planned.
|
|
||||||
|
|
||||||
- name: Deploy Vault Server
|
- name: Deploy Vaultwarden Password Manager
|
||||||
hosts: all
|
hosts: all
|
||||||
become: true
|
become: true
|
||||||
|
vars:
|
||||||
|
vaultwarden_domain: "{{ domain }}"
|
||||||
|
vaultwarden_admin_token: "{{ vault_admin_token }}"
|
||||||
|
vaultwarden_signups: "{{ vault_signups_allowed | default(false) }}"
|
||||||
|
target_host: "{{ target_host | default('localhost') }}"
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
- name: Placeholder
|
- name: Include Vaultwarden installation
|
||||||
debug:
|
include_tasks: tasks/install.yml
|
||||||
msg: "Vault playbook not yet implemented. Check phantom/README.md for status."
|
|
||||||
|
- name: Include Vaultwarden configuration
|
||||||
|
include_tasks: tasks/configure.yml
|
||||||
|
|
||||||
|
- name: Include Vaultwarden nginx reverse proxy
|
||||||
|
include_tasks: tasks/nginx.yml
|
||||||
|
|
||||||
|
handlers:
|
||||||
|
- name: reload nginx
|
||||||
|
service:
|
||||||
|
name: nginx
|
||||||
|
state: reloaded
|
||||||
|
|
||||||
|
- name: restart vaultwarden
|
||||||
|
service:
|
||||||
|
name: vaultwarden
|
||||||
|
state: restarted
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
---
|
||||||
|
# Vaultwarden service configuration
|
||||||
|
|
||||||
|
- name: Deploy Vaultwarden environment file
|
||||||
|
copy:
|
||||||
|
dest: /opt/vaultwarden/.env
|
||||||
|
content: |
|
||||||
|
DOMAIN=https://{{ vaultwarden_domain }}
|
||||||
|
SIGNUPS_ALLOWED={{ vaultwarden_signups | lower }}
|
||||||
|
ADMIN_TOKEN={{ vaultwarden_admin_token }}
|
||||||
|
ROCKET_ADDRESS=127.0.0.1
|
||||||
|
ROCKET_PORT=8081
|
||||||
|
DATA_FOLDER=/opt/vaultwarden/data
|
||||||
|
WEB_VAULT_FOLDER=/opt/vaultwarden/web-vault
|
||||||
|
LOG_FILE=/opt/vaultwarden/data/vaultwarden.log
|
||||||
|
LOG_LEVEL=warn
|
||||||
|
SENDS_ALLOWED=true
|
||||||
|
EMERGENCY_ACCESS_ALLOWED=true
|
||||||
|
WEBSOCKET_ENABLED=true
|
||||||
|
WEBSOCKET_ADDRESS=127.0.0.1
|
||||||
|
WEBSOCKET_PORT=3012
|
||||||
|
owner: vaultwarden
|
||||||
|
group: vaultwarden
|
||||||
|
mode: "0600"
|
||||||
|
notify: restart vaultwarden
|
||||||
|
|
||||||
|
- name: Deploy Vaultwarden systemd unit
|
||||||
|
copy:
|
||||||
|
dest: /etc/systemd/system/vaultwarden.service
|
||||||
|
content: |
|
||||||
|
[Unit]
|
||||||
|
Description=Vaultwarden (Bitwarden-compatible server)
|
||||||
|
Documentation=https://github.com/dani-garcia/vaultwarden
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
User=vaultwarden
|
||||||
|
Group=vaultwarden
|
||||||
|
EnvironmentFile=/opt/vaultwarden/.env
|
||||||
|
ExecStart=/opt/vaultwarden/vaultwarden
|
||||||
|
WorkingDirectory=/opt/vaultwarden
|
||||||
|
|
||||||
|
# Hardening
|
||||||
|
ProtectSystem=strict
|
||||||
|
ProtectHome=true
|
||||||
|
PrivateTmp=true
|
||||||
|
PrivateDevices=true
|
||||||
|
NoNewPrivileges=true
|
||||||
|
ReadWritePaths=/opt/vaultwarden/data
|
||||||
|
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
mode: "0644"
|
||||||
|
|
||||||
|
- name: Reload systemd daemon
|
||||||
|
systemd:
|
||||||
|
daemon_reload: true
|
||||||
|
|
||||||
|
- name: Enable and start Vaultwarden
|
||||||
|
service:
|
||||||
|
name: vaultwarden
|
||||||
|
state: started
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
- name: Wait for Vaultwarden to be ready
|
||||||
|
wait_for:
|
||||||
|
port: 8081
|
||||||
|
host: 127.0.0.1
|
||||||
|
delay: 3
|
||||||
|
timeout: 30
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
---
|
||||||
|
# Vaultwarden binary installation from GitHub releases
|
||||||
|
|
||||||
|
- name: Install prerequisites
|
||||||
|
apt:
|
||||||
|
name:
|
||||||
|
- nginx
|
||||||
|
- certbot
|
||||||
|
- python3-certbot-nginx
|
||||||
|
- libssl-dev
|
||||||
|
- pkg-config
|
||||||
|
- curl
|
||||||
|
- ca-certificates
|
||||||
|
state: present
|
||||||
|
update_cache: true
|
||||||
|
|
||||||
|
- name: Create vaultwarden system user
|
||||||
|
user:
|
||||||
|
name: vaultwarden
|
||||||
|
system: true
|
||||||
|
shell: /usr/sbin/nologin
|
||||||
|
home: /opt/vaultwarden
|
||||||
|
create_home: false
|
||||||
|
|
||||||
|
- name: Create Vaultwarden directories
|
||||||
|
file:
|
||||||
|
path: "{{ item }}"
|
||||||
|
state: directory
|
||||||
|
owner: vaultwarden
|
||||||
|
group: vaultwarden
|
||||||
|
mode: "0750"
|
||||||
|
loop:
|
||||||
|
- /opt/vaultwarden
|
||||||
|
- /opt/vaultwarden/data
|
||||||
|
|
||||||
|
- name: Detect system architecture
|
||||||
|
command: uname -m
|
||||||
|
register: system_arch
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Set architecture for download
|
||||||
|
set_fact:
|
||||||
|
vw_arch: "{{ 'aarch64' if system_arch.stdout == 'aarch64' else 'x86_64' }}"
|
||||||
|
|
||||||
|
- name: Get latest Vaultwarden release tag
|
||||||
|
uri:
|
||||||
|
url: https://api.github.com/repos/dani-garcia/vaultwarden/releases/latest
|
||||||
|
return_content: true
|
||||||
|
headers:
|
||||||
|
Accept: application/vnd.github+json
|
||||||
|
register: vw_release
|
||||||
|
|
||||||
|
- name: Set Vaultwarden version
|
||||||
|
set_fact:
|
||||||
|
vw_version: "{{ vw_release.json.tag_name }}"
|
||||||
|
|
||||||
|
- name: Download Vaultwarden binary
|
||||||
|
get_url:
|
||||||
|
url: "https://github.com/dani-garcia/vaultwarden/releases/download/{{ vw_version }}/vaultwarden-{{ vw_version }}-linux-{{ vw_arch }}.tar.gz"
|
||||||
|
dest: "/tmp/vaultwarden-{{ vw_version }}.tar.gz"
|
||||||
|
mode: "0644"
|
||||||
|
|
||||||
|
- name: Extract Vaultwarden binary
|
||||||
|
unarchive:
|
||||||
|
src: "/tmp/vaultwarden-{{ vw_version }}.tar.gz"
|
||||||
|
dest: /opt/vaultwarden/
|
||||||
|
remote_src: true
|
||||||
|
owner: vaultwarden
|
||||||
|
group: vaultwarden
|
||||||
|
|
||||||
|
- name: Ensure binary is executable
|
||||||
|
file:
|
||||||
|
path: /opt/vaultwarden/vaultwarden
|
||||||
|
mode: "0755"
|
||||||
|
owner: vaultwarden
|
||||||
|
group: vaultwarden
|
||||||
|
|
||||||
|
- name: Download Vaultwarden web vault
|
||||||
|
get_url:
|
||||||
|
url: "https://github.com/dani-garcia/bw_web_builds/releases/latest/download/bw_web_v2024.6.2c.tar.gz"
|
||||||
|
dest: /tmp/bw_web_vault.tar.gz
|
||||||
|
mode: "0644"
|
||||||
|
failed_when: false
|
||||||
|
|
||||||
|
- name: Extract web vault
|
||||||
|
unarchive:
|
||||||
|
src: /tmp/bw_web_vault.tar.gz
|
||||||
|
dest: /opt/vaultwarden/
|
||||||
|
remote_src: true
|
||||||
|
owner: vaultwarden
|
||||||
|
group: vaultwarden
|
||||||
|
failed_when: false
|
||||||
|
|
||||||
|
- name: Allow HTTP/HTTPS through UFW
|
||||||
|
ufw:
|
||||||
|
rule: allow
|
||||||
|
port: "{{ item }}"
|
||||||
|
proto: tcp
|
||||||
|
loop:
|
||||||
|
- "80"
|
||||||
|
- "443"
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
---
|
||||||
|
# Nginx reverse proxy for Vaultwarden
|
||||||
|
|
||||||
|
- name: Create certbot webroot
|
||||||
|
file:
|
||||||
|
path: /var/www/certbot
|
||||||
|
state: directory
|
||||||
|
owner: www-data
|
||||||
|
group: www-data
|
||||||
|
mode: "0755"
|
||||||
|
|
||||||
|
- name: Deploy HTTP-only nginx config (for certbot)
|
||||||
|
copy:
|
||||||
|
dest: /etc/nginx/sites-available/vaultwarden
|
||||||
|
content: |
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name {{ vaultwarden_domain }};
|
||||||
|
location /.well-known/acme-challenge/ { root /var/www/certbot; }
|
||||||
|
location / { return 200 'phantom setup in progress'; add_header Content-Type text/plain; }
|
||||||
|
}
|
||||||
|
mode: "0644"
|
||||||
|
notify: reload nginx
|
||||||
|
|
||||||
|
- name: Enable Vaultwarden nginx site
|
||||||
|
file:
|
||||||
|
src: /etc/nginx/sites-available/vaultwarden
|
||||||
|
dest: /etc/nginx/sites-enabled/vaultwarden
|
||||||
|
state: link
|
||||||
|
notify: reload nginx
|
||||||
|
|
||||||
|
- name: Flush handlers to apply HTTP config
|
||||||
|
meta: flush_handlers
|
||||||
|
|
||||||
|
- name: Setup TLS (certbot with self-signed fallback)
|
||||||
|
include_tasks: ../../common/tls_setup.yml
|
||||||
|
vars:
|
||||||
|
_tls_domain: "{{ vaultwarden_domain }}"
|
||||||
|
|
||||||
|
- name: Deploy SSL nginx config
|
||||||
|
copy:
|
||||||
|
dest: /etc/nginx/sites-available/vaultwarden
|
||||||
|
content: |
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name {{ vaultwarden_domain }};
|
||||||
|
location /.well-known/acme-challenge/ { root /var/www/certbot; }
|
||||||
|
location / { return 301 https://$host$request_uri; }
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 443 ssl http2;
|
||||||
|
server_name {{ vaultwarden_domain }};
|
||||||
|
|
||||||
|
ssl_certificate {{ _ssl_cert }};
|
||||||
|
ssl_certificate_key {{ _ssl_key }};
|
||||||
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
|
ssl_ciphers HIGH:!aNULL:!MD5;
|
||||||
|
ssl_prefer_server_ciphers on;
|
||||||
|
|
||||||
|
server_tokens off;
|
||||||
|
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
|
||||||
|
add_header X-Content-Type-Options nosniff always;
|
||||||
|
add_header X-Frame-Options DENY always;
|
||||||
|
|
||||||
|
client_max_body_size 525m;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://127.0.0.1:8081;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /notifications/hub {
|
||||||
|
proxy_pass http://127.0.0.1:3012;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
}
|
||||||
|
|
||||||
|
location /notifications/hub/negotiate {
|
||||||
|
proxy_pass http://127.0.0.1:8081;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mode: "0644"
|
||||||
|
notify: reload nginx
|
||||||
@@ -7,10 +7,11 @@
|
|||||||
connection: local
|
connection: local
|
||||||
gather_facts: false
|
gather_facts: false
|
||||||
vars:
|
vars:
|
||||||
|
deploy_id: "{{ deployment_id }}"
|
||||||
aws_region: "{{ region | default('us-east-1') }}"
|
aws_region: "{{ region | default('us-east-1') }}"
|
||||||
ec2_instance_type: "{{ instance_type | default('t3.micro') }}"
|
ec2_instance_type: "{{ instance_type | default('t3.micro') }}"
|
||||||
ec2_ami: "{{ ami | default('') }}"
|
ec2_ami: "{{ ami | default('') }}"
|
||||||
ec2_key_name: "phantom-{{ deploy_id }}"
|
ec2_key_name: "ph-{{ deployment_id }}"
|
||||||
ssh_key_path: "{{ ssh_key }}.pub"
|
ssh_key_path: "{{ ssh_key }}.pub"
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
@@ -48,7 +49,7 @@
|
|||||||
|
|
||||||
- name: Create security group
|
- name: Create security group
|
||||||
amazon.aws.ec2_security_group:
|
amazon.aws.ec2_security_group:
|
||||||
name: "phantom-{{ deploy_id }}"
|
name: "ph-{{ deploy_id }}"
|
||||||
description: "phantom deployment {{ deploy_id }}"
|
description: "phantom deployment {{ deploy_id }}"
|
||||||
region: "{{ aws_region }}"
|
region: "{{ aws_region }}"
|
||||||
rules:
|
rules:
|
||||||
@@ -68,7 +69,7 @@
|
|||||||
|
|
||||||
- name: Launch EC2 instance
|
- name: Launch EC2 instance
|
||||||
amazon.aws.ec2_instance:
|
amazon.aws.ec2_instance:
|
||||||
name: "phantom-{{ deploy_id }}"
|
name: "ph-{{ deploy_id }}"
|
||||||
key_name: "{{ ec2_key_name }}"
|
key_name: "{{ ec2_key_name }}"
|
||||||
instance_type: "{{ ec2_instance_type }}"
|
instance_type: "{{ ec2_instance_type }}"
|
||||||
image_id: "{{ ec2_ami }}"
|
image_id: "{{ ec2_ami }}"
|
||||||
@@ -97,6 +98,12 @@
|
|||||||
dest: "{{ _host_output_file }}"
|
dest: "{{ _host_output_file }}"
|
||||||
when: _host_output_file is defined
|
when: _host_output_file is defined
|
||||||
|
|
||||||
|
- name: Write instance ID for teardown
|
||||||
|
copy:
|
||||||
|
content: "{{ ec2_result.instances[0].instance_id }}"
|
||||||
|
dest: "{{ _host_output_file | dirname }}/instance_id"
|
||||||
|
when: _host_output_file is defined
|
||||||
|
|
||||||
- name: Wait for SSH
|
- name: Wait for SSH
|
||||||
wait_for:
|
wait_for:
|
||||||
host: "{{ target_host }}"
|
host: "{{ target_host }}"
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
---
|
||||||
|
# Tear down a Phantom AWS EC2 instance
|
||||||
|
# Matches c2itall cleanup pattern
|
||||||
|
|
||||||
|
- name: Teardown Phantom EC2 Instance
|
||||||
|
hosts: localhost
|
||||||
|
connection: local
|
||||||
|
gather_facts: false
|
||||||
|
|
||||||
|
environment:
|
||||||
|
AWS_ACCESS_KEY_ID: "{{ aws_access_key }}"
|
||||||
|
AWS_SECRET_ACCESS_KEY: "{{ aws_secret_key }}"
|
||||||
|
AWS_DEFAULT_REGION: "{{ region | default('us-east-1') }}"
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
- name: Find instance by name tag
|
||||||
|
amazon.aws.ec2_instance_info:
|
||||||
|
filters:
|
||||||
|
"tag:Name": "{{ instance_label }}"
|
||||||
|
instance-state-name: ["running", "stopped", "pending"]
|
||||||
|
region: "{{ region | default('us-east-1') }}"
|
||||||
|
register: ec2_info
|
||||||
|
|
||||||
|
- name: Terminate instance
|
||||||
|
amazon.aws.ec2_instance:
|
||||||
|
instance_ids: "{{ ec2_info.instances | map(attribute='instance_id') | list }}"
|
||||||
|
state: absent
|
||||||
|
region: "{{ region | default('us-east-1') }}"
|
||||||
|
when: ec2_info.instances | length > 0
|
||||||
|
register: deletion
|
||||||
|
ignore_errors: true
|
||||||
|
|
||||||
|
- name: Delete security group
|
||||||
|
amazon.aws.ec2_security_group:
|
||||||
|
name: "{{ instance_label }}"
|
||||||
|
state: absent
|
||||||
|
region: "{{ region | default('us-east-1') }}"
|
||||||
|
ignore_errors: true
|
||||||
|
|
||||||
|
- name: Delete key pair
|
||||||
|
amazon.aws.ec2_key:
|
||||||
|
name: "{{ instance_label }}"
|
||||||
|
state: absent
|
||||||
|
region: "{{ region | default('us-east-1') }}"
|
||||||
|
ignore_errors: true
|
||||||
|
|
||||||
|
- name: Report teardown result
|
||||||
|
debug:
|
||||||
|
msg: "Instance {{ instance_label }}: {{ (ec2_info.instances | length > 0) | ternary('Terminated', 'Not found') }}"
|
||||||
@@ -7,11 +7,12 @@
|
|||||||
connection: local
|
connection: local
|
||||||
gather_facts: false
|
gather_facts: false
|
||||||
vars:
|
vars:
|
||||||
|
deploy_id: "{{ deployment_id }}"
|
||||||
linode_token: "{{ api_token }}"
|
linode_token: "{{ api_token }}"
|
||||||
linode_region: "{{ region | default('us-east') }}"
|
linode_region: "{{ region | default('us-east') }}"
|
||||||
linode_plan: "{{ plan | default('g6-nanode-1') }}"
|
linode_plan: "{{ plan | default('g6-nanode-1') }}"
|
||||||
linode_image: "{{ image | default('linode/debian12') }}"
|
linode_image: "{{ image | default('linode/debian12') }}"
|
||||||
linode_label: "phantom-{{ deploy_id }}"
|
linode_label: "ph-{{ deployment_id }}"
|
||||||
ssh_key_path: "{{ ssh_key }}.pub"
|
ssh_key_path: "{{ ssh_key }}.pub"
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
@@ -20,6 +21,10 @@
|
|||||||
src: "{{ ssh_key_path }}"
|
src: "{{ ssh_key_path }}"
|
||||||
register: ssh_pubkey
|
register: ssh_pubkey
|
||||||
|
|
||||||
|
- name: Generate root password (required by API, SSH key auth used instead)
|
||||||
|
command: openssl rand -base64 32
|
||||||
|
register: root_pass_gen
|
||||||
|
|
||||||
- name: Create Linode instance
|
- name: Create Linode instance
|
||||||
uri:
|
uri:
|
||||||
url: https://api.linode.com/v4/linode/instances
|
url: https://api.linode.com/v4/linode/instances
|
||||||
@@ -33,6 +38,7 @@
|
|||||||
region: "{{ linode_region }}"
|
region: "{{ linode_region }}"
|
||||||
image: "{{ linode_image }}"
|
image: "{{ linode_image }}"
|
||||||
label: "{{ linode_label }}"
|
label: "{{ linode_label }}"
|
||||||
|
root_pass: "{{ root_pass_gen.stdout }}"
|
||||||
authorized_keys:
|
authorized_keys:
|
||||||
- "{{ ssh_pubkey.content | b64decode | trim }}"
|
- "{{ ssh_pubkey.content | b64decode | trim }}"
|
||||||
booted: true
|
booted: true
|
||||||
@@ -58,6 +64,12 @@
|
|||||||
dest: "{{ _host_output_file }}"
|
dest: "{{ _host_output_file }}"
|
||||||
when: _host_output_file is defined
|
when: _host_output_file is defined
|
||||||
|
|
||||||
|
- name: Write instance ID for teardown
|
||||||
|
copy:
|
||||||
|
content: "{{ linode_result.json.id }}"
|
||||||
|
dest: "{{ _host_output_file | dirname }}/instance_id"
|
||||||
|
when: _host_output_file is defined
|
||||||
|
|
||||||
- name: Wait for SSH
|
- name: Wait for SSH
|
||||||
wait_for:
|
wait_for:
|
||||||
host: "{{ target_host }}"
|
host: "{{ target_host }}"
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
---
|
||||||
|
# Tear down a Phantom Linode instance by label using raw API calls
|
||||||
|
# No extra collections needed — uses uri module (same as provisioning)
|
||||||
|
|
||||||
|
- name: Teardown Phantom Linode Instance
|
||||||
|
hosts: localhost
|
||||||
|
connection: local
|
||||||
|
gather_facts: false
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
- name: Validate required variables
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- api_token is defined and api_token != ""
|
||||||
|
- instance_label is defined and instance_label != ""
|
||||||
|
fail_msg: "api_token and instance_label are required for teardown"
|
||||||
|
|
||||||
|
- name: List all Linode instances
|
||||||
|
uri:
|
||||||
|
url: https://api.linode.com/v4/linode/instances
|
||||||
|
method: GET
|
||||||
|
headers:
|
||||||
|
Authorization: "Bearer {{ api_token }}"
|
||||||
|
return_content: true
|
||||||
|
register: linode_list
|
||||||
|
|
||||||
|
- name: Find instance by label
|
||||||
|
set_fact:
|
||||||
|
target_instance: "{{ linode_list.json.data | selectattr('label', 'equalto', instance_label) | list | first | default(None) }}"
|
||||||
|
|
||||||
|
- name: Delete instance
|
||||||
|
uri:
|
||||||
|
url: "https://api.linode.com/v4/linode/instances/{{ target_instance.id }}"
|
||||||
|
method: DELETE
|
||||||
|
headers:
|
||||||
|
Authorization: "Bearer {{ api_token }}"
|
||||||
|
status_code: 200
|
||||||
|
when: target_instance is not none and target_instance != None
|
||||||
|
register: deletion
|
||||||
|
|
||||||
|
- name: Report result
|
||||||
|
debug:
|
||||||
|
msg: "{{ (target_instance is not none and target_instance != None) | ternary('Instance ' ~ instance_label ~ ' (ID ' ~ target_instance.id | default('?') ~ ') deleted', 'No instance found with label ' ~ instance_label) }}"
|
||||||
@@ -0,0 +1,617 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Phantom test runner — exercises all modules, playbooks, and deployment flows.
|
||||||
|
|
||||||
|
Runs as a simulated user through every service type without actual deployment.
|
||||||
|
Tests: imports, module configs, playbook syntax, deploy pipeline (dry run).
|
||||||
|
"""
|
||||||
|
|
||||||
|
import getpass
|
||||||
|
import importlib
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import unittest
|
||||||
|
from io import StringIO
|
||||||
|
from pathlib import Path
|
||||||
|
from unittest.mock import patch, MagicMock
|
||||||
|
|
||||||
|
# Setup path
|
||||||
|
sys.path.insert(0, os.path.dirname(__file__))
|
||||||
|
|
||||||
|
BASE_DIR = Path(__file__).resolve().parent
|
||||||
|
PLAYBOOKS_DIR = BASE_DIR / "playbooks"
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Colors ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
RESET = "\033[0m"
|
||||||
|
BOLD = "\033[1m"
|
||||||
|
GREEN = "\033[38;5;49m"
|
||||||
|
RED = "\033[38;5;196m"
|
||||||
|
CYAN = "\033[38;5;51m"
|
||||||
|
YELLOW = "\033[38;5;214m"
|
||||||
|
GREY = "\033[38;5;244m"
|
||||||
|
MAGENTA = "\033[38;5;201m"
|
||||||
|
|
||||||
|
|
||||||
|
def header(msg):
|
||||||
|
print(f"\n{CYAN}{'═' * 60}{RESET}")
|
||||||
|
print(f"{MAGENTA} {msg}{RESET}")
|
||||||
|
print(f"{CYAN}{'═' * 60}{RESET}")
|
||||||
|
|
||||||
|
|
||||||
|
def passed(msg):
|
||||||
|
print(f" {GREEN}✓{RESET} {msg}")
|
||||||
|
|
||||||
|
|
||||||
|
def failed(msg):
|
||||||
|
print(f" {RED}✗{RESET} {msg}")
|
||||||
|
|
||||||
|
|
||||||
|
def skipped(msg):
|
||||||
|
print(f" {YELLOW}○{RESET} {msg}")
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Test 1: Module Imports ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def test_imports():
|
||||||
|
header("Test 1: Module Imports")
|
||||||
|
modules = ["matrix", "vpn", "dns", "cloud", "vault", "media", "email", "all_in_one"]
|
||||||
|
results = {"pass": 0, "fail": 0}
|
||||||
|
|
||||||
|
for mod_name in modules:
|
||||||
|
try:
|
||||||
|
mod = importlib.import_module(f"modules.{mod_name}")
|
||||||
|
if hasattr(mod, "gather_config"):
|
||||||
|
passed(f"modules.{mod_name} — imported, gather_config() present")
|
||||||
|
else:
|
||||||
|
failed(f"modules.{mod_name} — imported but no gather_config()")
|
||||||
|
results["fail"] += 1
|
||||||
|
continue
|
||||||
|
results["pass"] += 1
|
||||||
|
except Exception as e:
|
||||||
|
failed(f"modules.{mod_name} — import failed: {e}")
|
||||||
|
results["fail"] += 1
|
||||||
|
|
||||||
|
# Import phantom itself
|
||||||
|
try:
|
||||||
|
import phantom
|
||||||
|
for fn in ["generate_id", "instance_label", "generate_ssh_key",
|
||||||
|
"run_playbook", "deploy", "save_deploy_info", "main_menu",
|
||||||
|
"manage_menu", "_migrate_ssh_keys", "_archive_deployment"]:
|
||||||
|
assert hasattr(phantom, fn), f"missing {fn}"
|
||||||
|
passed(f"phantom.py — imported, all core functions present")
|
||||||
|
results["pass"] += 1
|
||||||
|
except Exception as e:
|
||||||
|
failed(f"phantom.py — import failed: {e}")
|
||||||
|
results["fail"] += 1
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Test 2: Deployment ID Generation ────────────────────────────────────────
|
||||||
|
|
||||||
|
def test_id_generation():
|
||||||
|
header("Test 2: Deployment ID Generation")
|
||||||
|
import phantom
|
||||||
|
results = {"pass": 0, "fail": 0}
|
||||||
|
|
||||||
|
ids = set()
|
||||||
|
for _ in range(100):
|
||||||
|
deployment_id = phantom.generate_id()
|
||||||
|
ids.add(deployment_id)
|
||||||
|
# verb+animal format: all lowercase, no dashes, no digits
|
||||||
|
if not deployment_id.isalpha() or not deployment_id.islower():
|
||||||
|
failed(f"Bad ID format: {deployment_id} (expected verbanimal, e.g. blazingwolf)")
|
||||||
|
results["fail"] += 1
|
||||||
|
return results
|
||||||
|
|
||||||
|
if len(ids) >= 50: # verb+animal combos have ~840 possibilities
|
||||||
|
passed(f"Generated 100 IDs, {len(ids)} unique — good entropy")
|
||||||
|
results["pass"] += 1
|
||||||
|
else:
|
||||||
|
failed(f"Only {len(ids)}/100 unique IDs — poor entropy")
|
||||||
|
results["fail"] += 1
|
||||||
|
|
||||||
|
# Verify instance_label format
|
||||||
|
sample_id = phantom.generate_id()
|
||||||
|
label = phantom.instance_label(sample_id)
|
||||||
|
if label == f"ph-{sample_id}":
|
||||||
|
passed(f"Instance label format correct: {label}")
|
||||||
|
results["pass"] += 1
|
||||||
|
else:
|
||||||
|
failed(f"Bad label: {label} (expected ph-{sample_id})")
|
||||||
|
results["fail"] += 1
|
||||||
|
|
||||||
|
# Sample output
|
||||||
|
sample = list(ids)[:5]
|
||||||
|
for s in sample:
|
||||||
|
print(f" {GREY}sample: {s} → ph-{s}{RESET}")
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Test 3: Ansible Playbook Syntax ─────────────────────────────────────────
|
||||||
|
|
||||||
|
def test_playbook_syntax():
|
||||||
|
header("Test 3: Ansible Playbook Syntax Check")
|
||||||
|
results = {"pass": 0, "fail": 0}
|
||||||
|
|
||||||
|
playbooks = [
|
||||||
|
PLAYBOOKS_DIR / "common/base_hardening.yml",
|
||||||
|
PLAYBOOKS_DIR / "matrix/main.yml",
|
||||||
|
PLAYBOOKS_DIR / "vpn/main.yml",
|
||||||
|
PLAYBOOKS_DIR / "dns/main.yml",
|
||||||
|
PLAYBOOKS_DIR / "cloud/main.yml",
|
||||||
|
PLAYBOOKS_DIR / "vault/main.yml",
|
||||||
|
PLAYBOOKS_DIR / "media/main.yml",
|
||||||
|
PLAYBOOKS_DIR / "email/main.yml",
|
||||||
|
PLAYBOOKS_DIR / "all_in_one/main.yml",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Also check provider playbooks
|
||||||
|
for p in (BASE_DIR / "providers").glob("*.yml"):
|
||||||
|
playbooks.append(p)
|
||||||
|
|
||||||
|
for pb in playbooks:
|
||||||
|
rel = pb.relative_to(BASE_DIR)
|
||||||
|
if not pb.exists():
|
||||||
|
failed(f"{rel} — file missing")
|
||||||
|
results["fail"] += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
result = subprocess.run(
|
||||||
|
["ansible-playbook", "--syntax-check", str(pb),
|
||||||
|
"-i", "localhost,", "--connection", "local"],
|
||||||
|
capture_output=True, text=True
|
||||||
|
)
|
||||||
|
if result.returncode == 0:
|
||||||
|
passed(f"{rel}")
|
||||||
|
results["pass"] += 1
|
||||||
|
else:
|
||||||
|
# Show first line of error
|
||||||
|
err_line = result.stderr.strip().split("\n")[0] if result.stderr else result.stdout.strip().split("\n")[0]
|
||||||
|
failed(f"{rel} — {err_line}")
|
||||||
|
results["fail"] += 1
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Test 4: Module gather_config Flows ──────────────────────────────────────
|
||||||
|
|
||||||
|
def test_gather_config():
|
||||||
|
header("Test 4: Module gather_config() — Simulated User Input")
|
||||||
|
results = {"pass": 0, "fail": 0}
|
||||||
|
|
||||||
|
# Input sequences for each module (what a user would type)
|
||||||
|
test_inputs = {
|
||||||
|
"matrix": [
|
||||||
|
"matrix.test.local", # domain
|
||||||
|
"admin", # admin user
|
||||||
|
"testpass123", # admin password (getpass)
|
||||||
|
"n", # registration
|
||||||
|
"y", # element web
|
||||||
|
],
|
||||||
|
"vpn": [
|
||||||
|
"", # port (default 51820)
|
||||||
|
"3", # client configs
|
||||||
|
"", # dns (default)
|
||||||
|
"", # allowed IPs (default)
|
||||||
|
"", # subnet (default)
|
||||||
|
],
|
||||||
|
"dns": [
|
||||||
|
"1", # upstream DNS (Quad9)
|
||||||
|
"", # admin domain (optional)
|
||||||
|
"1", # blocklist (standard)
|
||||||
|
],
|
||||||
|
"cloud": [
|
||||||
|
"cloud.test.local", # domain
|
||||||
|
"admin", # admin user
|
||||||
|
"10", # storage GB
|
||||||
|
],
|
||||||
|
"vault": [
|
||||||
|
"vault.test.local", # domain
|
||||||
|
"", # admin token (auto-generate)
|
||||||
|
"n", # signups
|
||||||
|
],
|
||||||
|
"media": [
|
||||||
|
"media.test.local", # domain
|
||||||
|
"/srv/media", # library path
|
||||||
|
],
|
||||||
|
"email": [
|
||||||
|
"mail.test.local", # domain
|
||||||
|
"admin@test.local", # first user
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
for mod_name, inputs in test_inputs.items():
|
||||||
|
try:
|
||||||
|
mod = importlib.import_module(f"modules.{mod_name}")
|
||||||
|
config = {"deployment_id": f"test-{mod_name}-01"}
|
||||||
|
|
||||||
|
# Mock input() and getpass.getpass() to feed our test inputs
|
||||||
|
input_iter = iter(inputs)
|
||||||
|
|
||||||
|
def mock_input(prompt=""):
|
||||||
|
try:
|
||||||
|
val = next(input_iter)
|
||||||
|
print(f" {GREY}→ {prompt.strip()[:50]} {CYAN}{val or '(default)'}{RESET}")
|
||||||
|
return val
|
||||||
|
except StopIteration:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def mock_getpass(prompt=""):
|
||||||
|
return mock_input(prompt)
|
||||||
|
|
||||||
|
with patch("builtins.input", mock_input), \
|
||||||
|
patch("getpass.getpass", mock_getpass):
|
||||||
|
# Some modules import getpass at module level
|
||||||
|
if hasattr(mod, "getpass"):
|
||||||
|
with patch.object(mod, "getpass", MagicMock(getpass=mock_getpass)):
|
||||||
|
result = mod.gather_config(config)
|
||||||
|
else:
|
||||||
|
# For modules that use getpass.getpass directly
|
||||||
|
import modules
|
||||||
|
result = mod.gather_config(config)
|
||||||
|
|
||||||
|
if result is not None:
|
||||||
|
keys = [k for k in result.keys() if k != "deployment_id"]
|
||||||
|
passed(f"{mod_name} — config collected: {', '.join(keys[:6])}")
|
||||||
|
results["pass"] += 1
|
||||||
|
else:
|
||||||
|
failed(f"{mod_name} — gather_config returned None")
|
||||||
|
results["fail"] += 1
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
failed(f"{mod_name} — {type(e).__name__}: {e}")
|
||||||
|
results["fail"] += 1
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Test 5: Deploy Pipeline (Dry Run) ──────────────────────────────────────
|
||||||
|
|
||||||
|
def test_deploy_pipeline():
|
||||||
|
header("Test 5: Deploy Pipeline — Dry Run (Cancel Before Execute)")
|
||||||
|
import phantom
|
||||||
|
results = {"pass": 0, "fail": 0}
|
||||||
|
|
||||||
|
services = ["matrix", "vpn", "dns", "cloud", "vault", "media", "email"]
|
||||||
|
|
||||||
|
for svc in services:
|
||||||
|
config = {
|
||||||
|
"deployment_id": f"dryrun-{svc}-99",
|
||||||
|
"provider": "local",
|
||||||
|
"target_host": "localhost",
|
||||||
|
"ssh_user": os.getenv("USER", "root"),
|
||||||
|
"domain": f"{svc}.test.local",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Capture deploy() output — answer "n" to the deploy confirmation
|
||||||
|
def mock_input(prompt=""):
|
||||||
|
return "n"
|
||||||
|
|
||||||
|
try:
|
||||||
|
with patch("builtins.input", mock_input), \
|
||||||
|
patch("sys.stdout", new_callable=StringIO) as mock_stdout:
|
||||||
|
phantom.deploy(svc, config)
|
||||||
|
|
||||||
|
output = mock_stdout.getvalue()
|
||||||
|
if "Deployment cancelled" in output and config["deployment_id"] in output:
|
||||||
|
passed(f"{svc} — summary displayed, deploy cancelled correctly")
|
||||||
|
results["pass"] += 1
|
||||||
|
else:
|
||||||
|
failed(f"{svc} — unexpected output")
|
||||||
|
results["fail"] += 1
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
failed(f"{svc} — {type(e).__name__}: {e}")
|
||||||
|
results["fail"] += 1
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Test 6: SSH Key Generation ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
def test_ssh_keygen():
|
||||||
|
header("Test 6: SSH Key Generation (new ~/.ssh/ path)")
|
||||||
|
import phantom
|
||||||
|
results = {"pass": 0, "fail": 0}
|
||||||
|
|
||||||
|
test_id = "testkeygen00"
|
||||||
|
try:
|
||||||
|
key_path = phantom.generate_ssh_key(test_id)
|
||||||
|
key_file = Path(key_path)
|
||||||
|
pub_file = Path(f"{key_path}.pub")
|
||||||
|
|
||||||
|
# Verify key is at ~/.ssh/c2deploy_ph-{id}
|
||||||
|
expected_path = Path.home() / ".ssh" / f"c2deploy_ph-{test_id}"
|
||||||
|
if key_file == expected_path:
|
||||||
|
passed(f"Key at correct path: ~/.ssh/c2deploy_ph-{test_id}")
|
||||||
|
results["pass"] += 1
|
||||||
|
else:
|
||||||
|
failed(f"Key at wrong path: {key_file} (expected {expected_path})")
|
||||||
|
results["fail"] += 1
|
||||||
|
|
||||||
|
if key_file.exists() and pub_file.exists():
|
||||||
|
# Check permissions
|
||||||
|
mode = oct(key_file.stat().st_mode)[-3:]
|
||||||
|
if mode == "600":
|
||||||
|
passed(f"Key generated: {key_file.name} (mode 0600)")
|
||||||
|
results["pass"] += 1
|
||||||
|
else:
|
||||||
|
failed(f"Key permissions wrong: {mode} (expected 600)")
|
||||||
|
results["fail"] += 1
|
||||||
|
|
||||||
|
# Verify key format
|
||||||
|
pub_content = pub_file.read_text()
|
||||||
|
if pub_content.startswith("ssh-rsa"):
|
||||||
|
passed(f"RSA 4096 public key valid")
|
||||||
|
results["pass"] += 1
|
||||||
|
else:
|
||||||
|
failed(f"Unexpected key format")
|
||||||
|
results["fail"] += 1
|
||||||
|
else:
|
||||||
|
failed("Key files not created")
|
||||||
|
results["fail"] += 1
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
failed(f"Key generation failed: {e}")
|
||||||
|
results["fail"] += 1
|
||||||
|
finally:
|
||||||
|
# Cleanup — remove from ~/.ssh/
|
||||||
|
for suffix in ["", ".pub"]:
|
||||||
|
f = Path.home() / ".ssh" / f"c2deploy_ph-{test_id}{suffix}"
|
||||||
|
if f.exists():
|
||||||
|
f.unlink()
|
||||||
|
kh = Path.home() / ".ssh" / f"c2deploy_ph-{test_id}_known_hosts"
|
||||||
|
if kh.exists():
|
||||||
|
kh.unlink()
|
||||||
|
print(f" {GREY}cleaned up test keys{RESET}")
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Test 7: Full Interactive Flow Simulation ────────────────────────────────
|
||||||
|
|
||||||
|
def test_interactive_flow():
|
||||||
|
header("Test 7: Full Menu Flow — Matrix Deploy (Simulated)")
|
||||||
|
import phantom
|
||||||
|
results = {"pass": 0, "fail": 0}
|
||||||
|
|
||||||
|
# Simulate: select Matrix (1), Local provider (5), confirm local (y),
|
||||||
|
# fill config, cancel deploy (n), exit (0)
|
||||||
|
inputs = iter([
|
||||||
|
"1", # Menu: Matrix
|
||||||
|
"5", # Provider: Local
|
||||||
|
"y", # Confirm local
|
||||||
|
"matrix.test.local", # Domain
|
||||||
|
"admin", # Admin user
|
||||||
|
"testpass123", # Admin password
|
||||||
|
"n", # Registration
|
||||||
|
"y", # Element web
|
||||||
|
"n", # Deploy? No
|
||||||
|
"0", # Exit menu
|
||||||
|
])
|
||||||
|
|
||||||
|
def mock_input(prompt=""):
|
||||||
|
try:
|
||||||
|
val = next(inputs)
|
||||||
|
short_prompt = prompt.strip()[:60].replace("\033[38;5;51m", "").replace("\033[0m", "").replace("\033[38;5;201m", "").replace("\033[38;5;255m", "").replace("\033[38;5;214m", "")
|
||||||
|
print(f" {GREY}→ {short_prompt} {CYAN}{val}{RESET}")
|
||||||
|
return val
|
||||||
|
except StopIteration:
|
||||||
|
return "0"
|
||||||
|
|
||||||
|
def mock_getpass(prompt=""):
|
||||||
|
return mock_input(prompt)
|
||||||
|
|
||||||
|
try:
|
||||||
|
with patch("builtins.input", mock_input), \
|
||||||
|
patch("getpass.getpass", mock_getpass), \
|
||||||
|
patch("sys.stdout", new_callable=StringIO):
|
||||||
|
phantom.main_menu()
|
||||||
|
|
||||||
|
passed("Full interactive flow completed without errors")
|
||||||
|
results["pass"] += 1
|
||||||
|
except SystemExit:
|
||||||
|
passed("Full interactive flow completed (sys.exit)")
|
||||||
|
results["pass"] += 1
|
||||||
|
except StopIteration:
|
||||||
|
passed("Full interactive flow completed (all inputs consumed)")
|
||||||
|
results["pass"] += 1
|
||||||
|
except Exception as e:
|
||||||
|
failed(f"Interactive flow failed: {type(e).__name__}: {e}")
|
||||||
|
results["fail"] += 1
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Test 8: Deployment Discovery (manage_menu) ─────────────────────────────
|
||||||
|
|
||||||
|
def test_deployment_discovery():
|
||||||
|
header("Test 8: Deployment Discovery")
|
||||||
|
import phantom
|
||||||
|
results = {"pass": 0, "fail": 0}
|
||||||
|
|
||||||
|
# Create a temporary deployment info file
|
||||||
|
test_id = "testdiscovery01"
|
||||||
|
info_file = phantom.PHANTOM_LOGS / f"deployment_info_{test_id}.txt"
|
||||||
|
try:
|
||||||
|
with open(info_file, "w") as f:
|
||||||
|
f.write("Deployment Information\n")
|
||||||
|
f.write("=" * 50 + "\n")
|
||||||
|
f.write(f"Deployment ID: {test_id}\n")
|
||||||
|
f.write("Provider: linode\n")
|
||||||
|
f.write("Deployment Type: phantom_matrix\n")
|
||||||
|
f.write("\nConfiguration:\n")
|
||||||
|
f.write("-" * 30 + "\n")
|
||||||
|
f.write("domain: test.example.com\n")
|
||||||
|
f.write("\nAccess Information:\n")
|
||||||
|
f.write("-" * 30 + "\n")
|
||||||
|
f.write("Instance IP: 10.0.0.99\n")
|
||||||
|
|
||||||
|
# Test parsing
|
||||||
|
data = phantom._parse_deployment_info(info_file)
|
||||||
|
checks = [
|
||||||
|
("deployment_id", test_id),
|
||||||
|
("deployment_type", "phantom_matrix"),
|
||||||
|
("ip", "10.0.0.99"),
|
||||||
|
("domain", "test.example.com"),
|
||||||
|
]
|
||||||
|
for field, expected in checks:
|
||||||
|
if data.get(field) == expected:
|
||||||
|
passed(f"Parsed {field}: {expected}")
|
||||||
|
results["pass"] += 1
|
||||||
|
else:
|
||||||
|
failed(f"Parse {field}: got '{data.get(field)}', expected '{expected}'")
|
||||||
|
results["fail"] += 1
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
failed(f"Discovery test failed: {e}")
|
||||||
|
results["fail"] += 1
|
||||||
|
finally:
|
||||||
|
if info_file.exists():
|
||||||
|
info_file.unlink()
|
||||||
|
print(f" {GREY}cleaned up test info file{RESET}")
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Test 9: Mode-Aware Log Paths ───────────────────────────────────────────
|
||||||
|
|
||||||
|
def test_mode_aware_paths():
|
||||||
|
header("Test 9: Mode-Aware Log Paths")
|
||||||
|
import phantom
|
||||||
|
results = {"pass": 0, "fail": 0}
|
||||||
|
|
||||||
|
c2_root = Path.home() / "tools" / "c2itall"
|
||||||
|
c2_integrated = os.environ.get("C2ITALL_INTEGRATED") == "1"
|
||||||
|
|
||||||
|
if c2_integrated:
|
||||||
|
expected_logs = c2_root / "logs"
|
||||||
|
mode_desc = "c2itall integrated"
|
||||||
|
else:
|
||||||
|
expected_logs = phantom.BASE_DIR / "logs"
|
||||||
|
mode_desc = "standalone"
|
||||||
|
|
||||||
|
if phantom.PHANTOM_LOGS == expected_logs:
|
||||||
|
passed(f"PHANTOM_LOGS correct for {mode_desc} mode: {expected_logs}")
|
||||||
|
results["pass"] += 1
|
||||||
|
else:
|
||||||
|
failed(f"PHANTOM_LOGS wrong: {phantom.PHANTOM_LOGS} (expected {expected_logs})")
|
||||||
|
results["fail"] += 1
|
||||||
|
|
||||||
|
# WORK_DIR should always be local
|
||||||
|
if phantom.WORK_DIR == phantom.BASE_DIR / "logs":
|
||||||
|
passed(f"WORK_DIR correct: {phantom.WORK_DIR}")
|
||||||
|
results["pass"] += 1
|
||||||
|
else:
|
||||||
|
failed(f"WORK_DIR wrong: {phantom.WORK_DIR}")
|
||||||
|
results["fail"] += 1
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Test 10: Config Key Rename ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
def test_config_key_rename():
|
||||||
|
header("Test 10: Config Key Rename (deployment_id)")
|
||||||
|
import phantom
|
||||||
|
results = {"pass": 0, "fail": 0}
|
||||||
|
|
||||||
|
# deploy() should use deployment_id, not deploy_id
|
||||||
|
config = {
|
||||||
|
"deployment_id": "testrename01",
|
||||||
|
"provider": "local",
|
||||||
|
"target_host": "localhost",
|
||||||
|
"ssh_user": os.getenv("USER", "root"),
|
||||||
|
}
|
||||||
|
|
||||||
|
def mock_input(prompt=""):
|
||||||
|
return "n"
|
||||||
|
|
||||||
|
try:
|
||||||
|
with patch("builtins.input", mock_input), \
|
||||||
|
patch("sys.stdout", new_callable=StringIO) as mock_stdout:
|
||||||
|
phantom.deploy("vpn", config)
|
||||||
|
|
||||||
|
output = mock_stdout.getvalue()
|
||||||
|
if "testrename01" in output:
|
||||||
|
passed("deploy() accepts deployment_id key")
|
||||||
|
results["pass"] += 1
|
||||||
|
else:
|
||||||
|
failed("deploy() didn't use deployment_id")
|
||||||
|
results["fail"] += 1
|
||||||
|
|
||||||
|
except KeyError as e:
|
||||||
|
if "deploy_id" in str(e):
|
||||||
|
failed(f"deploy() still looking for old 'deploy_id' key: {e}")
|
||||||
|
else:
|
||||||
|
failed(f"Unexpected KeyError: {e}")
|
||||||
|
results["fail"] += 1
|
||||||
|
except Exception as e:
|
||||||
|
failed(f"Config key rename test failed: {e}")
|
||||||
|
results["fail"] += 1
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Main ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print(f"""
|
||||||
|
{MAGENTA} ██████╗ ██╗ ██╗ █████╗ ███╗ ██╗████████╗ ██████╗ ███╗ ███╗
|
||||||
|
██╔══██╗██║ ██║██╔══██╗████╗ ██║╚══██╔══╝██╔═══██╗████╗ ████║
|
||||||
|
██████╔╝███████║███████║██╔██╗ ██║ ██║ ██║ ██║██╔████╔██║
|
||||||
|
██╔═══╝ ██╔══██║██╔══██║██║╚██╗██║ ██║ ██║ ██║██║╚██╔╝██║
|
||||||
|
██║ ██║ ██║██║ ██║██║ ╚████║ ██║ ╚██████╔╝██║ ╚═╝ ██║
|
||||||
|
╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝{RESET}
|
||||||
|
{CYAN} Test Suite{RESET}
|
||||||
|
""")
|
||||||
|
|
||||||
|
all_results = {"pass": 0, "fail": 0}
|
||||||
|
|
||||||
|
tests = [
|
||||||
|
test_imports,
|
||||||
|
test_id_generation,
|
||||||
|
test_playbook_syntax,
|
||||||
|
test_gather_config,
|
||||||
|
test_deploy_pipeline,
|
||||||
|
test_ssh_keygen,
|
||||||
|
test_interactive_flow,
|
||||||
|
test_deployment_discovery,
|
||||||
|
test_mode_aware_paths,
|
||||||
|
test_config_key_rename,
|
||||||
|
]
|
||||||
|
|
||||||
|
for test_fn in tests:
|
||||||
|
try:
|
||||||
|
r = test_fn()
|
||||||
|
all_results["pass"] += r["pass"]
|
||||||
|
all_results["fail"] += r["fail"]
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\n {RED}✗ {test_fn.__name__} crashed: {e}{RESET}")
|
||||||
|
all_results["fail"] += 1
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
total = all_results["pass"] + all_results["fail"]
|
||||||
|
header("Results")
|
||||||
|
print(f" {GREEN}Passed: {all_results['pass']}{RESET}")
|
||||||
|
if all_results["fail"]:
|
||||||
|
print(f" {RED}Failed: {all_results['fail']}{RESET}")
|
||||||
|
else:
|
||||||
|
print(f" {GREY}Failed: 0{RESET}")
|
||||||
|
print(f" {CYAN}Total: {total}{RESET}")
|
||||||
|
|
||||||
|
if all_results["fail"] == 0:
|
||||||
|
print(f"\n {GREEN}{BOLD}ALL TESTS PASSED{RESET}\n")
|
||||||
|
else:
|
||||||
|
print(f"\n {RED}{BOLD}{all_results['fail']} TEST(S) FAILED{RESET}\n")
|
||||||
|
|
||||||
|
return 0 if all_results["fail"] == 0 else 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
Reference in New Issue
Block a user