Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
34aa67f
Generate local secrets and keep them out of commits
ABilenduke Oct 8, 2026
4143fab
Create a name-constrained local CA for the platform hosts
ABilenduke Oct 8, 2026
825b41b
Run the infrastructure services behind a local TLS proxy
ABilenduke Oct 8, 2026
3bd5650
Check the running platform end to end with make smoke
ABilenduke Oct 8, 2026
8e303f8
Trust the platform's root where each OS's browsers look
ABilenduke Oct 8, 2026
42e1aa5
Add the local addresses to the hosts file with make hosts
ABilenduke Oct 8, 2026
402c7a4
Add make bootstrap, make status and make doctor
ABilenduke Oct 8, 2026
a6b8136
Lint every file and run the platform in CI
ABilenduke Oct 8, 2026
6ce422d
Record the platform stack and document getting started
ABilenduke Oct 8, 2026
d2fe6d5
Refuse a swapped hosts file and fix wrong lines in the block
ABilenduke Oct 8, 2026
ebde64e
Add the root from memory and check real trust on each system
ABilenduke Oct 8, 2026
d8a5fc0
Limit the root to TLS server certificates and cap its name
ABilenduke Oct 8, 2026
961d695
Harden the proxy's forwarded headers and timeouts
ABilenduke Oct 8, 2026
5a65a0f
Tighten service health and resources
ABilenduke Oct 8, 2026
2c5dd5f
Close gaps in keeping secrets out of commits
ABilenduke Oct 8, 2026
185ffbf
Let make logs and make status work with an incomplete .env
ABilenduke Oct 8, 2026
459ae1c
Check AMQP, the messages API and Mailpit's WebSocket in smoke
ABilenduke Oct 8, 2026
37fe179
Fail doctor when the containers can't read the checkout
ABilenduke Oct 8, 2026
88940b7
Keep CodeQL's token off disk and exercise make on macOS
ABilenduke Oct 8, 2026
c118505
Record the review's changes in ADR 0001
ABilenduke Oct 8, 2026
1260827
Require a Valkey password and keep data services off the edge
ABilenduke Oct 8, 2026
845df9f
Skip the S/MIME refusal where LibreSSL can't show it
ABilenduke Oct 8, 2026
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
14 changes: 14 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# The keys `make bootstrap` writes to .env (scripts/gen-env.sh). You don't copy this file: bootstrap
# creates .env with random development values and keeps them on every re-run. Never put a real secret
# in .env.

# Generated: local development credentials for the infrastructure services.
POSTGRES_PASSWORD=
RABBITMQ_PASSWORD=
MEILI_MASTER_KEY=
VALKEY_PASSWORD=

# Optional, never generated: your own Authorize.Net sandbox credentials. Leave them empty to run the
# platform without payments.
AUTHORIZE_NET_API_LOGIN_ID=
AUTHORIZE_NET_TRANSACTION_KEY=
1 change: 1 addition & 0 deletions .githooks/commit-msg
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#!/bin/sh
# Scans the commit message for secrets.
# shellcheck source=.githooks/require-git-secrets.sh
. "$(dirname "$0")/require-git-secrets.sh"
exec git secrets --commit_msg_hook -- "$@"
1 change: 1 addition & 0 deletions .githooks/pre-commit
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#!/bin/sh
# Scans the staged files for secrets.
# shellcheck source=.githooks/require-git-secrets.sh
. "$(dirname "$0")/require-git-secrets.sh"
exec git secrets --pre_commit_hook -- "$@"
1 change: 1 addition & 0 deletions .githooks/prepare-commit-msg
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#!/bin/sh
# Scans the commits a merge brings in for secrets.
# shellcheck source=.githooks/require-git-secrets.sh
. "$(dirname "$0")/require-git-secrets.sh"
exec git secrets --prepare_commit_msg_hook -- "$@"
1 change: 1 addition & 0 deletions .githooks/require-git-secrets.sh
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# shellcheck shell=sh
# Sourced by each hook. Stops the commit rather than letting it through unscanned.
if ! command -v git-secrets >/dev/null 2>&1; then
echo "git-secrets is not installed: https://github.com/awslabs/git-secrets#installing-git-secrets" >&2
Expand Down
5 changes: 5 additions & 0 deletions .githooks/setup
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,10 @@ git secrets --register-aws >/dev/null
prohibit 'BEGIN[A-Z[:blank:]]*PRIVATE[[:blank:]]KEY' # PEM and OpenSSH private keys
prohibit 'gh[pousr]_[A-Za-z0-9]{36,}' # GitHub tokens
prohibit 'github_pat_[A-Za-z0-9_]{22,}' # GitHub fine-grained tokens
prohibit 'rsldev_[0-9a-f]{32,}' # Values that scripts/gen-env.sh generates
# Filled credentials as KEY=value or KEY: value, quoted or not. Empty values, ${...} references and short
# placeholders (the Makefile's "unused") pass.
prohibit "(POSTGRES_PASSWORD|RABBITMQ_PASSWORD|MEILI_MASTER_KEY|VALKEY_PASSWORD)[[:blank:]]*[=:][[:blank:]]*[\"']?[^\$[:space:]\"']{8,}"
prohibit "AUTHORIZE_NET_(API_LOGIN_ID|TRANSACTION_KEY|SIGNATURE_KEY)[[:blank:]]*[=:][[:blank:]]*[\"']?[A-Za-z0-9]{8,}"

echo "Git hooks on: git-secrets now scans every commit in this clone."
39 changes: 38 additions & 1 deletion .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,45 @@
version: 2

# Every ecosystem waits 7 days after a release before proposing it, and groups its routine updates
# into one pull request. Images are pinned by tag and digest; Dependabot updates both.
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
# Add this repository's package ecosystem as its code lands
cooldown:
default-days: 7
groups:
actions:
patterns: ["*"]

# compose.yaml (the platform) and compose.tools.yaml (the lint tools).
- package-ecosystem: docker-compose
directory: /
schedule:
interval: weekly
cooldown:
default-days: 7
groups:
images:
patterns: ["*"]
update-types: [minor, patch]
ignore:
# Stay on nginx's stable line (1.30); a new stable line is a deliberate change.
- dependency-name: nginxinc/nginx-unprivileged
update-types: ["version-update:semver-minor", "version-update:semver-major"]
# A new PostgreSQL major version needs a data migration, so it is never automatic.
- dependency-name: postgres
update-types: ["version-update:semver-major"]

# The derived images' pinned bases (images/mailpit, images/meilisearch).
- package-ecosystem: docker
directories: ["/images/*"]
schedule:
interval: weekly
cooldown:
default-days: 7
groups:
base-images:
patterns: ["*"]
update-types: [minor, patch]
82 changes: 82 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
name: CI

on:
push:
branches: [main]
pull_request:
branches: [main]

permissions:
contents: read

concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true

jobs:
lint:
name: Lint
runs-on: ubuntu-26.04
timeout-minutes: 15
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Bootstrap (secrets and certificates only)
run: sh scripts/bootstrap.sh --ci

- name: Lint
run: make lint

smoke:
name: Smoke
runs-on: ubuntu-26.04
timeout-minutes: 20
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Bootstrap (secrets and certificates only)
run: sh scripts/bootstrap.sh --ci

- name: Start the platform
run: make up

- name: Smoke checks
run: make smoke

- name: Logs
if: failure()
run: docker compose logs --no-color --timestamps

- name: Remove the platform and its data
if: always()
run: make reset CONFIRM=yes

macos-tests:
# The CA and hosts scripts must also work with macOS's LibreSSL, BSD tools and /bin/sh.
name: macOS tests
runs-on: macos-latest
timeout-minutes: 10
env:
OPENSSL: /usr/bin/openssl # the system LibreSSL, not a Homebrew OpenSSL on PATH
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Every script parses under /bin/sh, and make runs (GNU Make 3.81)
run: |
for f in scripts/*.sh tests/*.sh .githooks/*; do /bin/sh -n "$f"; done
make help

- name: CA tests
run: /bin/sh tests/ca.test.sh

- name: Hosts tests
run: /bin/sh tests/hosts.test.sh
9 changes: 6 additions & 3 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ jobs:
analyze:
name: Analyze (${{ matrix.language }})
runs-on: ubuntu-26.04
timeout-minutes: 30
permissions:
contents: read
security-events: write # Upload results to code scanning
Expand All @@ -24,15 +25,17 @@ jobs:
language: [actions] # Add this repository's languages as its code lands
steps:
- name: Checkout repository
uses: actions/checkout@v7
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Initialize CodeQL
uses: github/codeql-action/init@v4
uses: github/codeql-action/init@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
with:
languages: ${{ matrix.language }}
build-mode: none

- name: Analyze
uses: github/codeql-action/analyze@v4
uses: github/codeql-action/analyze@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
with:
category: /language:${{ matrix.language }}
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,12 @@
# Agent working files such as worklog ledgers; the record lives in GitHub issues.
.agent/

# Generated per machine by `make bootstrap`. Never commit them: they hold local secrets and keys.
.env
.env.*
!.env.example
secrets/
certs/

# Tools fetched or cached by the scripts.
.tools/
58 changes: 58 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# The platform's commands: run `make` to list them. Compose does the work, and anything longer than a
# line lives in scripts/ as POSIX sh, so each command behaves the same on WSL2, macOS and Linux.

.DEFAULT_GOAL := help
.PHONY: help bootstrap up down reset logs status doctor smoke lint trust untrust hosts

# Stopping and reading logs create nothing, so they need no credentials. The placeholders let Compose
# read compose.yaml when .env is missing or incomplete; without them `make reset` fails in exactly the
# case bootstrap sends you to it (a credential missing while its data volume still exists).
# scripts/status.sh uses the same placeholders.
COMPOSE_NO_CREATE = RABBITMQ_PASSWORD=unused MEILI_MASTER_KEY=unused docker compose

help: ## List the commands
@grep -E '^[a-z][a-z-]*:.*## ' $(MAKEFILE_LIST) | awk 'BEGIN { FS = ":.*## " } { printf " %-10s %s\n", $$1, $$2 }'

bootstrap: ## Prepare this machine: secrets, certificates, trust; safe to run again
@sh ./scripts/bootstrap.sh

up: ## Start the platform and wait until every service is healthy
@for f in .env secrets/postgres_password secrets/valkey.conf certs/leaf/cert.pem certs/leaf/key.pem; do \
[ -f "$$f" ] || { echo "error: $$f is missing. Run 'make bootstrap' first." >&2; exit 1; }; \
done
docker compose up --detach --wait

down: ## Stop the platform and keep its data
@$(COMPOSE_NO_CREATE) down

reset: ## Stop the platform and delete its data; keeps .env, secrets/ and certs/ (CONFIRM=yes skips the question)
@if [ "$(CONFIRM)" != yes ]; then \
printf 'This deletes the Postgres, Valkey, RabbitMQ, Mailpit and Meilisearch data. Type yes to continue: '; \
read -r answer; \
[ "$$answer" = yes ] || { echo 'Cancelled. Nothing was deleted.'; exit 1; }; \
fi
@$(COMPOSE_NO_CREATE) down --volumes --remove-orphans

logs: ## Follow the logs (s=<service> for one service)
@$(COMPOSE_NO_CREATE) logs --follow $(s)

status: ## Show service health, the certificate, trust and the hosts file
@sh ./scripts/status.sh

doctor: ## Check this machine and checkout for problems, changing nothing
@sh ./scripts/doctor.sh

smoke: ## Check the running platform end to end
@sh ./scripts/smoke.sh

lint: ## Static checks and tests, as CI runs them (after make bootstrap)
@sh ./scripts/lint.sh

trust: ## Trust the platform's root certificate in your browsers (asks you once)
@sh ./scripts/trust.sh

untrust: ## Remove every root certificate this platform created from your trust store
@sh ./scripts/trust.sh untrust

hosts: ## Point the local addresses at 127.0.0.1 in your hosts file (asks once to elevate)
@sh ./scripts/hosts.sh
82 changes: 77 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ start, run and verify.
- Starting and stopping the ecosystem with a handful of simple commands: bootstrap, up, down, reset,
seed, logs and status
- The reverse proxy, local TLS, and routing each domain to its application
- Local infrastructure: PostgreSQL, Redis, RabbitMQ, Mailpit and Meilisearch
- Local infrastructure: PostgreSQL, Valkey (Redis-compatible), RabbitMQ, Mailpit and Meilisearch
- Local observability: logs, metrics, traces and dashboards
- Development configuration and secrets, generated rather than handed out
- Health checks across every service
Expand All @@ -25,11 +25,81 @@ https://api.rsl-commerce.test
https://admin.rsl-commerce.test
https://checkout.rsl-commerce.test
https://docs.rsl-commerce.test
https://design.rsl-commerce.test
https://mail.rsl-commerce.test
https://observe.rsl-commerce.test
```

Each address needs its own line in the hosts file, because hosts files do not support wildcards.
`make hosts` adds the missing ones. Until an application joins the stack, its address answers with
a "not running yet" page; `rsl-commerce.test` shows a placeholder until the storefront arrives.

## Getting started

You need Docker Desktop (WSL2 or macOS) or Docker Engine (Linux) with Compose 5 or newer, `make`,
`openssl` and `curl`. On Windows, clone into the WSL filesystem (such as `~/code`), not under `/mnt`.

```sh
make bootstrap # secrets, certificates, and trust for the root certificate
make hosts # adds the missing local addresses to your hosts file
make up # starts everything and waits until it's healthy
```

Then open <https://rsl-commerce.test> and <https://mail.rsl-commerce.test>. `make bootstrap` is safe
to run again at any time: it keeps your secrets and certificates and asks for nothing that's done.

## Commands

| Command | What it does |
| ------- | ------------ |
| `make bootstrap` | Checks prerequisites, writes `.env` and the Postgres secret, creates or renews the certificates, trusts the root, and lists missing hosts lines |
| `make up` | Starts the platform and waits until every service is healthy |
| `make down` | Stops it and keeps the data |
| `make reset` | Stops it and deletes its data (asks first; `CONFIRM=yes` skips the question). Keeps `.env` and the certificates |
| `make status` | Service health, the certificate's days left, trust and the hosts file |
| `make doctor` | Checks this machine and checkout for problems, changing nothing |
| `make logs` | Follows the logs; `make logs s=postgres` for one service |
| `make smoke` | Checks the running platform end to end |
| `make lint` | Static checks and tests, as CI runs them |
| `make trust`, `make untrust` | Trusts the root certificate, or removes every root this platform created |
| `make hosts` | Adds the missing local addresses to the hosts file |

The services, on the internal `data` network: `postgres:5432` (user and database `commerce`),
`valkey:6379`, `rabbitmq:5672`, `mailpit:1025` for SMTP, and `meilisearch:7700`. Each needs its
credential, generated into `.env`: `POSTGRES_PASSWORD`, `VALKEY_PASSWORD`, `RABBITMQ_PASSWORD` (user
`commerce`) and `MEILI_MASTER_KEY`. The proxy sits on a separate `edge` network and can't reach
them. RabbitMQ's management UI is at <http://127.0.0.1:15672>.

## What asks for permission

Two steps change your machine, and each asks once:

- **`make trust`.** The platform's own root certificate, which can only vouch for
`rsl-commerce.test` addresses (never another site or an IP address).
- WSL2: Windows asks you to confirm adding it to your user's certificate store. No administrator
rights. Check the thumbprint the command prints, then choose Yes. Edge and Chrome use it.
- macOS: `sudo` adds it to the System keychain, which Safari, Chrome and Firefox read.
- Linux: `sudo` adds it to the system store, then `certutil` adds it to Chrome's and Firefox's
databases (install `libnss3-tools` or `nss-tools` first).
- **`make hosts`.** It shows the change, then writes a block between `# BEGIN rsl-commerce` and
`# END rsl-commerce` holding only the missing addresses, keeping a copy of the original as
`hosts.rsl-commerce.bak`. On WSL2 that's Windows' hosts file, after one administrator (UAC)
prompt; on macOS and Linux, `/etc/hosts` with `sudo`.

`make untrust` removes the root again. To remove the hosts lines, delete the block by hand.

## Troubleshooting

Start with `make doctor`: it checks Docker and Compose, the ports, disk space, `.env`, the
certificates, trust and the hosts file, and says what to do about each problem.

- **A certificate warning in the browser.** Run `make trust`, then restart the browser.
- **Still an error after fixing it.** The proxy sends HSTS for a day, so a browser that saw a broken
certificate may refuse to let you continue. Clear it for the address: in Chrome or Edge open
`chrome://net-internals/#hsts` (or `edge://net-internals/#hsts`) and delete the domain policy for
`rsl-commerce.test`; in Firefox, forget the site from its history.
- **A credential stopped working.** Postgres and RabbitMQ keep the password from their first start
in their data volume. If `.env` changed since, `make reset` starts the data over.

## Does not own

Expand All @@ -45,13 +115,15 @@ Each address needs its own line in the hosts file, because hosts files do not su

## Git hooks

Run `.githooks/setup` once after cloning. It turns on the committed hooks, which use
[git-secrets](https://github.com/awslabs/git-secrets#installing-git-secrets) to refuse any commit
that contains a secret.
Run `.githooks/setup` once after cloning, and again when a pull changes it. It turns on the
committed hooks, which use [git-secrets](https://github.com/awslabs/git-secrets#installing-git-secrets)
to refuse any commit that contains a secret.

## Status

Planning. No code yet.
The walking skeleton: the proxy, local TLS and the infrastructure services run, with `make smoke`
and CI proving them. No application has joined yet. The decisions behind the stack are in
[ADR 0001](docs/adr/0001-platform-stack.md).

## License

Expand Down
Loading
Loading