7faa9098de
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
542 lines
16 KiB
Bash
542 lines
16 KiB
Bash
#!/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
|