Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@ jobs:
scripts/test-mobile-release-candidate-publisher.sh
- name: Mobile worktree identity contract
run: scripts/test-mobile-worktree-overrides.sh
- name: Production Compose environment initialization contract
run: |
shellcheck deploy/compose/run.sh deploy/compose/test-init.sh
deploy/compose/test-init.sh
- name: File size ratchet unit tests
run: node --test scripts/check-file-sizes-core.test.mjs

Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ __pycache__/
.env
.env.local
.env.*.local
.env.init.*

# Editor / IDE
.idea/
Expand Down
4 changes: 2 additions & 2 deletions deploy/compose/.env.example
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Buzz production Docker Compose environment.
# Copy to .env and replace every CHANGE_ME value before running.
# The bootstrap script should generate this file for normal users.
# Run `./run.sh init <domain> <owner-pubkey-hex>` to create `.env`, or install
# this file as mode 600 and replace every CHANGE_ME value before running.

# Image published by the public image pipeline. Use `:main` for pre-release testing. Pin `:sha-<7>` or a semver release tag for production.
BUZZ_IMAGE=ghcr.io/block/buzz:main
Expand Down
21 changes: 14 additions & 7 deletions deploy/compose/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ the root `docker-compose.yml`, which remains local development infrastructure.

```bash
cd deploy/compose
cp .env.example .env
$EDITOR .env # replace every CHANGE_ME value
./run.sh init buzz.example.com <64-character-owner-pubkey-hex>
$EDITOR .env # review the image tag and optional ports
./run.sh start
```

Expand All @@ -19,9 +19,17 @@ cd deploy/compose
BUZZ_COMPOSE_TLS=true ./run.sh start
```

The bootstrap script should eventually replace manual `.env` editing for normal
users. It is responsible for generating stable secrets and, optionally, an owner
keypair.
`init` refuses to overwrite an existing `.env`. It uses the public key from an
existing Nostr identity and generates the relay, hook, database, Redis, and
MinIO secrets once. The resulting file is mode 600. Back it up securely before
starting the stack; running `init` again is not a secret-rotation workflow.

To configure the file manually instead:

```bash
install -m 600 .env.example .env
$EDITOR .env # replace every CHANGE_ME value
```

## Production notes

Expand Down Expand Up @@ -52,8 +60,7 @@ Before sharing an install link publicly, verify a fresh install with:

```bash
cd deploy/compose
cp .env.example .env
$EDITOR .env
./run.sh init buzz.example.com <64-character-owner-pubkey-hex>
./run.sh config
./run.sh start
curl -fsS "http://127.0.0.1:$(grep -E '^BUZZ_HTTP_PORT=' .env | cut -d= -f2-)/_liveness"
Expand Down
111 changes: 109 additions & 2 deletions deploy/compose/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,114 @@ compose() {
docker compose --env-file .env "${COMPOSE_FILES[@]}" "$@"
}

valid_domain() {
local domain="$1"
local label
local -a labels

if ((${#domain} > 253)) ||
[[ "$domain" != *.* ]] ||
[[ "$domain" == .* ]] ||
[[ "$domain" == *. ]] ||
[[ "$domain" == *..* ]]; then
return 1
fi

IFS=. read -r -a labels <<<"$domain"
for label in "${labels[@]}"; do
if [[ ! "$label" =~ ^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$ ]]; then
return 1
fi
done
}

init_env() (
if (($# != 2)); then
printf 'Usage: ./run.sh init <domain> <owner-pubkey-hex>\n' >&2
exit 1
fi
if ! command -v openssl >/dev/null 2>&1; then
printf 'Environment initialization requires openssl\n' >&2
exit 1
fi

local domain="${1,,}"
local owner_pubkey="${2,,}"
if ! valid_domain "$domain"; then
printf 'Domain must be a valid DNS name such as buzz.example.com\n' >&2
exit 1
fi
if [[ ! "$owner_pubkey" =~ ^[0-9a-f]{64}$ ]]; then
printf 'Owner public key must be 64 hexadecimal characters\n' >&2
exit 1
fi
if [[ -e .env ]] || [[ -L .env ]]; then
printf 'Refusing to overwrite deploy/compose/.env\n' >&2
exit 1
fi

umask 077
local init_tmp
init_tmp="$(mktemp ./.env.init.XXXXXXXXXX)"
trap 'if [[ -n "${init_tmp:-}" ]] && [[ -f "$init_tmp" ]]; then rm -f -- "$init_tmp"; fi' EXIT

local relay_private_key
local hook_secret
local postgres_password
local redis_password
local s3_access_key
local s3_secret_key
relay_private_key="$(openssl rand -hex 32)"
hook_secret="$(openssl rand -hex 32)"
postgres_password="$(openssl rand -hex 32)"
redis_password="$(openssl rand -hex 32)"
s3_access_key="buzz$(openssl rand -hex 12)"
s3_secret_key="$(openssl rand -hex 32)"

while IFS= read -r line || [[ -n "$line" ]]; do
case "$line" in
BUZZ_DOMAIN=*) printf 'BUZZ_DOMAIN=%s\n' "$domain" ;;
RELAY_URL=*) printf 'RELAY_URL=wss://%s\n' "$domain" ;;
BUZZ_MEDIA_BASE_URL=*) printf 'BUZZ_MEDIA_BASE_URL=https://%s/media\n' "$domain" ;;
BUZZ_MEDIA_SERVER_DOMAIN=*) printf 'BUZZ_MEDIA_SERVER_DOMAIN=%s\n' "$domain" ;;
BUZZ_CORS_ORIGINS=*) printf 'BUZZ_CORS_ORIGINS=https://%s\n' "$domain" ;;
RELAY_OWNER_PUBKEY=*) printf 'RELAY_OWNER_PUBKEY=%s\n' "$owner_pubkey" ;;
BUZZ_RELAY_PRIVATE_KEY=*) printf 'BUZZ_RELAY_PRIVATE_KEY=%s\n' "$relay_private_key" ;;
BUZZ_GIT_HOOK_HMAC_SECRET=*) printf 'BUZZ_GIT_HOOK_HMAC_SECRET=%s\n' "$hook_secret" ;;
POSTGRES_PASSWORD=*) printf 'POSTGRES_PASSWORD=%s\n' "$postgres_password" ;;
REDIS_PASSWORD=*) printf 'REDIS_PASSWORD=%s\n' "$redis_password" ;;
BUZZ_S3_ACCESS_KEY=*) printf 'BUZZ_S3_ACCESS_KEY=%s\n' "$s3_access_key" ;;
BUZZ_S3_SECRET_KEY=*) printf 'BUZZ_S3_SECRET_KEY=%s\n' "$s3_secret_key" ;;
*) printf '%s\n' "$line" ;;
esac
done <.env.example >"$init_tmp"
chmod 0600 "$init_tmp"

if ! ln -- "$init_tmp" .env; then
printf 'Refusing to overwrite deploy/compose/.env\n' >&2
exit 1
fi
rm -f -- "$init_tmp"
init_tmp=
unset \
relay_private_key \
hook_secret \
postgres_password \
redis_password \
s3_access_key \
s3_secret_key

printf 'Created deploy/compose/.env with mode 600; review and back it up before starting Buzz\n'
)

require_env() {
if [[ ! -f .env ]]; then
cat >&2 <<'MSG'
Missing deploy/compose/.env.

Copy .env.example to .env and replace every CHANGE_ME value, or run the bootstrap
script once it lands. Do not start production with generated secrets missing.
Run ./run.sh init <domain> <owner-pubkey-hex>, or install .env.example as a
mode-600 .env and replace every CHANGE_ME value manually. Do not start
production with generated secrets missing.
MSG
exit 1
fi
Expand Down Expand Up @@ -51,6 +152,10 @@ MSG
}

case "${1:-help}" in
init)
shift
init_env "$@"
;;
start|up)
require_env
compose up -d --wait
Expand Down Expand Up @@ -100,6 +205,8 @@ case "${1:-help}" in
Usage: ./run.sh <command>

Commands:
init <domain> <owner-pubkey-hex>
Create a new mode-600 .env with stable generated secrets
start Start Buzz with docker compose up -d --wait
stop Stop containers without deleting volumes
restart Recreate the relay after env/image changes
Expand Down
102 changes: 102 additions & 0 deletions deploy/compose/test-init.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
#!/usr/bin/env bash
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
WORK_DIR="$(mktemp -d /tmp/buzz-compose-init-test.XXXXXXXXXX)"

cleanup() {
case "${WORK_DIR}" in
/tmp/buzz-compose-init-test.*) rm -rf -- "${WORK_DIR}" ;;
esac
}
trap cleanup EXIT

make_case() {
local name="$1"
local case_dir="${WORK_DIR}/${name}"
install -d "$case_dir"
cp "${SCRIPT_DIR}/run.sh" "${SCRIPT_DIR}/.env.example" "$case_dir/"
printf '%s\n' "$case_dir"
}

owner=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

success_dir="$(make_case success)"
(
cd "$success_dir"
./run.sh init Buzz.Example.Com "$owner" >stdout 2>stderr
[[ ! -s stderr ]]
grep -qx 'Created deploy/compose/.env with mode 600; review and back it up before starting Buzz' stdout
[[ "$(stat -c '%a' .env)" == 600 ]]
grep -qx 'BUZZ_DOMAIN=buzz.example.com' .env
grep -qx 'RELAY_URL=wss://buzz.example.com' .env
grep -qx 'BUZZ_MEDIA_BASE_URL=https://buzz.example.com/media' .env
grep -qx "RELAY_OWNER_PUBKEY=$owner" .env
if grep -Eq '^[A-Za-z_][A-Za-z0-9_]*=.*CHANGE_ME' .env; then
printf 'init left a CHANGE_ME assignment in .env\n' >&2
exit 1
fi
grep -Eq '^BUZZ_RELAY_PRIVATE_KEY=[0-9a-f]{64}$' .env
grep -Eq '^BUZZ_GIT_HOOK_HMAC_SECRET=[0-9a-f]{64}$' .env
grep -Eq '^POSTGRES_PASSWORD=[0-9a-f]{64}$' .env
grep -Eq '^REDIS_PASSWORD=[0-9a-f]{64}$' .env
grep -Eq '^BUZZ_S3_ACCESS_KEY=buzz[0-9a-f]{24}$' .env
grep -Eq '^BUZZ_S3_SECRET_KEY=[0-9a-f]{64}$' .env

before="$(sha256sum .env)"
if ./run.sh init buzz.example.com "$owner" >/dev/null 2>overwrite.stderr; then
printf 'init overwrote an existing .env\n' >&2
exit 1
fi
grep -qx 'Refusing to overwrite deploy/compose/.env' overwrite.stderr
[[ "$(sha256sum .env)" == "$before" ]]
)

invalid_domain_dir="$(make_case invalid-domain)"
(
cd "$invalid_domain_dir"
if ./run.sh init 'https://buzz.example.com' "$owner" >/dev/null 2>stderr; then
printf 'init accepted an invalid domain\n' >&2
exit 1
fi
grep -qx 'Domain must be a valid DNS name such as buzz.example.com' stderr
[[ ! -e .env ]]
)

invalid_owner_dir="$(make_case invalid-owner)"
(
cd "$invalid_owner_dir"
if ./run.sh init buzz.example.com not-a-pubkey >/dev/null 2>stderr; then
printf 'init accepted an invalid owner public key\n' >&2
exit 1
fi
grep -qx 'Owner public key must be 64 hexadecimal characters' stderr
[[ ! -e .env ]]
)

symlink_dir="$(make_case symlink)"
(
cd "$symlink_dir"
touch target
ln -s target .env
if ./run.sh init buzz.example.com "$owner" >/dev/null 2>stderr; then
printf 'init replaced an .env symlink\n' >&2
exit 1
fi
grep -qx 'Refusing to overwrite deploy/compose/.env' stderr
[[ -L .env ]]
[[ ! -s target ]]
)

missing_env_dir="$(make_case missing-env)"
(
cd "$missing_env_dir"
if ./run.sh config >/dev/null 2>stderr; then
printf 'config accepted a missing .env\n' >&2
exit 1
fi
grep -Fqx 'Run ./run.sh init <domain> <owner-pubkey-hex>, or install .env.example as a' stderr
grep -Fqx 'mode-600 .env and replace every CHANGE_ME value manually. Do not start' stderr
)

printf 'Production Compose environment initialization checks passed\n'