diff --git a/.env.example b/.env.example index b07330f..f73abdb 100644 --- a/.env.example +++ b/.env.example @@ -1,17 +1,24 @@ # ── Required ────────────────────────────────────────────── # fmsg domain name this host serves +# - fmsgd will uses TCP 4930 on fmsg. +# - fmsg-webapi uses HTTPS 443 on fmsgapi. FMSG_DOMAIN=example.com +# Email address for Let's Encrypt certificate registration +CERTBOT_EMAIL= + +# HMAC secret used to validate JWT tokens for fmsg-webapi +# Prefix with base64: to supply a base64-encoded key (e.g. base64:c2VjcmV0) +FMSG_API_JWT_SECRET=changeme + # Per-service database passwords (used by application services) FMSGD_WRITER_PGPASSWORD=changeme FMSGID_WRITER_PGPASSWORD=changeme # ── Optional (defaults shown) ──────────────────────────── -# GIT_SSL_NO_VERIFY=false # FMSG_PORT=4930 # FMSGID_PORT=8080 # GIN_MODE=release # FMSG_SKIP_DOMAIN_IP_CHECK=false -# PGUSER=postgres \ No newline at end of file diff --git a/QUICKSTART.md b/QUICKSTART.md new file mode 100644 index 0000000..273c1eb --- /dev/null +++ b/QUICKSTART.md @@ -0,0 +1,98 @@ +# Quickstart - Setting up an fmsg host with fmsg-docker + +This quickstart gets the docker compose stack from this repository up and running on your server. TLS provisioning is included and an HTTPS API is exposed so you can start sending and receiving fmsg messages for your domain. TCP port 4930 is also exposed for fmsg host-to-host communication. + +To learn more about fmsg, see the documentation repository: (fmsg)[https://github.com/markmnl/fmsg]. + +Read the (README.md)[https://github.com/markmnl/fmsg-docker] of this repo for more about settings and environment being used in this quickstart. + +## Requirements + +1. A domain you control, e.g `example.com` +2. A server with a public IP and + 1. TCP port `4930` open to the internet (fmsg TLS) + 2. TCP port `443` open to the internet (fmsg-webapi HTTPS) + 3. TCP port `80` open to the internet (only first start - required for initial Let's Encrypt certificate issuance) +3. Docker and Docker Compose + +## Steps + +### 0. Server Setup + +Clone this repository to the server and make sure docker is running. +``` +git clone https://github.com/markmnl/fmsg-docker.git +``` + +### 1. Configure DNS + +Create A (or AAAA if your public IP is IPv6) DNS records to resolve to your server IP for: + +1. `fmsg.` +2. `fmsgapi.` + +_NOTE_ Ensure DNS is kept up-to-date with your server's IP so you can send and receive messages! + +### 2. Configure FMSG + +Copy the example env file: + +```sh +cp .env.example compose/.env +``` + +Edit `compose/.env` and set at least: + +```env +FMSG_DOMAIN=example.com +CERTBOT_EMAIL= +FMSG_API_JWT_SECRET= +FMSGD_WRITER_PGPASSWORD= +FMSGID_WRITER_PGPASSWORD= +``` + +_NOTE_ +* FMSG_DOMAIN is the domain part of fmsg addresses e.g. in `@user@example.com` would be `example.com`. This server you are setting up is located at the subdomain `fmsg.` but addresses will be at ``, you should only specify `` for FMSG_DOMAIN here. +* CERTBOT_EMAIL is an email address supplied to [Let's Encrypt](https://letsencrypt.org/) for e.g. TLS expiry warnings. +* For all secrets and passwords env vars create your own. + +Start the stack for the first time from `compose/` and pass the one-time init passwords on the command line (keep these secret, keep them safe): + +(might require sudo) + +```sh +cd compose +PGPASSWORD= \ +FMSGD_READER_PGPASSWORD= \ +FMSGID_READER_PGPASSWORD= \ +docker compose up -d +``` + +If `fmsgd` is running and port `4930` is reachable on `fmsg.`, the host is up. + +On first start, certbot will request Let's Encrypt TLS certificates for `fmsg.` and `fmsgapi.`. If certificate issuance fails (e.g. the domains do not resolve to the server or port 80 is blocked), the stack will not start. Certificates are persisted in a Docker volume and reused on subsequent starts. Once certificates are issued port 80 is no longer needed until certificates need to be renewed - usually 90 days. + + +## Next Steps + +### Add Users + +Create users (message stores, analoguous to mailboxes) by placing a CSV file in the `fmsgid_data` volume at `/opt/fmsgid/data/addresses.csv`. The format is: + +```csv +address,display_name,accepting_new,limit_recv_size_total,limit_recv_size_per_msg,limit_recv_size_per_1d,limit_recv_count_per_1d,limit_send_size_total,limit_send_size_per_msg,limit_send_size_per_1d,limit_send_count_per_1d +@alice@example.com,Alice,true,102400000,10240,102400,1000,102400000,10240,102400,1000 +``` + +You can copy it into the volume with: + +```sh +docker compose cp addresses.csv fmsgid:/opt/fmsgid/data/addresses.csv +docker compose restart fmsgid +``` + +### Connect a Client + +* Connect a client such as [fmsg-cli](https://github.com/markmnl/fmsg-cli) to `fmsgapi.` configured with your `FMSG_API_JWT_SECRET` to send and retrieve messages. + +_NOTE_ Anyone with `FMSG_API_JWT_SECRET` can mint tokens for your `fmsgapi.` for any user e.g. `@alice@`. \ No newline at end of file diff --git a/compose/docker-compose.yml b/compose/docker-compose.yml index ea1bf31..9cd0243 100644 --- a/compose/docker-compose.yml +++ b/compose/docker-compose.yml @@ -1,5 +1,18 @@ services: + certbot: + image: certbot/certbot + restart: "no" + entrypoint: ["/bin/sh", "/entrypoint.sh"] + environment: + FMSG_DOMAIN: ${FMSG_DOMAIN} + CERTBOT_EMAIL: ${CERTBOT_EMAIL} + volumes: + - letsencrypt:/etc/letsencrypt + - ../docker/certbot/entrypoint.sh:/entrypoint.sh:ro + ports: + - "80:80" + postgres: image: postgres:18-alpine restart: unless-stopped @@ -26,18 +39,20 @@ services: dockerfile: Dockerfile args: FMSGID_REF: ${FMSGID_REF:-main} - GIT_SSL_NO_VERIFY: ${GIT_SSL_NO_VERIFY:-} CACHEBUST: ${CACHEBUST:-} restart: unless-stopped environment: GIN_MODE: ${GIN_MODE:-release} FMSGID_PORT: ${FMSGID_PORT:-8080} + FMSGID_CSV_FILE: /opt/fmsgid/data/addresses.csv PGHOST: postgres PGPORT: 5432 PGDATABASE: fmsgid PGUSER: fmsgid_writer PGPASSWORD: ${FMSGID_WRITER_PGPASSWORD} PGSSLMODE: disable + volumes: + - fmsgid_data:/opt/fmsgid/data depends_on: postgres: condition: service_healthy @@ -48,7 +63,6 @@ services: dockerfile: Dockerfile args: FMSGD_REF: ${FMSGD_REF:-main} - GIT_SSL_NO_VERIFY: ${GIT_SSL_NO_VERIFY:-} CACHEBUST: ${CACHEBUST:-} restart: unless-stopped environment: @@ -57,8 +71,8 @@ services: FMSG_ID_URL: http://fmsgid:${FMSGID_PORT:-8080} FMSG_SKIP_DOMAIN_IP_CHECK: ${FMSG_SKIP_DOMAIN_IP_CHECK:-false} FMSG_SKIP_AUTHORISED_IPS: ${FMSG_SKIP_AUTHORISED_IPS:-false} - FMSG_TLS_CERT: ${FMSG_TLS_CERT:-} - FMSG_TLS_KEY: ${FMSG_TLS_KEY:-} + FMSG_TLS_CERT: /etc/letsencrypt/live/fmsg.${FMSG_DOMAIN}/fullchain.pem + FMSG_TLS_KEY: /etc/letsencrypt/live/fmsg.${FMSG_DOMAIN}/privkey.pem PGHOST: postgres PGPORT: 5432 PGDATABASE: fmsgd @@ -67,9 +81,12 @@ services: PGSSLMODE: disable volumes: - fmsg_data:/opt/fmsg/data + - letsencrypt:/etc/letsencrypt:ro ports: - "${FMSG_PORT:-4930}:4930" depends_on: + certbot: + condition: service_completed_successfully postgres: condition: service_healthy fmsgid: @@ -81,7 +98,6 @@ services: dockerfile: Dockerfile args: FMSG_WEBAPI_REF: ${FMSG_WEBAPI_REF:-main} - GIT_SSL_NO_VERIFY: ${GIT_SSL_NO_VERIFY:-} CACHEBUST: ${CACHEBUST:-} restart: unless-stopped environment: @@ -94,14 +110,25 @@ services: PGDATABASE: fmsgd PGUSER: fmsgd_writer PGPASSWORD: ${FMSGD_WRITER_PGPASSWORD} + FMSG_TLS_CERT: /etc/letsencrypt/live/fmsgapi.${FMSG_DOMAIN}/fullchain.pem + FMSG_TLS_KEY: /etc/letsencrypt/live/fmsgapi.${FMSG_DOMAIN}/privkey.pem FMSG_DATA_DIR: /opt/fmsg/data PGSSLMODE: disable volumes: - fmsg_data:/opt/fmsg/data + - letsencrypt:/etc/letsencrypt:ro + ports: + - "443:443" depends_on: - - fmsgd - - fmsgid + certbot: + condition: service_completed_successfully + fmsgd: + condition: service_started + fmsgid: + condition: service_started volumes: postgres_data: fmsg_data: + fmsgid_data: + letsencrypt: diff --git a/docker/certbot/entrypoint.sh b/docker/certbot/entrypoint.sh new file mode 100644 index 0000000..67ac919 --- /dev/null +++ b/docker/certbot/entrypoint.sh @@ -0,0 +1,44 @@ +#!/bin/sh +set -e + +: "${FMSG_DOMAIN:?FMSG_DOMAIN is required}" +: "${CERTBOT_EMAIL:?CERTBOT_EMAIL is required}" + +FMSGD_DOMAIN="fmsg.${FMSG_DOMAIN}" +WEBAPI_DOMAIN="fmsgapi.${FMSG_DOMAIN}" + +# Skip issuance if both certificates already exist +if [ -d "/etc/letsencrypt/live/${FMSGD_DOMAIN}" ] && \ + [ -d "/etc/letsencrypt/live/${WEBAPI_DOMAIN}" ]; then + echo "Certificates for ${FMSGD_DOMAIN} and ${WEBAPI_DOMAIN} already exist, skipping." + exit 0 +fi + +echo "Requesting certificate for ${FMSGD_DOMAIN} ..." +certbot certonly \ + --standalone \ + --non-interactive \ + --agree-tos \ + --email "${CERTBOT_EMAIL}" \ + -d "${FMSGD_DOMAIN}" + +echo "Requesting certificate for ${WEBAPI_DOMAIN} ..." +certbot certonly \ + --standalone \ + --non-interactive \ + --agree-tos \ + --email "${CERTBOT_EMAIL}" \ + -d "${WEBAPI_DOMAIN}" + +# certbot creates private keys as root:root 0600. The application +# containers run as an unprivileged user so the keys must be readable. +chmod 0644 "/etc/letsencrypt/live/${FMSGD_DOMAIN}/privkey.pem" \ + "/etc/letsencrypt/live/${WEBAPI_DOMAIN}/privkey.pem" +chmod 0755 /etc/letsencrypt/live \ + /etc/letsencrypt/archive \ + "/etc/letsencrypt/live/${FMSGD_DOMAIN}" \ + "/etc/letsencrypt/live/${WEBAPI_DOMAIN}" \ + "/etc/letsencrypt/archive/${FMSGD_DOMAIN}" \ + "/etc/letsencrypt/archive/${WEBAPI_DOMAIN}" + +echo "Certificates issued successfully." diff --git a/docker/fmsg-webapi/Dockerfile b/docker/fmsg-webapi/Dockerfile index 720ba20..0b44def 100644 --- a/docker/fmsg-webapi/Dockerfile +++ b/docker/fmsg-webapi/Dockerfile @@ -1,16 +1,11 @@ FROM golang:1.25 AS builder ARG FMSG_WEBAPI_REF=main -ARG GIT_SSL_NO_VERIFY= ARG CACHEBUST WORKDIR /build -RUN if [ "$GIT_SSL_NO_VERIFY" = "true" ]; then \ - git config --global http.sslVerify false; \ - export GOINSECURE='*' GONOSUMDB='*' GONOSUMCHECK='*' GOPROXY=direct; \ - fi && \ - git clone --branch "$FMSG_WEBAPI_REF" --depth 1 https://github.com/markmnl/fmsg-webapi.git . && \ +RUN git clone --branch "$FMSG_WEBAPI_REF" --depth 1 https://github.com/markmnl/fmsg-webapi.git . && \ cd src && \ go build -o fmsg-webapi . diff --git a/docker/fmsgd/Dockerfile b/docker/fmsgd/Dockerfile index cfe592d..2798d3e 100644 --- a/docker/fmsgd/Dockerfile +++ b/docker/fmsgd/Dockerfile @@ -1,21 +1,19 @@ FROM golang:1.25 AS builder ARG FMSGD_REF=main -ARG GIT_SSL_NO_VERIFY= ARG CACHEBUST WORKDIR /build -RUN if [ "$GIT_SSL_NO_VERIFY" = "true" ]; then \ - git config --global http.sslVerify false; \ - export GOINSECURE='*' GONOSUMDB='*' GONOSUMCHECK='*' GOPROXY=direct; \ - fi && \ - git clone --branch "$FMSGD_REF" --depth 1 https://github.com/markmnl/fmsgd.git . && \ +RUN git clone --branch "$FMSGD_REF" --depth 1 https://github.com/markmnl/fmsgd.git . && \ cd src && \ go build -o fmsgd . FROM debian:bookworm-slim +RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates && \ + rm -rf /var/lib/apt/lists/* + RUN useradd -r -s /bin/false fmsg WORKDIR /opt/fmsgd diff --git a/docker/fmsgid/Dockerfile b/docker/fmsgid/Dockerfile index 9d13ea6..89fb92c 100644 --- a/docker/fmsgid/Dockerfile +++ b/docker/fmsgid/Dockerfile @@ -1,16 +1,11 @@ FROM golang:1.25 AS builder ARG FMSGID_REF=main -ARG GIT_SSL_NO_VERIFY= ARG CACHEBUST WORKDIR /build -RUN if [ "$GIT_SSL_NO_VERIFY" = "true" ]; then \ - git config --global http.sslVerify false; \ - export GOINSECURE='*' GONOSUMDB='*' GONOSUMCHECK='*' GOPROXY=direct; \ - fi && \ - git clone --branch "$FMSGID_REF" --depth 1 https://github.com/markmnl/fmsgid.git . && \ +RUN git clone --branch "$FMSGID_REF" --depth 1 https://github.com/markmnl/fmsgid.git . && \ cd src && \ go build -o fmsgid . diff --git a/test/docker-compose.test.yml b/test/docker-compose.test.yml index a6eac9b..a1a8c9f 100644 --- a/test/docker-compose.test.yml +++ b/test/docker-compose.test.yml @@ -11,6 +11,12 @@ services: + certbot: + entrypoint: ["true"] + restart: "no" + ports: !override [] + profiles: ["certbot"] + fmsgd: environment: FMSG_TLS_CERT: /opt/fmsg/tls/fmsg.${FMSG_DOMAIN}.crt @@ -18,6 +24,11 @@ services: FMSG_TLS_INSECURE_SKIP_VERIFY: "true" volumes: - ../test/.tls:/opt/fmsg/tls:ro + depends_on: !override + postgres: + condition: service_healthy + fmsgid: + condition: service_started networks: default: fmsg-test: @@ -26,7 +37,15 @@ services: - fmsg.${FMSG_DOMAIN} fmsg-webapi: - ports: + environment: + FMSG_TLS_CERT: "" + FMSG_TLS_KEY: "" + depends_on: !override + fmsgd: + condition: service_started + fmsgid: + condition: service_started + ports: !override - "${FMSG_WEBAPI_HOST_PORT:-8081}:${FMSG_API_PORT:-8000}" networks: diff --git a/test/run-tests.sh b/test/run-tests.sh index 4364ebd..d01a15f 100755 --- a/test/run-tests.sh +++ b/test/run-tests.sh @@ -89,9 +89,6 @@ export FMSGID_REF=${FMSGID_REF:-main} export FMSG_WEBAPI_REF=${FMSG_WEBAPI_REF:-main} FMSG_CLI_REF=${FMSG_CLI_REF:-main} -# ── Pass through optional SSL verification override ────────── -export GIT_SSL_NO_VERIFY=${GIT_SSL_NO_VERIFY:-} - # ── Ensure Go is on PATH ────────────────────────────────────── if ! command -v go &>/dev/null && [ -x /usr/local/go/bin/go ]; then export PATH="/usr/local/go/bin:$PATH" @@ -114,14 +111,8 @@ fi if [ "$NEED_BUILD_CLI" = "true" ]; then echo "==> Building fmsg CLI (ref: $FMSG_CLI_REF)..." FMSG_CLI_DIR=$(mktemp -d) - if [ "$GIT_SSL_NO_VERIFY" = "true" ]; then git config --global http.sslVerify false; fi git clone --branch "$FMSG_CLI_REF" --depth 1 https://github.com/markmnl/fmsg-cli.git "$FMSG_CLI_DIR" - if [ "$GIT_SSL_NO_VERIFY" = "true" ]; then - GOINSECURE='*' GONOSUMDB='*' GONOSUMCHECK='*' GOPROXY=direct \ - bash -c "cd \"$FMSG_CLI_DIR\" && go build -o \"$FMSG_BIN\" ." - else - (cd "$FMSG_CLI_DIR" && go build -o "$FMSG_BIN" .) - fi + (cd "$FMSG_CLI_DIR" && go build -o "$FMSG_BIN" .) rm -rf "$FMSG_CLI_DIR" echo "$FMSG_CLI_REF" > "$FMSG_BIN_REF_FILE" else @@ -148,7 +139,7 @@ if [ "$SKIP_START" != "true" ]; then -keyout "$TLS_DIR/fmsg.${domain}.key" \ -out "$TLS_DIR/fmsg.${domain}.crt" \ -days 1 -nodes \ - -subj "/CN=fmsg.${domain}" \ + -subj "//CN=fmsg.${domain}" \ -addext "subjectAltName=DNS:fmsg.${domain}" done chmod 644 "$TLS_DIR"/*.key