diff --git a/.env.example b/.env.example index 4948af1..2a07792 100644 --- a/.env.example +++ b/.env.example @@ -5,11 +5,11 @@ # record pointing at this host before the first start (Caddy gets the # certificates over HTTP-01). tomtervel.no itself stays where it is # until the vel decides to point the apex here. -PORTAL_HOST=portal.tomtervel.no -ID_HOST=id.tomtervel.no +PORTAL_HOST=vel.klingenbergbygg.no +ID_HOST=id.vel.klingenbergbygg.no # The portal version to run: a tag of https://project.uhhm.no/uhhm/portal -PORTAL_RELEASE=v0.3.36 +PORTAL_RELEASE=v0.5.2 # The content this instance serves, and reloads live on every push. CONTENT_REPO=https://prosjekt.klingenbergbygg.no/tomtervel/questions @@ -26,3 +26,8 @@ OAUTH2_CLIENT_SECRET= # Only for the optional runner profile: a registration token from # prosjekt.klingenbergbygg.no -> tomtervel org -> Settings -> Actions -> Runners. RUNNER_REGISTRATION_TOKEN= + +# The gdo version to run: a tag of https://project.uhhm.no/uhhm/gdo, +# installed from the [uhhm] Arch registry. gdo hands the vel's mail to +# the host's mail server; on kasse that is Klingenberg Bygg's postfix. +GDO_RELEASE=v0.2.0 diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index 35223a8..5f79f6b 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -1,10 +1,13 @@ -# Deploy from this repo onto the vel's own host (kasse). There is no -# `tomtervel`-labelled runner, so this workflow queues and does nothing; -# deploy by hand on the host with: -# cd /srv/tomtervel/infrastructure && git pull --ff-only && sudo sh bootstrap.sh -# (bootstrap.sh runs `podman compose up -d --build`, rootful). Rolling out -# a new portal version is a commit that bumps PORTAL_RELEASE in .env.example -# and, on the host, in .env. +# Deploy this stack onto the host it runs on. A push that changes the +# compose file, a Dockerfile or a rendered config pulls and brings the +# stack up; a portal or gdo upgrade is a commit that bumps the version in +# .env.example and, on the host, in .env. +# +# Runs on kasse's host runner. It is not root: it may run one script, +# /usr/local/bin/deploy-tomtervel, which pulls this repo under +# /srv/tomtervel and runs `podman compose up -d --build` and nothing +# else. The .env on the host - the NATS password, the OAuth2 secret, the +# Kanidm token - is never in this repo and is not touched by a deploy. name: deploy on: push: @@ -13,6 +16,7 @@ on: - compose.yml - Caddyfile - portal/** + - gdo/** - kanidm/server.toml.tpl - nats/nats.conf.tpl - .gitea/workflows/deploy.yml @@ -20,11 +24,7 @@ on: jobs: deploy: - runs-on: tomtervel + runs-on: fish steps: - - name: Pull and restart - run: | - set -eu - cd /srv/tomtervel/infrastructure - git pull --ff-only - sh bootstrap.sh + - name: Pull and bring the stack up + run: sudo /usr/local/bin/deploy-tomtervel diff --git a/README.md b/README.md index fda565f..a9cb10a 100644 --- a/README.md +++ b/README.md @@ -111,3 +111,23 @@ content repo's workflow matched to it. Mail (a person's reset link is a token you hand them), monitoring, and the vel's current website at tomtervel.no, which stays where it is until the vel points the apex at PORTAL_HOST. + +## Mail + +Portal decides what to send - a `mail:` on a state in the content, or an +invite - and publishes it on this stack's NATS. `gdo` is what hands it to +a mail server, and it runs here, in the vel's own stack, so the vel's mail +leaves on the vel's own terms. + +It has no mail server of its own. It relays through the host's, which on +kasse is Klingenberg Bygg's postfix on port 25, reached from the container +as `host.containers.internal`. That is the one thing in this stack the vel +borrows, and the one thing that changes if the vel ever moves to a host of +its own: point `SMTP_HOST` at whatever that host runs. + +`site.yaml` in the content repo needs a `mail.from`, or portal has nothing +to send from and says so in its log. + +Replies are not read yet: `JMAP_URL`, `JMAP_TOKEN` and `MAIL_REPLY_DOMAIN` +turn a reply into a note on the record it answers, and none of them are set +here. diff --git a/compose.yml b/compose.yml index ba10441..26010b0 100644 --- a/compose.yml +++ b/compose.yml @@ -101,6 +101,33 @@ services: kanidm: condition: service_started + # The mailer. Portal decides what to send - a `mail:` on a state in the + # content, or an invite - and publishes it on this NATS; gdo is what + # actually hands it to a mail server. It is in the vel's own stack rather + # than shared, so the vel's mail leaves on the vel's own terms, but it + # has no mail server of its own: it relays through the host's, which on + # kasse is Klingenberg Bygg's postfix on port 25. That is the one thing + # here the vel borrows. + gdo: + build: + context: ./gdo + args: + GDO_RELEASE: ${GDO_RELEASE} + restart: unless-stopped + environment: + NATS_URL: nats://portal:${NATS_PASSWORD}@nats:4222 + # The host, from inside the container. Podman resolves this to the + # gateway; the host's postfix listens on 0.0.0.0:25. + SMTP_HOST: host.containers.internal + SMTP_PORT: "25" + # A strict server refuses a bare container hostname in EHLO. + SMTP_HELO: ${PORTAL_HOST} + extra_hosts: + - "host.containers.internal:host-gateway" + depends_on: + nats: + condition: service_healthy + # The Gitea Actions runner is a host service on kasse (pacman gitea-runner), # registered against prosjekt.klingenbergbygg.no. It reaches this NATS via the # published 127.0.0.1:4222 port above, so no in-compose runner — and no diff --git a/gdo/Dockerfile b/gdo/Dockerfile new file mode 100644 index 0000000..8df2ad2 --- /dev/null +++ b/gdo/Dockerfile @@ -0,0 +1,19 @@ +# gdo, the mailer, from the [uhhm] Arch registry - the same package the +# bare-metal hosts install. Arch base rather than Debian because that is +# how every uhhm binary is published; portal ships a release tarball and +# is built from that instead. +FROM archlinux:base +ARG GDO_RELEASE +RUN printf '%s\n' \ + '[uhhm]' \ + 'SigLevel = Required DatabaseOptional' \ + 'Server = https://project.uhhm.no/api/packages/bl/arch/$repo/$arch' \ + >> /etc/pacman.conf \ + && pacman -Sy --noconfirm --needed ca-certificates gdo \ + && pacman -Scc --noconfirm \ + && rm -rf /var/cache/pacman/pkg/* /var/lib/pacman/sync/* +# A non-root user; gdo writes nothing and holds no state - what it has +# not yet delivered is on the stream, not on disk. +RUN useradd --system --no-create-home gdo +USER gdo +CMD ["/usr/bin/gdo"] diff --git a/kanidm-setup.sh b/kanidm-setup.sh index e138422..ce3c922 100755 --- a/kanidm-setup.sh +++ b/kanidm-setup.sh @@ -36,6 +36,36 @@ $K system oauth2 update-claim-map-join $C groups array secret=$($K system oauth2 show-basic-secret $C 2>/dev/null | tail -1) sed -i "s|^OAUTH2_CLIENT_SECRET=.*|OAUTH2_CLIENT_SECRET=$secret|" .env portal.env + +# Onboarding from a desk. A committee inviting a neighbour, and any +# state whose `grants:` makes someone a member, both go through Kanidm +# as this service account - not as a person, and not as an admin. It is +# in idm_people_on_boarding, which may create people and issue a first +# credential reset and nothing else, so the token cannot touch anyone's +# existing credentials. idm_people_pii_read lets an invite find someone +# who already has an account by their email, and add them to the group +# instead of making a second account for the same person. +# +# The token is shown once, by Kanidm, at creation. Written straight into +# the env files here and never printed. +SA=portal-onboarding +if ! $K service-account get $SA >/dev/null 2>&1; then + $K service-account create $SA "Portal onboarding" idm_admin + $K group add-members idm_people_on_boarding $SA + $K group add-members idm_people_pii_read $SA || true +fi +if ! grep -q '^KANIDM_API_TOKEN=.' portal.env 2>/dev/null; then + token=$($K service-account api-token generate $SA "portal" --rw 2>/dev/null | tail -1) + if [ -n "$token" ]; then + grep -q '^KANIDM_API_TOKEN=' portal.env \ + && sed -i "s|^KANIDM_API_TOKEN=.*|KANIDM_API_TOKEN=$token|" portal.env \ + || printf 'KANIDM_API_TOKEN=%s\n' "$token" >> portal.env + echo "onboarding token written to portal.env" + else + echo "warning: could not generate the onboarding token - invites will fail closed until it is set" + fi +fi + podman compose restart portal echo "client $C configured; portal restarted with its secret" echo