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:
@@ -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.
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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!"
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user