From 88ea07c3542b6d0028d53f66fa9a36b0a23e225b Mon Sep 17 00:00:00 2001 From: Brandon Corbett Date: Thu, 22 Oct 2026 14:54:03 -0400 Subject: [PATCH] chore: bind the compose stack to loopback and pin the release supply chain Closes #473. Closes #474. --- ...ompose-loopback-and-pinned-supply-chain.md | 11 +++++++++ .github/workflows/ci.yml | 11 +++++---- .github/workflows/codeql.yml | 8 +++---- .github/workflows/conformance.yml | 2 +- .github/workflows/docker-publish.yml | 22 +++++++++--------- .github/workflows/postgres.yml | 8 +++---- .github/workflows/release.yml | 6 ++--- Dockerfile | 17 ++++++++------ README.md | 23 +++++++++++-------- docker-compose.dev.yml | 8 ++++--- docker-compose.yml | 15 ++++++++---- docs/configuration.md | 19 +++++++++++++-- resources/coverage-badge.svg | 8 +++---- 13 files changed, 100 insertions(+), 58 deletions(-) create mode 100644 .changeset/compose-loopback-and-pinned-supply-chain.md diff --git a/.changeset/compose-loopback-and-pinned-supply-chain.md b/.changeset/compose-loopback-and-pinned-supply-chain.md new file mode 100644 index 00000000..e75fbb00 --- /dev/null +++ b/.changeset/compose-loopback-and-pinned-supply-chain.md @@ -0,0 +1,11 @@ +--- +'seamless-auth-api': patch +--- + +The bundled Docker Compose stack serves on loopback only and no longer grants admin to a fixed address (#473). + +- `docker-compose.yml` publishes the API on `127.0.0.1:${API_PORT:-5312}` and no longer publishes Postgres. Docker's port publishing bypasses host firewalls such as ufw, so the old bare port exposed the development defaults to the network. `POSTGRES_PORT` no longer applies to this file; use `docker compose exec postgres psql -U seamless seamless_auth` for a shell. +- `docker-compose.dev.yml` publishes the API and Postgres on `127.0.0.1` only. +- Neither file defaults `OWNER_EMAIL` to `owner@example.com` any more. Pass your own, for example `OWNER_EMAIL=you@example.com docker compose up`; without it nobody is granted admin. An existing stack whose owner account was already created keeps its role. + +The release image now builds the admin dashboard from a pinned commit (`SEAMLESS_ADMIN_DASHBOARD_REF`, dashboard v0.10.1) and refuses a build arg that is not a full commit SHA (#474). Overriding it with a tag or branch name fails the build. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 11d7e97f..19407889 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,6 +4,9 @@ on: pull_request: branches: ['**'] +permissions: + contents: read + jobs: # A pull request only ever runs ci.yml and codeql.yml, so a mistake in # release.yml or docker-publish.yml reaches main unchallenged and is not @@ -22,7 +25,7 @@ jobs: steps: - name: Checkout repo - uses: actions/checkout@v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Install actionlint run: | @@ -51,10 +54,10 @@ jobs: steps: - name: Checkout repo - uses: actions/checkout@v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Setup Node.js - uses: actions/setup-node@v7 + uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version: ${{ matrix.node-version }} cache: 'npm' @@ -87,7 +90,7 @@ jobs: - name: Upload coverage reports to Codecov if: matrix.node-version == 24 - uses: codecov/codecov-action@v7 + uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1 with: token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 9e8a5a6e..60bc545f 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -24,10 +24,10 @@ jobs: steps: - name: Checkout repo - uses: actions/checkout@v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Initialize CodeQL - uses: github/codeql-action/init@v4 + uses: github/codeql-action/init@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1 with: languages: javascript-typescript # security-extended adds rules that are lower signal on their own but @@ -36,10 +36,10 @@ jobs: queries: security-extended - name: Autobuild - uses: github/codeql-action/autobuild@v4 + uses: github/codeql-action/autobuild@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1 - name: Perform CodeQL analysis - uses: github/codeql-action/analyze@v4 + uses: github/codeql-action/analyze@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1 with: category: /language:javascript-typescript diff --git a/.github/workflows/conformance.yml b/.github/workflows/conformance.yml index 2ce29174..3e8331b0 100644 --- a/.github/workflows/conformance.yml +++ b/.github/workflows/conformance.yml @@ -7,6 +7,6 @@ on: jobs: verify: - uses: fells-code/seamless-cli/.github/workflows/verify-conformance.yml@main + uses: fells-code/seamless-cli/.github/workflows/verify-conformance.yml@66cb013722ccfb747eb29ba3ac0bfc50a8d78b39 # v0.25.1 with: api-ref: ${{ github.event.pull_request.head.sha }} diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 0680ea47..939c47e7 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -35,15 +35,15 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: ${{ inputs.image_tag || github.ref_name }} - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v4 + uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1 - name: Log in to GitHub Container Registry - uses: docker/login-action@v4 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} @@ -51,7 +51,7 @@ jobs: - name: Extract Docker metadata id: meta - uses: docker/metadata-action@v6 + uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0 with: images: | ghcr.io/${{ env.IMAGE_NAME }} @@ -64,7 +64,7 @@ jobs: # the local daemon, which is why this is separate from the push below. # Buildx caches the layers, so the second build is cheap. - name: Build image for scanning - uses: docker/build-push-action@v7 + uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0 with: context: . push: false @@ -73,7 +73,7 @@ jobs: platforms: linux/amd64 - name: Scan image for vulnerabilities - uses: aquasecurity/trivy-action@v0.36.0 + uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 with: image-ref: ${{ env.IMAGE_NAME }}:scan format: table @@ -96,7 +96,7 @@ jobs: # Runs even when the gate above failed, so the findings that blocked the # release are visible rather than only in the job log. if: always() - uses: aquasecurity/trivy-action@v0.36.0 + uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 with: image-ref: ${{ env.IMAGE_NAME }}:scan format: sarif @@ -107,14 +107,14 @@ jobs: - name: Upload scan findings if: always() - uses: github/codeql-action/upload-sarif@v4 + uses: github/codeql-action/upload-sarif@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1 with: sarif_file: trivy-results.sarif category: trivy-image - name: Build and push Docker image id: push - uses: docker/build-push-action@v7 + uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0 with: context: . push: true @@ -129,7 +129,7 @@ jobs: provenance: mode=max - name: Install cosign - uses: sigstore/cosign-installer@v3 + uses: sigstore/cosign-installer@398d4b0eeef1380460a10c8013a76f728fb906ac # v3.9.1 - name: Sign the published image env: @@ -152,7 +152,7 @@ jobs: echo echo '```bash' echo "cosign verify ghcr.io/${IMAGE_NAME}@${DIGEST} \\" - echo " --certificate-identity-regexp '^https://github.com/${GITHUB_REPOSITORY}/' \\" + echo " --certificate-identity-regexp '^https://github\\.com/${GITHUB_REPOSITORY}/\\.github/workflows/docker-publish\\.yml@refs/(heads/main|tags/v.*)\$' \\" echo " --certificate-oidc-issuer https://token.actions.githubusercontent.com" echo '```' echo diff --git a/.github/workflows/postgres.yml b/.github/workflows/postgres.yml index 8afb7427..470efbe7 100644 --- a/.github/workflows/postgres.yml +++ b/.github/workflows/postgres.yml @@ -72,10 +72,10 @@ jobs: steps: - name: Checkout repo - uses: actions/checkout@v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Setup Node.js - uses: actions/setup-node@v7 + uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version-file: '.nvmrc' cache: 'npm' @@ -111,12 +111,12 @@ jobs: steps: - name: Checkout repo - uses: actions/checkout@v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 - name: Setup Node.js - uses: actions/setup-node@v7 + uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version-file: '.nvmrc' cache: 'npm' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 850e8bb4..c93f1ee0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -27,12 +27,12 @@ jobs: steps: - name: Checkout repo - uses: actions/checkout@v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 - name: Setup Node.js - uses: actions/setup-node@v7 + uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version-file: '.nvmrc' cache: npm @@ -57,7 +57,7 @@ jobs: - name: Create or update version and changelog PR id: changesets - uses: changesets/action@v2 + uses: changesets/action@ae32849d5ba541f9ae29e40e22a623bc13562f51 # v2.1.2 with: # v2 renamed every one of these. Unknown inputs are ignored rather than # rejected, so the old names would have left the version PR titled diff --git a/Dockerfile b/Dockerfile index 7d6b170c..8917deba 100644 --- a/Dockerfile +++ b/Dockerfile @@ -19,23 +19,26 @@ RUN npm prune --omit=dev # (`build:console` sets VITE_BASE_PATH=/console/ and VITE_SAME_ORIGIN=true, so assets, the # React Router basename, and the origin-derived API base all agree). This API then serves the # result at /console. The dashboard repo is public, but its npm package is not published -# (package.json is marked private), so it is fetched from git. Bump -# SEAMLESS_ADMIN_DASHBOARD_REF to a dashboard release tag to ship a new dashboard on the next -# auth-image release. Never point a release build at a floating branch. +# (package.json is marked private), so it is fetched from git. To ship a new dashboard on the +# next auth-image release, set SEAMLESS_ADMIN_DASHBOARD_REF to the full commit SHA of a +# dashboard release tag and update the tag in the comment above it. A tag can be moved, so the +# build refuses anything but a full SHA and checks HEAD against it after checkout. FROM node:24-slim AS admin-dashboard WORKDIR /dashboard ARG SEAMLESS_ADMIN_DASHBOARD_REPO=https://github.com/fells-code/seamless-auth-admin-dashboard.git -# Pinned dashboard release tag. The same-origin /console build has shipped since v0.2.0. -ARG SEAMLESS_ADMIN_DASHBOARD_REF=v0.10.1 +# Dashboard release v0.10.1. The same-origin /console build has shipped since v0.2.0. +ARG SEAMLESS_ADMIN_DASHBOARD_REF=e16c1e84e6a105fb7cadc328570a2fbc469f463d RUN apt-get update && \ apt-get install -y git python3 make g++ && \ rm -rf /var/lib/apt/lists/* -# Full clone + checkout so the ref can be a tag, branch, or commit SHA. +RUN echo "${SEAMLESS_ADMIN_DASHBOARD_REF}" | grep -Eqx '[0-9a-f]{40}' || \ + { echo "SEAMLESS_ADMIN_DASHBOARD_REF must be a full commit SHA" >&2; exit 1; } RUN git clone "${SEAMLESS_ADMIN_DASHBOARD_REPO}" . && \ - git checkout "${SEAMLESS_ADMIN_DASHBOARD_REF}" + git -c advice.detachedHead=false checkout --detach "${SEAMLESS_ADMIN_DASHBOARD_REF}" && \ + test "$(git rev-parse HEAD)" = "${SEAMLESS_ADMIN_DASHBOARD_REF}" RUN npm ci RUN npm run build:console diff --git a/README.md b/README.md index 27c4026d..00d9261f 100644 --- a/README.md +++ b/README.md @@ -335,29 +335,32 @@ development defaults, so there is nothing to configure first: no `.env` file, no no separate Postgres. ```bash -docker compose up +OWNER_EMAIL=you@example.com docker compose up ``` That starts the API on `http://localhost:5312`, runs migrations on first boot, generates a development signing keypair (kept on the `dev-keys` volume, so it survives a recreate), and serves the admin console at `http://localhost:5312/console`. +The API is published on `127.0.0.1` only, so it is not reachable from other machines on the +network, and Postgres is not published at all: only the API container reaches it. Docker's port +publishing bypasses host firewalls such as ufw, which is why the bind address is set in the compose +file rather than left to the host. For a database shell, use +`docker compose exec postgres psql -U seamless seamless_auth`. + Verify it: ```bash curl http://localhost:5312/health/status ``` -To get an admin account, sign up at `/console` with `owner@example.com`. That address is the compose -file's `OWNER_EMAIL`, so the account is granted `admin:write` on creation (or `admin` on an instance -whose `AVAILABLE_ROLES` lists only that). -Override it before first boot to use your own: - -```bash -OWNER_EMAIL=you@example.com docker compose up -``` +To get an admin account, sign up at `/console` with the address you passed as `OWNER_EMAIL`. That +account is granted `admin:write` on creation (or `admin` on an instance whose `AVAILABLE_ROLES` +lists only that). `OWNER_EMAIL` has no default: without it the stack still starts, but nobody is +granted admin. The grant happens only when the account is created, so set it before that account +signs up. -Other values worth overriding are `API_PORT`, `POSTGRES_PORT`, `APP_ORIGINS`, `ORIGINS`, and +Other values worth overriding are `API_PORT`, `APP_ORIGINS`, `ORIGINS`, and `SEAMLESS_AUTH_IMAGE` (to pin a specific published tag). Stop the stack with `docker compose down`, or `docker compose down -v` to also discard the database. diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 1d2291fb..76181050 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -15,8 +15,9 @@ services: build: context: . dockerfile: Dockerfile.dev + # Loopback only, as in docker-compose.yml: Docker publishes ports ahead of host firewalls. ports: - - '${API_PORT:-5312}:5312' + - '127.0.0.1:${API_PORT:-5312}:5312' volumes: - .:/app # node_modules stays out of the bind mount: the host tree is built for the host's @@ -67,7 +68,7 @@ services: SEAMLESS_SERVICE_TOKEN: ${SEAMLESS_SERVICE_TOKEN:-${API_SERVICE_TOKEN:-local-development-service-token-not-for-deployment}} ALLOW_UNCREDENTIALED_DELIVERY_SECRETS: 'true' - OWNER_EMAIL: ${OWNER_EMAIL:-owner@example.com} + OWNER_EMAIL: ${OWNER_EMAIL:-} SERVE_ADMIN_DASHBOARD: 'false' env_file: - path: .env @@ -76,8 +77,9 @@ services: postgres: image: postgres:18-alpine + # Published on loopback so the suite can run against it with TEST_DB=postgres. ports: - - '${POSTGRES_PORT:-5432}:5432' + - '127.0.0.1:${POSTGRES_PORT:-5432}:5432' environment: POSTGRES_USER: seamless POSTGRES_PASSWORD: seamless diff --git a/docker-compose.yml b/docker-compose.yml index 005bcdf8..03a2e189 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,6 +3,7 @@ # Brings up Postgres and the published Seamless Auth API image with development # defaults, so no .env file and no generated secrets are needed to try the project. # The admin console is served by the same container at http://localhost:5312/console. +# The API listens on 127.0.0.1 only and Postgres is not published. # # Every value below is a LOCAL DEVELOPMENT default and is not safe for a deployed # instance. See docs/configuration.md for the real thing, and @@ -16,8 +17,10 @@ name: seamless-auth services: api: image: ${SEAMLESS_AUTH_IMAGE:-ghcr.io/fells-code/seamless-auth-api:latest} + # Loopback only. Docker publishes ports ahead of host firewalls such as ufw, so a bare + # port here would expose these development defaults to the network. ports: - - '${API_PORT:-5312}:5312' + - '127.0.0.1:${API_PORT:-5312}:5312' depends_on: postgres: condition: service_healthy @@ -67,8 +70,10 @@ services: ALLOW_UNCREDENTIALED_DELIVERY_SECRETS: 'true' # The first account created with this email is granted the admin role, so the - # console at /console has a working admin as soon as you sign up. - OWNER_EMAIL: ${OWNER_EMAIL:-owner@example.com} + # console at /console has a working admin as soon as you sign up. No default: unset, + # nobody is granted admin. Set it before that account signs up, for example + # `OWNER_EMAIL=you@example.com docker compose up`. + OWNER_EMAIL: ${OWNER_EMAIL:-} SERVE_ADMIN_DASHBOARD: 'true' @@ -91,8 +96,8 @@ services: postgres: image: postgres:18-alpine - ports: - - '${POSTGRES_PORT:-5432}:5432' + # Not published: only the api service reaches it, over the compose network. Use + # `docker compose exec postgres psql -U seamless seamless_auth` for a shell. environment: POSTGRES_USER: seamless POSTGRES_PASSWORD: seamless diff --git a/docs/configuration.md b/docs/configuration.md index 30312f59..02f26c57 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -239,8 +239,13 @@ sets `VITE_BASE_PATH=/console/` and `VITE_SAME_ORIGIN=true` so the assets, the r and the origin-derived API base all agree) in a dedicated stage (see the `admin-dashboard` stage in [`Dockerfile`](../Dockerfile)). Bump the dashboard shipped to tenants by raising the `SEAMLESS_ADMIN_DASHBOARD_REF` build ARG; the new version then flows to tenants through the normal -upstream auth-image release. The ref is pinned to the dashboard release tag `v0.10.1`; -`v0.2.0` was the first release to ship the same-origin `/console` build. +upstream auth-image release. The ref is the full commit SHA of the dashboard release tag +`v0.10.1` (`e16c1e84e6a105fb7cadc328570a2fbc469f463d`), because a tag can be moved and a SHA +cannot. The build refuses a ref that is not a full SHA and checks the checked-out commit against +it. To bump, resolve the new tag with +`git ls-remote https://github.com/fells-code/seamless-auth-admin-dashboard refs/tags/` +(use the `^{}` line for an annotated tag) and update the tag named in the comment above the +ARG. `v0.2.0` was the first release to ship the same-origin `/console` build. Because the same-origin build derives its API base from the page origin **and still speaks the server-adapter contract** (it calls `/auth/...` with `credentials: include`), the origin @@ -375,6 +380,16 @@ start in development mode when `ISSUER` or any `APP_ORIGINS` entry is reachable names each one. Local means `localhost`, `*.localhost`, `*.test`, a single-label name such as a compose service, a loopback or private address. +That check reads configuration, not the address the server listens on, so it cannot tell when a +machine left on these localhost defaults is reachable from the network. The bundled +`docker-compose.yml` and `docker-compose.dev.yml` therefore publish the API on `127.0.0.1` only, +because Docker's port publishing bypasses host firewalls such as ufw. `docker-compose.yml` does not +publish Postgres at all; `docker-compose.dev.yml` publishes it on `127.0.0.1` for running the suite +with `TEST_DB=postgres`. Neither sets a default `OWNER_EMAIL`, so no fixed address is granted +admin. To reach the stack from another machine, put a TLS proxy in front of it and move to a +production configuration ([docs/production-operations.md](./production-operations.md)) rather +than widening the bind address. + | Variable | Required | Default | Notes | | ----------------------------- | -------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `SEAMLESS_ALLOW_INSECURE_DEV` | No | `false` | Start in development mode on a non-local deployment anyway, for a shared staging box. Logs a warning and reports `insecureDevelopmentMode` on `/health/status`. | diff --git a/resources/coverage-badge.svg b/resources/coverage-badge.svg index c931b02b..322cf29d 100644 --- a/resources/coverage-badge.svg +++ b/resources/coverage-badge.svg @@ -1,5 +1,5 @@ - - coverage: 99.2% + + coverage: 99.1% @@ -17,7 +17,7 @@ coverage coverage - 99.2% - 99.2% + 99.1% + 99.1%