Harden security
This commit is contained in:
58
utilities/prosody/ensure_xmpp_secret.sh
Executable file
58
utilities/prosody/ensure_xmpp_secret.sh
Executable 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"
|
||||
@@ -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() {
|
||||
|
||||
@@ -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 .. "/"
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user