Harden security

This commit is contained in:
2026-03-05 05:42:19 +00:00
parent 06735bdfb1
commit 438e561da0
75 changed files with 6260 additions and 278 deletions

View File

@@ -0,0 +1,58 @@
#!/usr/bin/env bash
set -euo pipefail
STACK_ENV="${1:-}"
if [[ -z "$STACK_ENV" ]]; then
echo "Usage: $0 /path/to/stack.env" >&2
exit 2
fi
mkdir -p "$(dirname "$STACK_ENV")"
touch "$STACK_ENV"
current_secret=""
if grep -q '^XMPP_SECRET=' "$STACK_ENV"; then
current_secret="$(grep '^XMPP_SECRET=' "$STACK_ENV" | head -n1 | cut -d= -f2- | tr -d '"' | tr -d "'" | tr -d '\r' | tr -d '\n')"
fi
if [[ -n "$current_secret" ]]; then
printf "%s" "$current_secret"
exit 0
fi
generate_secret() {
if command -v openssl >/dev/null 2>&1; then
openssl rand -base64 48 | tr -d '\n'
return 0
fi
if command -v python3 >/dev/null 2>&1; then
python3 -c 'import secrets; print(secrets.token_urlsafe(48))'
return 0
fi
head -c 48 /dev/urandom | base64 | tr -d '\n'
}
secret="$(generate_secret)"
if [[ -z "$secret" ]]; then
echo "Failed to generate XMPP_SECRET." >&2
exit 1
fi
tmp="$(mktemp)"
awk -v s="$secret" '
BEGIN { done = 0 }
/^XMPP_SECRET=/ {
if (!done) {
print "XMPP_SECRET=" s
done = 1
}
next
}
{ print }
END {
if (!done) print "XMPP_SECRET=" s
}
' "$STACK_ENV" > "$tmp"
mv "$tmp" "$STACK_ENV"
printf "%s" "$secret"

View File

@@ -3,6 +3,7 @@ set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
STACK_ENV="${STACK_ENV:-$ROOT_DIR/stack.env}"
ENSURE_XMPP_SECRET_SCRIPT="$ROOT_DIR/utilities/prosody/ensure_xmpp_secret.sh"
if [[ -f "$STACK_ENV" ]]; then
set -a
@@ -10,6 +11,11 @@ if [[ -f "$STACK_ENV" ]]; then
set +a
fi
if [[ -x "$ENSURE_XMPP_SECRET_SCRIPT" ]]; then
XMPP_SECRET="$("$ENSURE_XMPP_SECRET_SCRIPT" "$STACK_ENV")"
export XMPP_SECRET
fi
STACK_ID="${GIA_STACK_ID:-${STACK_ID:-}}"
STACK_ID="$(echo "$STACK_ID" | tr -cs 'a-zA-Z0-9._-' '-' | sed 's/^-*//; s/-*$//')"
@@ -29,21 +35,41 @@ PROSODY_CONFIG_FILE="${QUADLET_PROSODY_CONFIG_FILE:-$ROOT_DIR/utilities/prosody/
PROSODY_CERTS_DIR="${QUADLET_PROSODY_CERTS_DIR:-$ROOT_DIR/.podman/gia_prosody_certs}"
PROSODY_DATA_DIR="${QUADLET_PROSODY_DATA_DIR:-$ROOT_DIR/.podman/gia_prosody_data}"
PROSODY_LOGS_DIR="${QUADLET_PROSODY_LOGS_DIR:-$ROOT_DIR/.podman/gia_prosody_logs}"
PROSODY_IMAGE="${PROSODY_IMAGE:-docker.io/prosody/prosody-alpine:latest}"
mkdir -p "$PROSODY_CERTS_DIR" "$PROSODY_DATA_DIR" "$PROSODY_LOGS_DIR"
up() {
local run_args=()
local pod_state=""
if podman pod exists "$POD_NAME"; then
pod_state="$(podman pod inspect "$POD_NAME" --format '{{.State}}' 2>/dev/null || true)"
if [[ "$pod_state" == "Running" ]]; then
run_args+=(--pod "$POD_NAME")
else
echo "Warning: pod '$POD_NAME' state is '$pod_state'; starting $PROSODY_CONTAINER standalone with explicit ports." >&2
run_args+=(-p 5222:5222 -p 5269:5269 -p 5280:5280 -p 8888:8888)
fi
else
echo "Warning: pod '$POD_NAME' not found; starting $PROSODY_CONTAINER standalone with explicit ports." >&2
run_args+=(-p 5222:5222 -p 5269:5269 -p 5280:5280 -p 8888:8888)
fi
podman run -d \
--replace \
--name "$PROSODY_CONTAINER" \
--pod "$POD_NAME" \
"${run_args[@]}" \
--env-file "$STACK_ENV" \
-v "$PROSODY_CONFIG_FILE:/etc/prosody/prosody.cfg.lua:ro" \
-v "$PROSODY_CERTS_DIR:/etc/prosody/certs" \
-v "$PROSODY_DATA_DIR:/var/lib/prosody" \
-v "$PROSODY_LOGS_DIR:/var/log/prosody" \
-v "$ROOT_DIR:/code" \
docker.io/prosody/prosody:0.12 >/dev/null
echo "Started $PROSODY_CONTAINER in pod $POD_NAME"
"$PROSODY_IMAGE" >/dev/null
if [[ " ${run_args[*]} " == *" --pod "* ]]; then
echo "Started $PROSODY_CONTAINER in pod $POD_NAME"
else
echo "Started $PROSODY_CONTAINER standalone (not attached to pod $POD_NAME)"
fi
}
down() {

View File

@@ -1,9 +1,19 @@
local env = os.getenv
local domain = env("DOMAIN") or "example.com"
local xmpp_component = env("XMPP_JID") or ("jews." .. domain)
local share_host = env("XMPP_SHARE_HOST") or ("share." .. domain)
local xmpp_secret = env("XMPP_SECRET") or ""
if xmpp_secret == "" then
error("XMPP_SECRET is required for Prosody component authentication")
end
sasl_mechanisms = { "PLAIN", "SCRAM-SHA-1", "SCRAM-SHA-256" }
daemonize = false
pidfile = "/run/prosody/prosody.pid"
admins = { "mm@zm.is" }
admins = { env("XMPP_ADMIN_JID") or ("admin@" .. domain) }
modules_enabled = {
"disco";
@@ -59,16 +69,16 @@ certificates = "certs"
component_ports = { 8888 }
component_interfaces = { "0.0.0.0" }
VirtualHost "zm.is"
VirtualHost domain
authentication = "external_insecure"
external_auth_command = "/code/utilities/prosody/auth_django.sh"
certificate = "/etc/prosody/certs/cert.pem"
Component "jews.zm.is"
component_secret = "REepvw+QeX3ZzfmRSbBMKQhyiPd5bFowesnYuiiYbiYy2ZQVXvayxmsB"
Component xmpp_component
component_secret = xmpp_secret
Component "share.zm.is" "http_file_share"
Component share_host "http_file_share"
http_ports = { 5280 }
http_interfaces = { "0.0.0.0", "::" }
http_external_url = "https://share.zm.is/"
http_external_url = "https://" .. share_host .. "/"

View File

@@ -3,8 +3,13 @@ set -euo pipefail
# Run as root from host. This script pipes certificate material through the
# `code` user into the Prosody container via podman exec.
DOMAIN="${DOMAIN:-zm.is}"
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
STACK_ENV="${STACK_ENV:-$ROOT_DIR/stack.env}"
if [[ -f "$STACK_ENV" ]]; then
set -a
. "$STACK_ENV"
set +a
fi
STACK_ID="${GIA_STACK_ID:-${STACK_ID:-}}"
STACK_ID="$(echo "$STACK_ID" | tr -cs 'a-zA-Z0-9._-' '-' | sed 's/^-*//; s/-*$//')"
@@ -14,16 +19,101 @@ else
PROSODY_CONTAINER_DEFAULT="prosody_gia"
fi
PROSODY_CONTAINER="${PROSODY_CONTAINER:-$PROSODY_CONTAINER_DEFAULT}"
MANAGE_SCRIPT="${MANAGE_SCRIPT:-$ROOT_DIR/utilities/prosody/manage_prosody_container.sh}"
ACME_BASE_DIR="${ACME_BASE_DIR:-/root/.acme.sh}"
CERT_NAME="${CERT_NAME:-${ACME_CERT_NAME:-}}"
FULLCHAIN_PATH="${FULLCHAIN_PATH:-/root/.acme.sh/${DOMAIN}/fullchain.cer}"
KEY_PATH="${KEY_PATH:-/root/.acme.sh/${DOMAIN}/${DOMAIN}.key}"
FULLCHAIN_PATH="${FULLCHAIN_PATH:-}"
KEY_PATH="${KEY_PATH:-}"
CERT_PATH_IN_CONTAINER="${CERT_PATH_IN_CONTAINER:-/etc/prosody/certs/cert.pem}"
CONTAINER_WAIT_SECONDS="${CONTAINER_WAIT_SECONDS:-15}"
resolve_cert_paths() {
local cert_dir=""
local expected_key=""
if [[ -n "$CERT_NAME" ]]; then
cert_dir="$ACME_BASE_DIR/$CERT_NAME"
if [[ -r "$cert_dir/fullchain.cer" ]]; then
expected_key="$cert_dir/${CERT_NAME}.key"
if [[ -r "$expected_key" ]]; then
FULLCHAIN_PATH="$cert_dir/fullchain.cer"
KEY_PATH="$expected_key"
return 0
fi
KEY_PATH="$(find "$cert_dir" -maxdepth 1 -type f -name '*.key' | head -n1 || true)"
if [[ -n "$KEY_PATH" && -r "$KEY_PATH" ]]; then
FULLCHAIN_PATH="$cert_dir/fullchain.cer"
return 0
fi
fi
echo "Requested CERT_NAME '$CERT_NAME' does not provide readable fullchain/key under $cert_dir" >&2
return 1
fi
cert_dir="$(find "$ACME_BASE_DIR" -mindepth 1 -maxdepth 1 -type d \
-exec test -r '{}/fullchain.cer' ';' -printf '%T@ %p\n' \
| sort -nr \
| awk 'NR==1 {print $2}' || true)"
if [[ -z "$cert_dir" ]]; then
echo "No readable ACME certificate directories with fullchain.cer found under $ACME_BASE_DIR" >&2
return 1
fi
FULLCHAIN_PATH="$cert_dir/fullchain.cer"
KEY_PATH="$(find "$cert_dir" -maxdepth 1 -type f -name '*.key' | head -n1 || true)"
if [[ -z "$KEY_PATH" || ! -r "$KEY_PATH" ]]; then
echo "No readable key file (*.key) found in $cert_dir" >&2
return 1
fi
return 0
}
code_podman() {
su -s /bin/sh code -c "podman $*"
}
container_exists() {
code_podman "container exists '$PROSODY_CONTAINER'"
}
container_is_running() {
[[ "$(code_podman "inspect '$PROSODY_CONTAINER' --format '{{.State.Running}}'" 2>/dev/null || true)" == "true" ]]
}
ensure_running_container() {
if ! container_exists; then
echo "Prosody container '$PROSODY_CONTAINER' not found for user 'code'; attempting startup..." >&2
su -s /bin/sh code -c "cd '$ROOT_DIR/utilities/prosody' && '$MANAGE_SCRIPT' up"
fi
if ! container_exists; then
echo "Failed to create/start Prosody container: $PROSODY_CONTAINER" >&2
exit 1
fi
if ! container_is_running; then
code_podman "start '$PROSODY_CONTAINER'" >/dev/null 2>&1 || true
fi
local i=0
while (( i < CONTAINER_WAIT_SECONDS )); do
if container_is_running; then
return 0
fi
sleep 1
i=$((i + 1))
done
echo "Prosody container exists but is not running: $PROSODY_CONTAINER" >&2
code_podman "inspect '$PROSODY_CONTAINER' --format 'status={{.State.Status}} exit={{.State.ExitCode}} error={{.State.Error}}'" >&2 || true
echo "Recent Prosody logs:" >&2
code_podman "logs --tail 120 '$PROSODY_CONTAINER'" >&2 || true
exit 1
}
if [[ "$(id -u)" -ne 0 ]]; then
echo "This script must run as root." >&2
exit 1
fi
if [[ -z "$FULLCHAIN_PATH" || -z "$KEY_PATH" ]]; then
resolve_cert_paths
fi
if [[ ! -r "$FULLCHAIN_PATH" ]]; then
echo "Missing or unreadable fullchain: $FULLCHAIN_PATH" >&2
exit 1
@@ -34,10 +124,22 @@ if [[ ! -r "$KEY_PATH" ]]; then
exit 1
fi
ensure_running_container
cat "$FULLCHAIN_PATH" "$KEY_PATH" \
| sed '/^$/d' \
| su -s /bin/sh code -c "podman exec -i $PROSODY_CONTAINER sh -lc 'cat > $CERT_PATH_IN_CONTAINER'"
| su -s /bin/sh code -c "podman exec --user 0 -i '$PROSODY_CONTAINER' sh -lc 'cat > \"$CERT_PATH_IN_CONTAINER\"'"
su -s /bin/sh code -c "podman exec $PROSODY_CONTAINER sh -lc 'chown prosody:prosody $CERT_PATH_IN_CONTAINER && chmod 0600 $CERT_PATH_IN_CONTAINER && prosodyctl reload'"
su -s /bin/sh code -c "podman exec --user 0 '$PROSODY_CONTAINER' sh -lc '
set -e
chown prosody:prosody \"$CERT_PATH_IN_CONTAINER\"
chmod 0600 \"$CERT_PATH_IN_CONTAINER\"
if prosodyctl reload >/dev/null 2>&1; then
exit 0
fi
# In foreground/container mode prosodyctl may report \"Prosody is not running\"
# despite PID 1 being the active prosody process. HUP PID 1 as reload fallback.
kill -HUP 1
'"
echo "Prosody certificate updated and reloaded in container: $PROSODY_CONTAINER"