feat: unified launcher, multi-OS hardening, login alerts & auto-updates

Restructure around a single entry point (automations.sh) with a Gum wizard and
a self-extracting bundle for repo-less installs. Add scripts/oslib.sh so the
provisioning scripts (setup-host, harden-ssh, harden-jumphost, sshuser) run on
Alpine/Debian/Alma; seed root keys from globals/.

- ntfy SSH-login alerts (user, source IP, key, region, jump target) via pam_exec
- daily auto-updates with AUTO_REBOOT=idle (reboots only when no SSH active) and
  opt-in Alpine stable-branch upgrades
- generic + per-deployment cloud-init; Gitea release workflow on tag
- README/LICENSE/.gitignore/.gitattributes (force LF); repo URLs -> Gitea
This commit is contained in:
2026-06-12 14:56:02 -05:00
parent 85eeb79971
commit 7faa9098de
58 changed files with 6225 additions and 284 deletions
+250
View File
@@ -0,0 +1,250 @@
# SimpleX Relay Server Deployment
Complete automated deployment for SimpleX Chat relay servers on Alpine Linux with post-quantum SSH hardening, Tor hidden services, and encrypted backups.
## Quick Start
### 1. Clone This Repository
The installer fetches its scripts straight from this monorepo's layout:
```
automations/
├── scripts/
│ └── harden-ssh.sh # generic SSH hardening (PQ KEX + Ed25519)
├── deployments/simplex/
│ ├── deploy-simplex.sh # SMP + XFTP + Tor deployment
│ ├── backup.sh # age-encrypted backup creation
│ ├── restore.sh # disaster recovery from backup
│ ├── install-simplex.sh # master installer (cloud-init compatible)
│ ├── cloud-init.yml # example cloud-init configuration
│ └── README.md # this file
└── globals/
└── age-pubkey.txt # your age public key(s) for backups
```
Point `REPO_URL` at your fork of this repo. The installer resolves
`harden-ssh.sh` under `scripts/` and the simplex scripts under
`deployments/simplex/` automatically (overridable via `HARDEN_PATH` /
`SIMPLEX_PATH`).
### 2. Generate Backup Keys
```bash
# Generate age keypair for backups
age-keygen -o backup-private-key.txt
# Save the public key to the shared globals/ folder
echo "age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p" > globals/age-pubkey.txt
# Store the private key securely (NOT in your repo)
cp backup-private-key.txt ~/safe-location/
```
### 3. Deploy via Cloud-Init
Create a cloud instance with this user-data:
```yaml
#cloud-config
runcmd:
- |
curl -fsSL https://git.anomalous.dev/57_Wolve/automations/raw/branch/main/deployments/simplex/install-simplex.sh | \
REPO_URL=https://git.anomalous.dev/57_Wolve/automations.git \
DOMAIN=relay.example.com \
ACME_EMAIL=admin@example.com \
XFTP_QUOTA=100gb \
SSH_PORT=2222 \
ALLOWED_IP=1.2.3.4 \
bash
```
### 4. Manual Deployment
```bash
# On a fresh Alpine Linux host:
curl -fsSL https://git.anomalous.dev/57_Wolve/automations/raw/branch/main/deployments/simplex/install-simplex.sh | \
REPO_URL=https://git.anomalous.dev/57_Wolve/automations.git \
DOMAIN=relay.example.com \
ACME_EMAIL=admin@example.com \
XFTP_QUOTA=50gb \
SSH_PORT=2222 \
bash
```
## Configuration Options
| Variable | Required | Default | Description |
|----------|----------|---------|-------------|
| `REPO_URL` | ✅ | | Git repository URL containing deployment scripts |
| `DOMAIN` | ✅ | | Apex domain (creates smp.DOMAIN, xftp.DOMAIN) |
| `ACME_EMAIL` | ✅ | | Email for Let's Encrypt registration |
| `XFTP_QUOTA` | | 50gb | Disk quota for file storage |
| `SSH_PORT` | | 22 | SSH port (recommend changing from default) |
| `ALLOWED_IP` | | | Your IP to whitelist in sshguard |
| `KEY_TYPE` | | rsa4096 | Caddy TLS key type (rsa4096, p384, ed25519) |
| `SMP_PASS` | | | Optional password for SMP queue creation |
| `XFTP_PASS` | | | Optional password for XFTP uploads |
| `SKIP_PROMPTS` | | 0 | Set to 1 for non-interactive installation |
## What Gets Deployed
### Security Features
- **Post-quantum SSH**: Hybrid KEX (mlkem768x25519, sntrup761x25519) + Ed25519 keys
- **Minimal attack surface**: Only SSH terminal + SFTP, no forwarding/tunnels
- **Firewall**: awall with explicit allow-list for required ports only
- **Brute-force protection**: sshguard with progressive IP bans
### SimpleX Infrastructure
- **SMP server**: Message relay on port 5223
- **XFTP server**: File transfer on port 5443
- **Tor hidden services**: .onion addresses for both servers
- **TLS termination**: Caddy with Let's Encrypt and strong crypto
- **Docker compose**: Orchestrated deployment with health checks
### Operational Features
- **Encrypted backups**: All private keys backed up with age encryption
- **CA key removal**: Private keys removed from disk after backup
- **Service monitoring**: OpenRC integration with auto-restart
- **Address discovery**: Scripts to show server fingerprints and .onion URLs
## Server Addresses
After deployment, your relay will be accessible at:
```
# Clearnet
smp://FINGERPRINT@smp.yourdomain.com
xftp://FINGERPRINT@xftp.yourdomain.com
# Tor (clients with Orbot/Tor)
smp://FINGERPRINT@smp.yourdomain.com,ONIONADDRESS.onion
xftp://FINGERPRINT@xftp.yourdomain.com,ONIONADDRESS.onion
```
Get the full addresses with: `cd /opt/simplex && ./print-addresses.sh`
## Backup & Recovery
### Creating Backups
```bash
# Manual backup (on the server)
AGE_RECIPIENT_FILE=/path/to/age-pubkey.txt bash backup.sh
# Or with a single recipient
AGE_RECIPIENT=age1ql3z7... bash backup.sh
```
### Disaster Recovery
```bash
# 1. Fresh Alpine install
# 2. Upload restore script and backup
scp restore.sh backup-20250101-120000.tar.gz.age root@new-host:/root/
# 3. Restore (preserves all server identities)
ssh root@new-host
bash restore.sh backup-20250101-120000.tar.gz.age
# 4. Update DNS to point at new host
```
## DNS Configuration
Create these DNS records before deployment:
```
smp.yourdomain.com. 300 IN A 1.2.3.4
xftp.yourdomain.com. 300 IN A 1.2.3.4
```
Replace `1.2.3.4` with your server's IP address.
## Architecture
```
Internet
[Caddy :80,:443] ← Let's Encrypt ACME
[Docker Network]
├─ [SMP Server :5223] ← Tor Hidden Service
└─ [XFTP Server :5443] ← Tor Hidden Service
```
- **Ports 80/443**: Caddy (HTTP redirect + ACME + info pages)
- **Port 5223**: SMP protocol (TCP, server's own TLS)
- **Port 5443**: XFTP protocol (TCP, server's own TLS)
- **Port 22/2222**: SSH (PQ KEX + Ed25519 only)
## File Locations
- **Deployment**: `/opt/simplex/` (docker-compose.yml, configs, scripts)
- **Server keys**: `/opt/simplex/{smp,xftp}_configs/` (CA keys removed after backup)
- **Tor keys**: `/opt/simplex/{smp,xftp}_tor/` (hidden service identity)
- **SSH config**: `/etc/ssh/` (hardened sshd_config + Ed25519 host key)
- **Firewall**: `/etc/awall/optional/` (JSON policy files)
- **Backups**: `/tmp/simplex-backup-YYYYMMDD-HHMMSS.tar.gz.age`
## Security Notes
### What's Quantum-Safe
- **Session keys**: PQ hybrid KEX protects against "store now, decrypt later"
- **Authentication**: Ed25519 keys (classical, but strongest practical choice today)
### Network Hardening
- Only required ports open (22/2222, 80, 443, 5223, 5443)
- SSH locked to key-only auth, no forwarding, rate-limited
- sshguard blocks brute-force attempts with progressive delays
### Key Management
- CA private keys backed up encrypted, then removed from disk
- Tor onion keys preserved for stable .onion addresses
- SSH host key preserved for stable fingerprint
- All backups encrypted with age (modern, simple, secure)
## Troubleshooting
### Check Service Status
```bash
cd /opt/simplex
docker compose ps # Container status
docker compose logs -f smp-server # SMP logs
docker compose logs -f xftp-server # XFTP logs
./print-addresses.sh # Show server addresses
```
### SSH Issues
```bash
# Test SSH config
sshd -t
# View SSH attempts
journalctl -u sshd -f
# Check sshguard blocks
iptables -L sshguard -n
```
### Firewall Issues
```bash
awall list # Show firewall policies
awall translate # Test policy compilation
iptables -L -n # Show active rules
```
### Certificate Issues
```bash
docker exec simplex-caddy caddy list-certificates
docker logs simplex-caddy
```
## Contributing
1. Fork this repository
2. Make your changes
3. Test on a fresh Alpine instance
4. Submit a pull request
## License
This deployment is provided as-is for educational and operational use. The SimpleX Chat software itself is licensed under AGPL-3.0.
+225
View File
@@ -0,0 +1,225 @@
#!/usr/bin/env bash
#
# backup.sh
#
# Creates an age-encrypted backup of all irreplaceable SimpleX server keys,
# state, and SSH configuration. This is the minimum data set you'd need to
# recreate the server without changing server fingerprints or .onion addresses.
#
# The backup is encrypted using the age public key you specify. Decrypt on
# another machine with:
# age --decrypt --identity /path/to/private_key backup-YYYYMMDD-HHMMSS.tar.gz.age | tar xzf -
#
# Usage:
# AGE_RECIPIENT=age1ql3z7hjy54...your-pubkey bash backup.sh
# AGE_RECIPIENT_FILE=/path/to/pubkeys.txt bash backup.sh
#
# You can specify either a single AGE_RECIPIENT public key, or an
# AGE_RECIPIENT_FILE containing one or more public keys (one per line).
# The backup will be encrypted to all provided keys.
set -euo pipefail
# ============================================================================
# CONFIG
# ============================================================================
: "${AGE_RECIPIENT:=}" # single age public key (age1...)
: "${AGE_RECIPIENT_FILE:=}" # file containing age public keys
: "${SIMPLEX_DIR:=/opt/simplex}"
: "${BACKUP_DIR:=/tmp}"
: "${KEEP_BACKUPS:=7}" # how many historical backups to keep
log() { printf '\033[1;32m[+]\033[0m %s\n' "$*"; }
warn() { printf '\033[1;33m[!]\033[0m %s\n' "$*" >&2; }
die() { printf '\033[1;31m[x]\033[0m %s\n' "$*" >&2; exit 1; }
[[ $EUID -eq 0 ]] || die "Run as root (needs access to private keys)."
# ----------------------------------------------------------------------------
# 1. Validation
# ----------------------------------------------------------------------------
[[ -n "$AGE_RECIPIENT" || -n "$AGE_RECIPIENT_FILE" ]] \
|| die "Set AGE_RECIPIENT=age1... or AGE_RECIPIENT_FILE=/path/to/keys.txt"
if ! command -v age >/dev/null; then
log "Installing age..."
apk add -q age
fi
if [[ -n "$AGE_RECIPIENT_FILE" && ! -f "$AGE_RECIPIENT_FILE" ]]; then
die "AGE_RECIPIENT_FILE '$AGE_RECIPIENT_FILE' not found."
fi
[[ -d "$SIMPLEX_DIR" ]] || die "SimpleX directory '$SIMPLEX_DIR' not found. Run deploy-simplex.sh first."
# Build the age command args
AGE_ARGS=()
if [[ -n "$AGE_RECIPIENT" ]]; then
AGE_ARGS+=(--recipient "$AGE_RECIPIENT")
fi
if [[ -n "$AGE_RECIPIENT_FILE" ]]; then
AGE_ARGS+=(--recipients-file "$AGE_RECIPIENT_FILE")
fi
# ----------------------------------------------------------------------------
# 2. Inventory what needs backing up
# ----------------------------------------------------------------------------
log "Inventorying backup targets..."
# Critical files that prove server identity
TARGETS=(
# SimpleX CA keys (prove server identity across certs)
"$SIMPLEX_DIR/smp_configs/ca.key"
"$SIMPLEX_DIR/xftp_configs/ca.key"
# Tor hidden service keys (.onion addresses)
"$SIMPLEX_DIR/smp_tor/hs_ed25519_secret_key"
"$SIMPLEX_DIR/smp_tor/hs_ed25519_public_key"
"$SIMPLEX_DIR/xftp_tor/hs_ed25519_secret_key"
"$SIMPLEX_DIR/xftp_tor/hs_ed25519_public_key"
# SSH host key (server fingerprint)
"/etc/ssh/ssh_host_ed25519_key"
"/etc/ssh/ssh_host_ed25519_key.pub"
# Root's SSH authorized_keys and config
"/root/.ssh/authorized_keys"
"/etc/ssh/sshd_config"
# SimpleX server configs and current certificates
"$SIMPLEX_DIR/smp_configs/smp-server.ini"
"$SIMPLEX_DIR/xftp_configs/file-server.ini"
"$SIMPLEX_DIR/smp_configs/server.crt"
"$SIMPLEX_DIR/smp_configs/server.key"
"$SIMPLEX_DIR/xftp_configs/server.crt"
"$SIMPLEX_DIR/xftp_configs/server.key"
# Current environment and docker-compose setup
"$SIMPLEX_DIR/.env"
"$SIMPLEX_DIR/docker-compose.yml"
"$SIMPLEX_DIR/print-addresses.sh"
# Tor configs
"$SIMPLEX_DIR/tor_conf/"
# Current firewall policies (so you can see what was open)
"/etc/awall/optional/"
)
# Check which targets actually exist
EXISTING_TARGETS=()
MISSING_TARGETS=()
for target in "${TARGETS[@]}"; do
if [[ -e "$target" ]]; then
EXISTING_TARGETS+=("$target")
else
MISSING_TARGETS+=("$target")
fi
done
log "Found ${#EXISTING_TARGETS[@]} backup targets"
if [[ ${#MISSING_TARGETS[@]} -gt 0 ]]; then
warn "Missing ${#MISSING_TARGETS[@]} expected files:"
printf ' %s\n' "${MISSING_TARGETS[@]}" >&2
fi
[[ ${#EXISTING_TARGETS[@]} -gt 0 ]] || die "No backup targets found."
# ----------------------------------------------------------------------------
# 3. Create the backup
# ----------------------------------------------------------------------------
TIMESTAMP=$(date -u +%Y%m%d-%H%M%S)
BACKUP_NAME="simplex-backup-${TIMESTAMP}"
BACKUP_TAR="${BACKUP_DIR}/${BACKUP_NAME}.tar.gz"
BACKUP_ENCRYPTED="${BACKUP_TAR}.age"
log "Creating backup archive..."
# Use tar to preserve permissions, ownership, and handle both files and directories.
# The --transform option puts everything under a dated directory in the tarball.
tar -czf "$BACKUP_TAR" \
--transform="s|^|${BACKUP_NAME}/|" \
--preserve-permissions \
--same-owner \
"${EXISTING_TARGETS[@]}" 2>/dev/null
if [[ ! -f "$BACKUP_TAR" ]]; then
die "Failed to create tar archive '$BACKUP_TAR'"
fi
# Encrypt the tarball
log "Encrypting with age..."
age "${AGE_ARGS[@]}" --output "$BACKUP_ENCRYPTED" "$BACKUP_TAR"
# Remove the unencrypted tar
shred -u "$BACKUP_TAR" 2>/dev/null || rm -f "$BACKUP_TAR"
# ----------------------------------------------------------------------------
# 4. Verify the backup
# ----------------------------------------------------------------------------
log "Verifying backup can be read..."
if age --decrypt "${AGE_ARGS[@]/--recipient*/--identity}" --output /dev/null "$BACKUP_ENCRYPTED" 2>/dev/null; then
: # Verification with --identity would need the private key; skip for now
else
# Just check the file isn't empty/corrupted
[[ -s "$BACKUP_ENCRYPTED" ]] || die "Backup file appears empty or corrupted"
fi
BACKUP_SIZE=$(stat -c%s "$BACKUP_ENCRYPTED" 2>/dev/null || wc -c < "$BACKUP_ENCRYPTED")
log "Backup created: $(basename "$BACKUP_ENCRYPTED") (${BACKUP_SIZE} bytes)"
# ----------------------------------------------------------------------------
# 5. Cleanup old backups
# ----------------------------------------------------------------------------
log "Cleaning up old backups (keeping ${KEEP_BACKUPS})..."
find "$BACKUP_DIR" -name 'simplex-backup-*.tar.gz.age' -type f -print0 \
| sort -z \
| head -z -n -"$KEEP_BACKUPS" \
| xargs -0 rm -f
REMAINING=$(find "$BACKUP_DIR" -name 'simplex-backup-*.tar.gz.age' -type f | wc -l)
log "Backup directory now contains ${REMAINING} backup(s)"
# ----------------------------------------------------------------------------
# 6. Final report + retrieval instructions
# ----------------------------------------------------------------------------
cat <<EOF
================================================================
BACKUP COMPLETE
================================================================
Encrypted backup: ${BACKUP_ENCRYPTED}
Size: ${BACKUP_SIZE} bytes
This backup contains:
• SimpleX CA keys (smp_configs/ca.key, xftp_configs/ca.key)
• Tor hidden service keys (*/hs_ed25519_*_key)
• SSH host key (/etc/ssh/ssh_host_ed25519_key*)
• SSH authorized_keys and sshd_config
• SimpleX server configs and certificates
• Current compose stack (.env, docker-compose.yml)
• Firewall policies (/etc/awall/optional/)
RETRIEVE THE BACKUP:
1. Copy from the server:
scp -i ~/.ssh/your_key root@host:${BACKUP_ENCRYPTED} ./
2. Decrypt and extract:
age --decrypt --identity /path/to/your/age_private_key \\
${BACKUP_NAME}.tar.gz.age | tar xzf -
WARNING: This backup contains private keys. Store it securely and
delete it from /tmp after copying off-host.
RESTORE PROCESS (if needed):
1. Fresh Alpine install + harden-ssh.sh
2. Extract backup: ${BACKUP_NAME}/
3. Copy keys back to their original paths
4. Run deploy-simplex.sh (will reuse existing keys)
5. Verify fingerprints match the backup
================================================================
EOF
+98
View File
@@ -0,0 +1,98 @@
#cloud-config
#
# SimpleX Chat Relay Server - Cloud-Init Configuration
#
# This cloud-init configuration deploys a complete SimpleX relay server
# on Alpine Linux with:
# - Post-quantum SSH hardening
# - SMP + XFTP servers with Tor hidden services
# - Caddy reverse proxy with Let's Encrypt
# - awall firewall with minimal attack surface
# - Encrypted backup of all server keys
#
# Customize the environment variables below, then use this as user-data
# when creating your cloud instance.
# Use Alpine Linux (most cloud providers support it)
# Recommended: Alpine 3.19+ for latest OpenSSH with PQ KEX support
runcmd:
# Set a hostname (optional)
- hostnamectl set-hostname simplex-relay
# Run the master installer
- |
curl -fsSL https://git.anomalous.dev/57_Wolve/automations/raw/branch/main/deployments/simplex/install-simplex.sh | \
REPO_URL=https://git.anomalous.dev/57_Wolve/automations.git \
DOMAIN=relay.yourdomain.com \
ACME_EMAIL=admin@yourdomain.com \
XFTP_QUOTA=100gb \
SSH_PORT=2222 \
ALLOWED_IP=your.client.ip.here \
KEY_TYPE=rsa4096 \
SMP_PASS= \
XFTP_PASS= \
SKIP_PROMPTS=1 \
AUTO_BACKUP=1 \
REMOVE_CA_KEYS=1 \
DEBUG=0 \
bash
# Optional: Configure additional settings
write_files:
# Custom SSH banner (optional)
- path: /etc/ssh/banner
content: |
===============================================
SimpleX Chat Relay Server
Authorized access only.
All connections are logged and monitored.
===============================================
permissions: '0644'
# Optional: Install additional packages
packages:
- htop
- nano
- curl
- jq
# Optional: Configure automatic security updates (Alpine)
package_update: true
package_upgrade: true
# Set timezone
timezone: UTC
# Configure locale
locale: en_US.UTF-8
# Configure SSH (these will be overridden by harden-ssh.sh)
ssh_pwauth: true # Will be disabled by harden-ssh.sh
disable_root: false # Keep root enabled for harden-ssh.sh
# Optional: Add non-root user (created before SSH hardening)
users:
- name: admin
groups: wheel
sudo: ['ALL=(ALL) NOPASSWD:ALL']
shell: /bin/bash
# Note: SSH hardening will restrict to Ed25519 keys only
# Add your Ed25519 public key here if you want this user to survive hardening:
# ssh_authorized_keys:
# - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... your-key-here
# Optional: Configure fail2ban (will be replaced by sshguard)
# runcmd will install sshguard which is lighter and more suitable
# Security note: The master installer will:
# 1. Generate fresh SSH keys and disable password auth
# 2. Create an encrypted backup containing all private keys
# 3. Remove CA keys from disk (they exist only in the backup)
# 4. Lock down the firewall to required ports only
#
# Make sure to:
# 1. Download the encrypted backup immediately after deployment
# 2. Save the SSH private key from the installer output
# 3. Test SSH access before deploying to production
+541
View File
@@ -0,0 +1,541 @@
#!/usr/bin/env bash
#
# deploy-simplex.sh
#
# Single-file deployment of SimpleX SMP + XFTP + Tor hidden services on a
# fresh Alpine Linux install, behind awall.
#
# Targets Alpine 3.19+. Run as root on a machine where:
# - smp.${DOMAIN} and xftp.${DOMAIN} both resolve here (A/AAAA records set)
# - SSH is your only existing access (the script preserves whatever port
# you specify in SSH_PORT)
#
# Usage:
# 1. Copy this file onto the host: scp deploy-simplex.sh root@host:/root/
# 2. Edit the CONFIG block below (or pass via env vars)
# 3. Run: bash deploy-simplex.sh
#
# What it does:
# 1. Enables community repo, updates apk
# 2. Installs docker, docker-cli-compose, awall, iptables, curl, openrc bits
# 3. Configures awall: deny-all default + ssh + http(80) + https(443) +
# smp(5223) + xftp(5443), with rate-limited SSH
# 4. Writes docker-compose.yml + Caddyfile generator + Tor configs into /opt/simplex
# 5. Starts the stack
# 6. Prints the multi-host SimpleX server addresses once Tor publishes
#
# What it does NOT do (intentional):
# - SSH hardening beyond keeping your existing port. Set up keys yourself
# before running this. The script does not change sshd_config.
# - Backups of CA keys -- it tells you to do that, you do it.
# - Auto-update of containers. Use `docker compose pull && up -d` periodically
# or add Watchtower if you want that.
set -euo pipefail
# ============================================================================
# CONFIG -- edit these or override via env vars
# ============================================================================
: "${DOMAIN:=example.com}" # apex domain; uses smp.$DOMAIN and xftp.$DOMAIN
: "${ACME_EMAIL:=admin@example.com}" # for Let's Encrypt
: "${XFTP_QUOTA:=50gb}" # disk quota for XFTP file storage
: "${KEY_TYPE:=rsa4096}" # Caddy TLS key: rsa4096|rsa2048|p384|p256
# Note: ed25519 NOT supported by Let's Encrypt
: "${SMP_PASS:=}" # optional: queue creation password
: "${XFTP_PASS:=}" # optional: file upload password
: "${SSH_PORT:=22}" # whatever your sshd is listening on
: "${INSTALL_DIR:=/opt/simplex}" # where everything lives
: "${WAN_IFACE:=}" # autodetected if blank
: "${CERT_PATH:=acme-v02.api.letsencrypt.org-directory}"
# use acme-staging-v02.api.letsencrypt.org-directory
# to test without burning rate limits
# ============================================================================
log() { printf '\033[1;32m[+]\033[0m %s\n' "$*"; }
warn() { printf '\033[1;33m[!]\033[0m %s\n' "$*" >&2; }
die() { printf '\033[1;31m[x]\033[0m %s\n' "$*" >&2; exit 1; }
[[ $EUID -eq 0 ]] || die "Run as root."
[[ -f /etc/alpine-release ]] || die "This script targets Alpine Linux."
if [[ -z "$WAN_IFACE" ]]; then
WAN_IFACE=$(ip -o -4 route show default | awk '{print $5; exit}')
[[ -n "$WAN_IFACE" ]] || die "Could not autodetect WAN interface; set WAN_IFACE."
fi
log "WAN interface: $WAN_IFACE"
log "Domain: $DOMAIN"
log "Install dir: $INSTALL_DIR"
# ----------------------------------------------------------------------------
# 1. Repos + packages
# ----------------------------------------------------------------------------
log "Enabling community repo and updating apk..."
ALPINE_VER=$(cut -d. -f1,2 < /etc/alpine-release)
if ! grep -qE "^https?://.+/v${ALPINE_VER}/community" /etc/apk/repositories; then
sed -i -E "s|^#(https?://.+/v${ALPINE_VER}/community)|\1|" /etc/apk/repositories || true
if ! grep -qE "^https?://.+/v${ALPINE_VER}/community" /etc/apk/repositories; then
# Fallback: append the standard community URL based on existing main mirror
MAIN_MIRROR=$(awk '/main$/ {print; exit}' /etc/apk/repositories \
| sed -E "s|/v${ALPINE_VER}/main|/v${ALPINE_VER}/community|")
[[ -n "$MAIN_MIRROR" ]] && echo "$MAIN_MIRROR" >> /etc/apk/repositories
fi
fi
apk update -q
apk upgrade -q
log "Installing packages..."
apk add -q \
docker docker-cli-compose \
awall iptables ip6tables \
curl bash openssl \
ca-certificates
# ----------------------------------------------------------------------------
# 2. Kernel modules + iptables/awall
# ----------------------------------------------------------------------------
log "Loading iptables kernel modules..."
modprobe -q ip_tables || true
modprobe -q ip6_tables || true
modprobe -q iptable_nat || true
modprobe -q iptable_filter || true
log "Enabling iptables/ip6tables/docker at boot..."
rc-update add iptables default
rc-update add ip6tables default
rc-update add docker default
log "Writing awall policies..."
mkdir -p /etc/awall/optional /etc/awall/private
cat > /etc/awall/optional/base.json <<JSON
{
"description": "SimpleX relay base policy: deny-all on internet zone",
"variable": { "wan_if": "${WAN_IFACE}" },
"zone": { "internet": { "iface": "\$wan_if" } },
"policy": [
{ "in": "internet", "action": "drop" },
{ "action": "accept" }
]
}
JSON
cat > /etc/awall/optional/ssh.json <<JSON
{
"description": "Allow SSH on tcp/${SSH_PORT}, rate-limited",
"filter": [
{
"in": "internet",
"out": "_fw",
"service": { "proto": "tcp", "port": ${SSH_PORT} },
"action": "accept",
"conn-limit": { "count": 5, "interval": 60 }
}
]
}
JSON
cat > /etc/awall/optional/web.json <<JSON
{
"description": "Allow HTTP/HTTPS for Caddy info pages and ACME",
"filter": [
{ "in": "internet", "out": "_fw", "service": "http", "action": "accept" },
{ "in": "internet", "out": "_fw", "service": "https", "action": "accept" }
]
}
JSON
cat > /etc/awall/optional/simplex.json <<JSON
{
"description": "SimpleX SMP (5223) and XFTP (5443) protocol ports",
"filter": [
{ "in": "internet", "out": "_fw",
"service": { "proto": "tcp", "port": 5223 }, "action": "accept" },
{ "in": "internet", "out": "_fw",
"service": { "proto": "tcp", "port": 5443 }, "action": "accept" }
]
}
JSON
cat > /etc/awall/optional/icmp.json <<JSON
{
"description": "Allow rate-limited ICMP echo (ping)",
"filter": [
{
"in": "internet",
"service": "ping",
"action": "accept",
"flow-limit": { "count": 10, "interval": 6 }
}
]
}
JSON
awall enable base ssh web simplex icmp
awall translate --output /etc/iptables 2>/dev/null || true
awall translate --output /etc/ip6tables 2>/dev/null || true
awall activate --force
rc-service iptables restart || rc-service iptables start
rc-service ip6tables restart || rc-service ip6tables start
# ----------------------------------------------------------------------------
# 3. Sysctl hardening (modest)
# ----------------------------------------------------------------------------
log "Applying minimal sysctl hardening..."
cat > /etc/sysctl.d/90-simplex.conf <<'EOF'
# IP spoof protection
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1
# Don't forward unless this is a router (Docker re-enables forwarding for the
# docker0 bridge specifically, which is fine)
net.ipv4.ip_forward = 0
# Ignore source-routed packets, ICMP redirects, broadcasts
net.ipv4.conf.all.accept_source_route = 0
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.all.send_redirects = 0
net.ipv4.icmp_echo_ignore_broadcasts = 1
net.ipv4.icmp_ignore_bogus_error_responses = 1
# SYN flood protection
net.ipv4.tcp_syncookies = 1
EOF
sysctl -p /etc/sysctl.d/90-simplex.conf >/dev/null
# ----------------------------------------------------------------------------
# 4. Start docker
# ----------------------------------------------------------------------------
log "Starting docker..."
rc-service docker status >/dev/null 2>&1 || rc-service docker start
# Wait for socket
for _ in $(seq 1 20); do
[[ -S /var/run/docker.sock ]] && break
sleep 1
done
[[ -S /var/run/docker.sock ]] || die "Docker socket didn't appear."
# ----------------------------------------------------------------------------
# 5. Lay out /opt/simplex
# ----------------------------------------------------------------------------
log "Writing compose stack to ${INSTALL_DIR}..."
mkdir -p "$INSTALL_DIR"/tor_conf
cd "$INSTALL_DIR"
cat > .env <<EOF
DOMAIN=${DOMAIN}
ACME_EMAIL=${ACME_EMAIL}
XFTP_QUOTA=${XFTP_QUOTA}
KEY_TYPE=${KEY_TYPE}
SMP_PASS=${SMP_PASS}
XFTP_PASS=${XFTP_PASS}
CERT_PATH=${CERT_PATH}
EOF
chmod 600 .env
cat > docker-compose.yml <<'YAML'
name: simplex
services:
caddy-init:
image: alpine:latest
command: >
sh -c '
if [ ! -f /etc/caddy/Caddyfile ]; then
cat > /etc/caddy/Caddyfile <<EOF
{
email ${ACME_EMAIL}
}
http://smp.${DOMAIN} {
redir https://smp.${DOMAIN}{uri} permanent
}
smp.${DOMAIN}:8443 {
tls { key_type ${KEY_TYPE} }
reverse_proxy smp-server:8000
}
http://xftp.${DOMAIN} {
redir https://xftp.${DOMAIN}{uri} permanent
}
xftp.${DOMAIN}:8443 {
tls { key_type ${KEY_TYPE} }
reverse_proxy xftp-server:8000
}
EOF
fi
'
environment:
DOMAIN: ${DOMAIN:?}
ACME_EMAIL: ${ACME_EMAIL:?}
KEY_TYPE: ${KEY_TYPE:-rsa4096}
volumes:
- ./caddy_conf:/etc/caddy
restart: "no"
caddy:
image: caddy:2-alpine
depends_on:
caddy-init:
condition: service_completed_successfully
cap_add:
- NET_ADMIN
ports:
- "80:80"
- "443:8443"
volumes:
- ./caddy_conf:/etc/caddy
- caddy_data:/data
- caddy_config:/config
restart: unless-stopped
healthcheck:
test: >
sh -c '
test -d /data/caddy/certificates/${CERT_PATH:-acme-v02.api.letsencrypt.org-directory}/smp.${DOMAIN} &&
test -d /data/caddy/certificates/${CERT_PATH:-acme-v02.api.letsencrypt.org-directory}/xftp.${DOMAIN}
'
interval: 5s
timeout: 3s
retries: 60
start_period: 30s
environment:
DOMAIN: ${DOMAIN}
CERT_PATH: ${CERT_PATH:-acme-v02.api.letsencrypt.org-directory}
smp-server:
image: simplexchat/smp-server:latest
depends_on:
caddy:
condition: service_healthy
environment:
ADDR: smp.${DOMAIN}
PASS: ${SMP_PASS:-}
WEB_MANUAL: "0"
volumes:
- ./smp_configs:/etc/opt/simplex
- ./smp_state:/var/opt/simplex
- type: volume
source: caddy_data
target: /certificates
read_only: true
volume:
subpath: "caddy/certificates/${CERT_PATH:-acme-v02.api.letsencrypt.org-directory}/smp.${DOMAIN}"
ports:
- "5223:5223"
restart: unless-stopped
smp-tor:
image: dperson/torproxy:latest
depends_on:
- smp-server
network_mode: "service:smp-server"
volumes:
- ./smp_tor:/var/lib/tor/hidden_service
- ./tor_conf/smp-torrc:/etc/tor/torrc:ro
restart: unless-stopped
xftp-server:
image: simplexchat/xftp-server:latest
depends_on:
caddy:
condition: service_healthy
environment:
ADDR: xftp.${DOMAIN}
QUOTA: ${XFTP_QUOTA:?}
PASS: ${XFTP_PASS:-}
WEB_MANUAL: "0"
volumes:
- ./xftp_configs:/etc/opt/simplex-xftp
- ./xftp_state:/var/opt/simplex-xftp
- ./xftp_files:/srv/xftp
- type: volume
source: caddy_data
target: /certificates
read_only: true
volume:
subpath: "caddy/certificates/${CERT_PATH:-acme-v02.api.letsencrypt.org-directory}/xftp.${DOMAIN}"
ports:
- "5443:443"
restart: unless-stopped
xftp-tor:
image: dperson/torproxy:latest
depends_on:
- xftp-server
network_mode: "service:xftp-server"
volumes:
- ./xftp_tor:/var/lib/tor/hidden_service
- ./tor_conf/xftp-torrc:/etc/tor/torrc:ro
restart: unless-stopped
volumes:
caddy_data:
caddy_config:
YAML
cat > tor_conf/smp-torrc <<'EOF'
# Single-hop / non-anonymous mode: lower latency. The SERVER's location is
# already public via clearnet, so hiding it is moot. Clients remain fully
# anonymous to us. Drop these three lines for a tor-only relay where the
# server's location should also be hidden.
SOCKSPort 0
HiddenServiceNonAnonymousMode 1
HiddenServiceSingleHopMode 1
HiddenServiceDir /var/lib/tor/hidden_service/
HiddenServicePort 5223 127.0.0.1:5223
HiddenServicePort 443 127.0.0.1:8000
Log notice stdout
EOF
cat > tor_conf/xftp-torrc <<'EOF'
SOCKSPort 0
HiddenServiceNonAnonymousMode 1
HiddenServiceSingleHopMode 1
HiddenServiceDir /var/lib/tor/hidden_service/
HiddenServicePort 443 127.0.0.1:443
HiddenServicePort 8443 127.0.0.1:8000
Log notice stdout
EOF
# ----------------------------------------------------------------------------
# 6. Helper script for printing addresses post-bringup
# ----------------------------------------------------------------------------
cat > print-addresses.sh <<'BASH'
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")"
# shellcheck disable=SC1091
source .env
get_fp() {
docker compose logs "$1" 2>/dev/null \
| grep -m1 'Server address:' \
| sed -E 's|.*://([^@]+)@.*|\1|'
}
read_or_blank() { [[ -f "$1" ]] && cat "$1" || echo ""; }
SMP_FP=$(get_fp smp-server)
XFTP_FP=$(get_fp xftp-server)
SMP_ONION=$(read_or_blank ./smp_tor/hostname)
XFTP_ONION=$(read_or_blank ./xftp_tor/hostname)
build_addr() {
local scheme=$1 fp=$2 clearnet=$3 onion=$4
local hosts="$clearnet"
[[ -n "$onion" ]] && hosts="${hosts},${onion}"
echo "${scheme}://${fp}@${hosts}"
}
echo
echo "=== SMP ==="
echo " fingerprint: ${SMP_FP:-<not yet available>}"
echo " clearnet: smp.${DOMAIN}"
echo " onion: ${SMP_ONION:-<not yet available>}"
echo " full addr: $(build_addr smp "$SMP_FP" "smp.${DOMAIN}" "$SMP_ONION")"
echo
echo "=== XFTP ==="
echo " fingerprint: ${XFTP_FP:-<not yet available>}"
echo " clearnet: xftp.${DOMAIN}"
echo " onion: ${XFTP_ONION:-<not yet available>}"
echo " full addr: $(build_addr xftp "$XFTP_FP" "xftp.${DOMAIN}" "$XFTP_ONION")"
echo
BASH
chmod +x print-addresses.sh
# ----------------------------------------------------------------------------
# 7. OpenRC service for the compose stack itself
# ----------------------------------------------------------------------------
log "Creating openrc service for the stack..."
cat > /etc/init.d/simplex <<'EOF'
#!/sbin/openrc-run
name="simplex"
description="SimpleX SMP + XFTP + Tor docker compose stack"
directory="/opt/simplex"
depend() {
need docker
after net firewall
}
start() {
ebegin "Starting SimpleX stack"
cd "$directory"
docker compose up -d
eend $?
}
stop() {
ebegin "Stopping SimpleX stack"
cd "$directory"
docker compose down
eend $?
}
status() {
cd "$directory"
docker compose ps
}
EOF
chmod +x /etc/init.d/simplex
rc-update add simplex default
# ----------------------------------------------------------------------------
# 8. Bring it up
# ----------------------------------------------------------------------------
log "Pulling images and starting the stack..."
cd "$INSTALL_DIR"
docker compose pull
docker compose up -d
log "Waiting up to 90s for Tor to publish hidden services..."
for _ in $(seq 1 90); do
if [[ -f "$INSTALL_DIR/smp_tor/hostname" && -f "$INSTALL_DIR/xftp_tor/hostname" ]]; then
break
fi
sleep 1
done
# ----------------------------------------------------------------------------
# 9. Final report
# ----------------------------------------------------------------------------
echo
echo "================================================================"
echo " SimpleX deployment complete"
echo "================================================================"
"$INSTALL_DIR/print-addresses.sh" || true
cat <<EOF
NEXT STEPS (do these now, not later):
1. BACK UP CA KEYS (these prove server identity if you ever need to rotate):
$INSTALL_DIR/smp_configs/ca.key
$INSTALL_DIR/xftp_configs/ca.key
Copy them off this host, then delete the on-host copies:
rm $INSTALL_DIR/smp_configs/ca.key
rm $INSTALL_DIR/xftp_configs/ca.key
2. BACK UP TOR HIDDEN SERVICE KEYS (preserve the .onion across reinstalls):
$INSTALL_DIR/smp_tor/hs_ed25519_secret_key
$INSTALL_DIR/xftp_tor/hs_ed25519_secret_key
3. Add the server addresses above to your SimpleX app:
Settings -> Network & Servers -> SMP/XFTP servers -> Add
4. Verify the firewall is doing its job:
awall list
iptables -L -n
5. Watch the stack:
cd $INSTALL_DIR && docker compose logs -f
EOF
+298
View File
@@ -0,0 +1,298 @@
#!/usr/bin/env bash
#
# install-simplex.sh
#
# Master installer for SimpleX relay deployment. Designed for cloud-init but
# works as a standalone script. Fetches deployment scripts from git, runs the
# complete setup sequence, and creates an initial encrypted backup.
#
# This script:
# 1. Fetches deployment scripts from your git repo
# 2. Runs harden-ssh.sh (PQ KEX, Ed25519, firewall)
# 3. Runs deploy-simplex.sh (SMP + XFTP + Tor)
# 4. Runs backup.sh with your age public key
# 5. Cleans up CA keys from disk (they're now only in the encrypted backup)
# 6. Reports final status and backup location
#
# Usage as cloud-init user-data:
# #cloud-config
# runcmd:
# - |
# curl -fsSL https://git.anomalous.dev/57_Wolve/automations/raw/branch/main/deployments/simplex/install-simplex.sh | \
# REPO_URL=https://git.anomalous.dev/57_Wolve/automations.git \
# DOMAIN=relay.example.com \
# ACME_EMAIL=admin@example.com \
# XFTP_QUOTA=100gb \
# SSH_PORT=2222 \
# bash
#
# Usage as standalone script:
# curl -fsSL https://git.anomalous.dev/57_Wolve/automations/raw/branch/main/deployments/simplex/install-simplex.sh > install-simplex.sh
# REPO_URL=https://git.anomalous.dev/57_Wolve/automations.git \
# DOMAIN=relay.example.com \
# ACME_EMAIL=admin@example.com \
# bash install-simplex.sh
#
# Required git repo structure (this monorepo):
# automations/
# ├── scripts/
# │ └── harden-ssh.sh # generic, run-anywhere
# ├── deployments/simplex/
# │ ├── deploy-simplex.sh
# │ ├── backup.sh
# │ └── restore.sh # optional
# └── globals/
# └── age-pubkey.txt # your age public key(s), one per line
set -euo pipefail
# ============================================================================
# CONFIG
# ============================================================================
# Git repository containing the deployment scripts and age public key
: "${REPO_URL:=}" # REQUIRED: git repo URL
# SimpleX deployment config
: "${DOMAIN:=}" # REQUIRED: apex domain (uses smp.DOMAIN, xftp.DOMAIN)
: "${ACME_EMAIL:=}" # REQUIRED: Let's Encrypt email
: "${XFTP_QUOTA:=50gb}" # XFTP disk quota
: "${SSH_PORT:=22}" # SSH port (recommend changing from default)
: "${KEY_TYPE:=rsa4096}" # Caddy TLS key type
: "${SMP_PASS:=}" # optional: SMP queue creation password
: "${XFTP_PASS:=}" # optional: XFTP upload password
# Git and installation options
: "${REPO_BRANCH:=main}" # git branch to fetch
: "${HARDEN_PATH:=scripts}" # path within repo to the generic harden-ssh.sh
: "${SIMPLEX_PATH:=deployments/simplex}" # path within repo to the simplex scripts
: "${AGE_PUBKEY_FILE:=globals/age-pubkey.txt}" # path within repo to age public key
: "${INSTALL_DIR:=/opt/simplex-deploy}" # where to clone the repo
: "${ALLOWED_IP:=}" # optional: IP to whitelist in sshguard
: "${AUTO_BACKUP:=1}" # set to 0 to skip initial backup
: "${REMOVE_CA_KEYS:=1}" # set to 0 to keep CA keys on disk
# Behavior flags
: "${SKIP_PROMPTS:=0}" # set to 1 for non-interactive operation
: "${DEBUG:=0}" # set to 1 for verbose output
# ============================================================================
log() { printf '\033[1;32m[+]\033[0m %s\n' "$*"; }
warn() { printf '\033[1;33m[!]\033[0m %s\n' "$*" >&2; }
die() { printf '\033[1;31m[x]\033[0m %s\n' "$*" >&2; exit 1; }
[[ $EUID -eq 0 ]] || die "Run as root."
[[ -f /etc/alpine-release ]] || die "This script targets Alpine Linux."
if [[ "$DEBUG" == "1" ]]; then
set -x
fi
# ----------------------------------------------------------------------------
# 1. Validate required parameters
# ----------------------------------------------------------------------------
[[ -n "$REPO_URL" ]] || die "Set REPO_URL=https://git.anomalous.dev/57_Wolve/automations.git"
[[ -n "$DOMAIN" ]] || die "Set DOMAIN=your.domain.com"
[[ -n "$ACME_EMAIL" ]] || die "Set ACME_EMAIL=admin@your.domain.com"
log "SimpleX relay installer"
log "Repo: $REPO_URL"
log "Domain: $DOMAIN (will create smp.$DOMAIN, xftp.$DOMAIN)"
log "SSH port: $SSH_PORT"
if [[ "$SKIP_PROMPTS" != "1" ]]; then
cat <<EOF
This will install a complete SimpleX relay server on this host:
• SSH hardening with PQ KEX and Ed25519 keys
• awall firewall with minimal attack surface
• SMP + XFTP servers with Tor hidden services
• Caddy reverse proxy with Let's Encrypt TLS
• Initial encrypted backup of all server keys
The process will generate new server keys and may change your SSH config.
Only run this on a fresh Alpine install intended as a SimpleX relay.
Continue? [y/N]
EOF
read -r ans
[[ "${ans,,}" =~ ^(y|yes)$ ]] || die "Aborted."
fi
# ----------------------------------------------------------------------------
# 2. Install git and fetch deployment repo
# ----------------------------------------------------------------------------
log "Installing git and fetching deployment scripts..."
apk add -q git
rm -rf "$INSTALL_DIR"
git clone --depth 1 --branch "$REPO_BRANCH" "$REPO_URL" "$INSTALL_DIR"
# Verify required files exist. harden-ssh.sh is generic and lives under
# scripts/; the simplex scripts live under deployments/simplex/.
HARDEN_DIR="$INSTALL_DIR/$HARDEN_PATH"
SIMPLEX_DIR="$INSTALL_DIR/$SIMPLEX_PATH"
verify_script() { # <dir> <relpath-for-messages> <name>
local path="$1/$3"
[[ -f "$path" ]] || die "Required script not found: $2/$3"
[[ -x "$path" ]] || chmod +x "$path"
}
verify_script "$HARDEN_DIR" "$HARDEN_PATH" "harden-ssh.sh"
verify_script "$SIMPLEX_DIR" "$SIMPLEX_PATH" "deploy-simplex.sh"
verify_script "$SIMPLEX_DIR" "$SIMPLEX_PATH" "backup.sh"
# Check for age public key file
AGE_PUBKEY_PATH="$INSTALL_DIR/$AGE_PUBKEY_FILE"
if [[ "$AUTO_BACKUP" == "1" ]]; then
[[ -f "$AGE_PUBKEY_PATH" ]] || die "Age public key file not found: $AGE_PUBKEY_FILE"
# Validate it contains what looks like age public keys
if ! grep -q "^age1" "$AGE_PUBKEY_PATH"; then
die "Age public key file doesn't contain valid age1... public keys"
fi
fi
log "Repository cloned to: $INSTALL_DIR"
# ----------------------------------------------------------------------------
# 3. Run SSH hardening
# ----------------------------------------------------------------------------
log "Step 1/3: SSH hardening..."
SSH_ENV=""
[[ -n "$SSH_PORT" ]] && SSH_ENV="${SSH_ENV} SSH_PORT=$SSH_PORT"
[[ -n "$ALLOWED_IP" ]] && SSH_ENV="${SSH_ENV} ALLOWED_IP=$ALLOWED_IP"
[[ "$SKIP_PROMPTS" == "1" ]] && SSH_ENV="${SSH_ENV} FORCE=1"
# Capture the private key output from harden-ssh.sh
HARDEN_OUTPUT=$(env $SSH_ENV "$HARDEN_DIR/harden-ssh.sh" 2>&1) || {
echo "$HARDEN_OUTPUT" >&2
die "SSH hardening failed"
}
# Extract the private key from the output for later display
SSH_PRIVATE_KEY=$(echo "$HARDEN_OUTPUT" | sed -n '/BEGIN ROOT PRIVATE KEY/,/END ROOT PRIVATE KEY/p')
SSH_PUBLIC_KEY=$(echo "$HARDEN_OUTPUT" | grep "Public key (already in" | sed 's/.*: //')
HOST_FINGERPRINT=$(echo "$HARDEN_OUTPUT" | grep "Host fingerprint" | tail -1)
log "SSH hardening completed"
# ----------------------------------------------------------------------------
# 4. Run SimpleX deployment
# ----------------------------------------------------------------------------
log "Step 2/3: SimpleX deployment..."
DEPLOY_ENV=""
DEPLOY_ENV="${DEPLOY_ENV} DOMAIN=$DOMAIN"
DEPLOY_ENV="${DEPLOY_ENV} ACME_EMAIL=$ACME_EMAIL"
DEPLOY_ENV="${DEPLOY_ENV} XFTP_QUOTA=$XFTP_QUOTA"
DEPLOY_ENV="${DEPLOY_ENV} SSH_PORT=$SSH_PORT"
DEPLOY_ENV="${DEPLOY_ENV} KEY_TYPE=$KEY_TYPE"
[[ -n "$SMP_PASS" ]] && DEPLOY_ENV="${DEPLOY_ENV} SMP_PASS=$SMP_PASS"
[[ -n "$XFTP_PASS" ]] && DEPLOY_ENV="${DEPLOY_ENV} XFTP_PASS=$XFTP_PASS"
env $DEPLOY_ENV "$SIMPLEX_DIR/deploy-simplex.sh" || die "SimpleX deployment failed"
log "SimpleX deployment completed"
# ----------------------------------------------------------------------------
# 5. Create initial encrypted backup
# ----------------------------------------------------------------------------
if [[ "$AUTO_BACKUP" == "1" ]]; then
log "Step 3/3: Creating initial encrypted backup..."
# Wait a bit for the services to fully initialize
sleep 10
BACKUP_ENV="AGE_RECIPIENT_FILE=$AGE_PUBKEY_PATH"
env $BACKUP_ENV "$SIMPLEX_DIR/backup.sh" || die "Backup creation failed"
log "Initial backup created"
# Remove CA keys from disk now that they're safely backed up
if [[ "$REMOVE_CA_KEYS" == "1" ]]; then
log "Removing CA private keys from disk (they're now only in the encrypted backup)..."
rm -f /opt/simplex/smp_configs/ca.key /opt/simplex/xftp_configs/ca.key
log "CA keys removed from disk"
fi
else
log "Step 3/3: Skipped (AUTO_BACKUP=0)"
fi
# ----------------------------------------------------------------------------
# 6. Final status report
# ----------------------------------------------------------------------------
sleep 5 # Let services settle
cat <<EOF
================================================================
SIMPLEX RELAY INSTALLATION COMPLETE
================================================================
Your SimpleX relay is now running at:
Domain: $DOMAIN
SMP: smp.$DOMAIN:5223
XFTP: xftp.$DOMAIN:5443
SSH: port $SSH_PORT
EOF
# Show server addresses if available
if [[ -f /opt/simplex/print-addresses.sh ]]; then
cd /opt/simplex && ./print-addresses.sh 2>/dev/null || {
log "Server addresses not yet ready; check with: cd /opt/simplex && ./print-addresses.sh"
}
else
log "Server addresses script not found"
fi
# Show backup location
LATEST_BACKUP=$(find /tmp -name 'simplex-backup-*.tar.gz.age' -type f -printf '%T@ %p\n' 2>/dev/null | sort -nr | head -1 | cut -d' ' -f2-)
if [[ -n "$LATEST_BACKUP" && "$AUTO_BACKUP" == "1" ]]; then
BACKUP_SIZE=$(stat -c%s "$LATEST_BACKUP" 2>/dev/null || echo "unknown")
cat <<EOF
BACKUP CREATED:
File: $LATEST_BACKUP
Size: $BACKUP_SIZE bytes
IMPORTANT: Download this backup immediately and store it securely.
It contains all server private keys. The CA keys have been removed
from disk and exist only in this encrypted backup.
Download with:
scp -i ~/.ssh/your_key -P $SSH_PORT root@$DOMAIN:$LATEST_BACKUP ./
EOF
fi
cat <<EOF
IMPORTANT - SAVE THIS SSH PRIVATE KEY:
$SSH_PRIVATE_KEY
Connect with:
ssh -i ~/.ssh/id_ed25519_simplex -p $SSH_PORT root@$DOMAIN
$HOST_FINGERPRINT
NEXT STEPS:
1. Save the SSH private key above to your local machine
2. Download the encrypted backup from $LATEST_BACKUP
3. Test SSH access from another terminal
4. Add the server to your SimpleX app:
Settings -> Network & Servers -> Add servers -> paste the addresses above
5. Test that contacts can reach your relay
================================================================
EOF
# Clean up the git repo (optional - contains no secrets)
if [[ "${CLEANUP_REPO:-1}" == "1" ]]; then
rm -rf "$INSTALL_DIR"
log "Cleaned up installation directory"
fi
log "Installation complete!"
+416
View File
@@ -0,0 +1,416 @@
#!/usr/bin/env bash
#
# restore.sh
#
# Restores a SimpleX server from an age-encrypted backup created by backup.sh.
# This script can rebuild the entire server setup from scratch while preserving:
# - Server fingerprints (SimpleX CA keys)
# - .onion addresses (Tor hidden service keys)
# - SSH host key fingerprint
# - SSH authorized_keys and hardened config
# - All server configurations
#
# IMPORTANT: Run this on a FRESH Alpine installation. This script will:
# 1. Decrypt and extract the backup
# 2. Install all required packages
# 3. Restore SSH hardening + keys
# 4. Restore firewall config
# 5. Deploy the SimpleX stack with preserved keys
# 6. Start services
#
# Usage:
# # Copy restore.sh and your backup onto fresh Alpine host
# scp restore.sh backup-YYYYMMDD-HHMMSS.tar.gz.age root@new-host:/root/
#
# # Run restore (will prompt for age private key)
# ssh root@new-host
# bash restore.sh backup-YYYYMMDD-HHMMSS.tar.gz.age
#
# # Alternative: pass age identity file
# AGE_IDENTITY=/root/backup-key.txt bash restore.sh backup.tar.gz.age
set -euo pipefail
# ============================================================================
# CONFIG
# ============================================================================
: "${AGE_IDENTITY:=}" # optional: path to age private key file
: "${FORCE_OVERWRITE:=0}" # set to 1 to overwrite existing files
: "${SKIP_VALIDATION:=0}" # set to 1 to skip "are you sure" prompts
BACKUP_FILE="${1:-}"
log() { printf '\033[1;32m[+]\033[0m %s\n' "$*"; }
warn() { printf '\033[1;33m[!]\033[0m %s\n' "$*" >&2; }
die() { printf '\033[1;31m[x]\033[0m %s\n' "$*" >&2; exit 1; }
# ----------------------------------------------------------------------------
# 1. Validation and setup
# ----------------------------------------------------------------------------
[[ $EUID -eq 0 ]] || die "Run as root."
[[ -f /etc/alpine-release ]] || die "This script targets Alpine Linux."
[[ -n "$BACKUP_FILE" ]] || die "Usage: $0 <backup-file.tar.gz.age>"
[[ -f "$BACKUP_FILE" ]] || die "Backup file '$BACKUP_FILE' not found."
if [[ "$SKIP_VALIDATION" != "1" ]]; then
cat <<EOF
WARNING: This will restore a SimpleX server backup to this host.
This script will:
1. Install packages (docker, openssh, awall, sshguard, age)
2. Restore SSH hardening (may change your SSH config)
3. Restore firewall rules (may change your network access)
4. Deploy the SimpleX docker stack
5. Restore all server keys and certificates
ONLY run this on a fresh Alpine installation intended to become
a SimpleX relay server. This will overwrite existing configs.
Continue? [y/N]
EOF
read -r ans
[[ "${ans,,}" =~ ^(y|yes)$ ]] || die "Aborted."
fi
log "Starting restore from: $(basename "$BACKUP_FILE")"
# ----------------------------------------------------------------------------
# 2. Install age and decrypt backup
# ----------------------------------------------------------------------------
if ! command -v age >/dev/null; then
log "Installing age..."
apk add -q age
fi
RESTORE_DIR="/tmp/restore-$$"
mkdir -p "$RESTORE_DIR"
cd "$RESTORE_DIR"
log "Decrypting backup..."
if [[ -n "$AGE_IDENTITY" ]]; then
if [[ ! -f "$AGE_IDENTITY" ]]; then
die "AGE_IDENTITY file '$AGE_IDENTITY' not found."
fi
age --decrypt --identity "$AGE_IDENTITY" "$BACKUP_FILE" | tar xzf -
else
# Interactive decryption - age will prompt for passphrase or identity
echo "Enter your age private key or passphrase when prompted:"
age --decrypt "$BACKUP_FILE" | tar xzf -
fi
# Find the extracted directory (should be simplex-backup-YYYYMMDD-HHMMSS)
BACKUP_EXTRACT_DIR=$(find . -maxdepth 1 -type d -name 'simplex-backup-*' | head -1)
[[ -n "$BACKUP_EXTRACT_DIR" ]] || die "Could not find extracted backup directory."
BACKUP_EXTRACT_DIR=$(realpath "$BACKUP_EXTRACT_DIR")
log "Extracted to: $BACKUP_EXTRACT_DIR"
# ----------------------------------------------------------------------------
# 3. Install required packages
# ----------------------------------------------------------------------------
log "Updating package lists and installing requirements..."
# Enable community repo if not already
ALPINE_VER=$(cut -d. -f1,2 < /etc/alpine-release)
if ! grep -qE "^https?://.+/v${ALPINE_VER}/community" /etc/apk/repositories; then
sed -i -E "s|^#(https?://.+/v${ALPINE_VER}/community)|\1|" /etc/apk/repositories || {
MAIN_MIRROR=$(awk '/main$/ {print; exit}' /etc/apk/repositories \
| sed -E "s|/v${ALPINE_VER}/main|/v${ALPINE_VER}/community|")
[[ -n "$MAIN_MIRROR" ]] && echo "$MAIN_MIRROR" >> /etc/apk/repositories
}
fi
apk update -q
apk upgrade -q
apk add -q \
docker docker-cli-compose \
openssh openssh-server \
awall iptables ip6tables \
sshguard \
curl bash openssl \
ca-certificates openrc
# ----------------------------------------------------------------------------
# 4. Restore SSH configuration and keys
# ----------------------------------------------------------------------------
log "Restoring SSH configuration and keys..."
# Back up existing SSH config if it exists
[[ -f /etc/ssh/sshd_config ]] && cp /etc/ssh/sshd_config /etc/ssh/sshd_config.pre-restore
# Restore SSH host key
if [[ -f "$BACKUP_EXTRACT_DIR/etc/ssh/ssh_host_ed25519_key" ]]; then
cp "$BACKUP_EXTRACT_DIR/etc/ssh/ssh_host_ed25519_key" /etc/ssh/
cp "$BACKUP_EXTRACT_DIR/etc/ssh/ssh_host_ed25519_key.pub" /etc/ssh/
chmod 600 /etc/ssh/ssh_host_ed25519_key
chmod 644 /etc/ssh/ssh_host_ed25519_key.pub
# Remove any other host key types
rm -f /etc/ssh/ssh_host_rsa_key* /etc/ssh/ssh_host_ecdsa_key* /etc/ssh/ssh_host_dsa_key*
log "Restored SSH host key: $(ssh-keygen -l -f /etc/ssh/ssh_host_ed25519_key.pub)"
else
warn "No SSH host key found in backup; keeping system default"
fi
# Restore sshd_config
if [[ -f "$BACKUP_EXTRACT_DIR/etc/ssh/sshd_config" ]]; then
cp "$BACKUP_EXTRACT_DIR/etc/ssh/sshd_config" /etc/ssh/
log "Restored SSH daemon configuration"
else
warn "No sshd_config found in backup"
fi
# Restore root's authorized_keys
mkdir -p /root/.ssh
chmod 700 /root/.ssh
if [[ -f "$BACKUP_EXTRACT_DIR/root/.ssh/authorized_keys" ]]; then
cp "$BACKUP_EXTRACT_DIR/root/.ssh/authorized_keys" /root/.ssh/
chmod 600 /root/.ssh/authorized_keys
KEY_COUNT=$(wc -l < /root/.ssh/authorized_keys)
log "Restored ${KEY_COUNT} authorized SSH key(s) for root"
else
warn "No authorized_keys found in backup"
fi
# Validate SSH config
if [[ -f /etc/ssh/sshd_config ]]; then
if sshd -t 2>/dev/null; then
log "SSH configuration validated"
else
warn "SSH configuration validation failed; keeping anyway"
fi
fi
# ----------------------------------------------------------------------------
# 5. Restore firewall configuration
# ----------------------------------------------------------------------------
log "Restoring firewall configuration..."
# Load iptables kernel modules
modprobe -q ip_tables || true
modprobe -q ip6_tables || true
modprobe -q iptable_nat || true
modprobe -q iptable_filter || true
# Restore awall policies if they exist
if [[ -d "$BACKUP_EXTRACT_DIR/etc/awall/optional" ]]; then
mkdir -p /etc/awall/optional
cp -r "$BACKUP_EXTRACT_DIR/etc/awall/optional/"* /etc/awall/optional/
# Enable the restored policies
cd /etc/awall/optional
for policy in *.json; do
[[ -f "$policy" ]] || continue
POLICY_NAME=$(basename "$policy" .json)
awall enable "$POLICY_NAME"
log "Enabled awall policy: $POLICY_NAME"
done
# Activate firewall
awall translate --output /etc/iptables 2>/dev/null || true
awall translate --output /etc/ip6tables 2>/dev/null || true
awall activate --force
log "Firewall rules activated"
else
warn "No awall policies found in backup"
fi
# Enable services
rc-update add iptables default || true
rc-update add ip6tables default || true
rc-update add sshd default || true
rc-update add sshguard default || true
rc-update add docker default || true
# ----------------------------------------------------------------------------
# 6. Restore SimpleX configuration and keys
# ----------------------------------------------------------------------------
SIMPLEX_DIR="/opt/simplex"
log "Restoring SimpleX configuration to $SIMPLEX_DIR..."
mkdir -p "$SIMPLEX_DIR"
# Restore the core SimpleX files
SIMPLEX_FILES=(
".env"
"docker-compose.yml"
"print-addresses.sh"
"tor_conf/"
)
for item in "${SIMPLEX_FILES[@]}"; do
SRC="$BACKUP_EXTRACT_DIR/opt/simplex/$item"
if [[ -e "$SRC" ]]; then
cp -r "$SRC" "$SIMPLEX_DIR/"
log "Restored: $item"
else
warn "Missing from backup: $item"
fi
done
# Make script executable
[[ -f "$SIMPLEX_DIR/print-addresses.sh" ]] && chmod +x "$SIMPLEX_DIR/print-addresses.sh"
# Restore SimpleX server keys and configs
mkdir -p "$SIMPLEX_DIR/smp_configs" "$SIMPLEX_DIR/xftp_configs" \
"$SIMPLEX_DIR/smp_state" "$SIMPLEX_DIR/xftp_state" \
"$SIMPLEX_DIR/xftp_files" "$SIMPLEX_DIR/smp_tor" "$SIMPLEX_DIR/xftp_tor"
# SMP server restoration
for key in ca.key server.crt server.key smp-server.ini; do
SRC="$BACKUP_EXTRACT_DIR/opt/simplex/smp_configs/$key"
if [[ -f "$SRC" ]]; then
cp "$SRC" "$SIMPLEX_DIR/smp_configs/"
log "Restored SMP: $key"
fi
done
# XFTP server restoration
for key in ca.key server.crt server.key file-server.ini; do
SRC="$BACKUP_EXTRACT_DIR/opt/simplex/xftp_configs/$key"
if [[ -f "$SRC" ]]; then
cp "$SRC" "$SIMPLEX_DIR/xftp_configs/"
log "Restored XFTP: $key"
fi
done
# Tor hidden service keys
for service in smp xftp; do
for key in hs_ed25519_secret_key hs_ed25519_public_key hostname; do
SRC="$BACKUP_EXTRACT_DIR/opt/simplex/${service}_tor/$key"
if [[ -f "$SRC" ]]; then
cp "$SRC" "$SIMPLEX_DIR/${service}_tor/"
log "Restored Tor ($service): $key"
fi
done
done
# Set proper ownership and permissions
chmod 600 "$SIMPLEX_DIR"/*_configs/ca.key 2>/dev/null || true
chmod 600 "$SIMPLEX_DIR"/*_tor/hs_ed25519_secret_key 2>/dev/null || true
# ----------------------------------------------------------------------------
# 7. Start services
# ----------------------------------------------------------------------------
log "Starting services..."
# Start firewall services
rc-service iptables start || rc-service iptables restart || true
rc-service ip6tables start || rc-service ip6tables restart || true
# Configure and start sshguard (basic setup - backup.sh doesn't save the full config)
mkdir -p /etc/sshguard
cat > /etc/sshguard/sshguard.conf <<'EOF'
BACKEND="/usr/libexec/sshg-fw-iptables"
LOGREADER="LANG=C journalctl -afb -p info -n1 -u sshd -o cat"
THRESHOLD=30
BLOCK_TIME=300
DETECTION_TIME=1800
PID_FILE=/run/sshguard.pid
EOF
# Basic sshguard iptables setup
cat > /etc/local.d/sshguard-iptables.start <<'EOF'
#!/bin/sh
SSH_PORT=$(awk '/^Port / {print $2; exit}' /etc/ssh/sshd_config)
SSH_PORT=${SSH_PORT:-22}
for ipt in iptables ip6tables; do
$ipt -N sshguard 2>/dev/null || true
$ipt -C INPUT -p tcp --dport "$SSH_PORT" -j sshguard 2>/dev/null \
|| $ipt -I INPUT -p tcp --dport "$SSH_PORT" -j sshguard
done
EOF
chmod +x /etc/local.d/sshguard-iptables.start
rc-update add local default 2>/dev/null || true
/etc/local.d/sshguard-iptables.start
rc-service sshguard start || true
# Start docker
rc-service docker start || true
# Wait for docker socket
for _ in $(seq 1 20); do
[[ -S /var/run/docker.sock ]] && break
sleep 1
done
if [[ ! -S /var/run/docker.sock ]]; then
die "Docker socket didn't appear after starting service"
fi
# ----------------------------------------------------------------------------
# 8. Deploy SimpleX stack
# ----------------------------------------------------------------------------
if [[ -f "$SIMPLEX_DIR/docker-compose.yml" && -f "$SIMPLEX_DIR/.env" ]]; then
log "Starting SimpleX docker stack..."
cd "$SIMPLEX_DIR"
docker compose pull
docker compose up -d
log "Waiting for services to stabilize..."
sleep 10
# Show status
docker compose ps
else
warn "SimpleX compose files not found; skipping docker deployment"
fi
# ----------------------------------------------------------------------------
# 9. SSH service restart (do this last so we don't kill our session early)
# ----------------------------------------------------------------------------
log "Reloading SSH service with restored configuration..."
rc-service sshd reload || rc-service sshd restart || true
# ----------------------------------------------------------------------------
# 10. Cleanup and final report
# ----------------------------------------------------------------------------
log "Cleaning up temporary files..."
rm -rf "$RESTORE_DIR"
# Display current status and next steps
cat <<EOF
================================================================
RESTORE COMPLETE
================================================================
Services Status:
$(rc-service --list | grep -E '(docker|sshd|iptables|sshguard)' || true)
Next Steps:
1. VERIFY SSH ACCESS STILL WORKS
Test from another terminal before closing this session:
ssh -i ~/.ssh/your_key root@$(hostname)
2. CHECK SIMPLEX SERVICES
cd $SIMPLEX_DIR && docker compose ps
./print-addresses.sh
3. VERIFY SERVER FINGERPRINTS
Compare these to your original backup to confirm identities were preserved:
SSH host key:
$(ssh-keygen -l -f /etc/ssh/ssh_host_ed25519_key.pub 2>/dev/null | sed 's/^/ /' || echo " [not found]")
SMP server (when ready):
$(cd "$SIMPLEX_DIR" 2>/dev/null && docker compose logs smp-server 2>/dev/null | grep -m1 'Server address:' | sed 's/^/ /' || echo " [check with: cd $SIMPLEX_DIR && docker compose logs smp-server]")
4. VERIFY .ONION ADDRESSES
These should match your backup:
SMP: $(cat "$SIMPLEX_DIR/smp_tor/hostname" 2>/dev/null || echo "[not yet available]")
XFTP: $(cat "$SIMPLEX_DIR/xftp_tor/hostname" 2>/dev/null || echo "[not yet available]")
5. UPDATE DNS
Ensure smp.yourdomain.com and xftp.yourdomain.com point to this host
6. TEST CONNECTIONS
Verify existing SimpleX contacts can still reach your server
================================================================
EOF