Skip to content

Commit 7f7a1e9

Browse files
authored
Merge pull request #650 from sij411/feat/smoke-strict
Add strict-mode smoke test lane (HTTPS + signature verification)
2 parents e54cb03 + c6db09a commit 7f7a1e9

File tree

10 files changed

+459
-9
lines changed

10 files changed

+459
-9
lines changed
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
2+
#
3+
# Strict-mode interoperability smoke tests (HTTPS + HTTP signature verification).
4+
# Uses a standalone Docker Compose file with Caddy TLS proxies to verify that
5+
# Fedify correctly signs and verifies requests over HTTPS.
6+
# See: https://github.com/fedify-dev/fedify/issues/481
7+
name: smoke-mastodon-strict
8+
9+
on:
10+
schedule:
11+
- cron: "0 6 * * *"
12+
workflow_dispatch:
13+
14+
concurrency:
15+
group: ${{ github.workflow }}-${{ github.ref }}
16+
cancel-in-progress: true
17+
18+
jobs:
19+
smoke:
20+
runs-on: ubuntu-latest
21+
timeout-minutes: 25
22+
23+
env:
24+
COMPOSE: >-
25+
docker compose
26+
-f test/smoke/mastodon/docker-compose.strict.yml
27+
28+
steps:
29+
- uses: actions/checkout@v4
30+
31+
- uses: ./.github/actions/setup-mise
32+
33+
- name: Generate TLS certificates
34+
run: bash test/smoke/mastodon/generate-certs.sh test/smoke/mastodon/.certs
35+
36+
- name: Verify certificates
37+
run: |
38+
openssl verify -CAfile test/smoke/mastodon/.certs/ca.crt \
39+
test/smoke/mastodon/.certs/fedify-harness.crt
40+
openssl verify -CAfile test/smoke/mastodon/.certs/ca.crt \
41+
test/smoke/mastodon/.certs/mastodon.crt
42+
43+
- name: Generate Mastodon secrets
44+
run: |
45+
IMAGE=ghcr.io/mastodon/mastodon:v4.3.9
46+
docker pull "$IMAGE"
47+
48+
SECRET1=$(docker run --rm "$IMAGE" bundle exec rails secret)
49+
SECRET2=$(docker run --rm "$IMAGE" bundle exec rails secret)
50+
51+
{
52+
echo "SECRET_KEY_BASE=$SECRET1"
53+
echo "OTP_SECRET=$SECRET2"
54+
docker run --rm "$IMAGE" bundle exec rails mastodon:webpush:generate_vapid_key \
55+
| grep -E '^[A-Z_]+=.+'
56+
docker run --rm "$IMAGE" bundle exec rails db:encryption:init \
57+
| grep -E '^[A-Z_]+=.+'
58+
} >> test/smoke/mastodon/mastodon-strict.env
59+
60+
- name: Start database and redis
61+
run: |
62+
$COMPOSE up -d db redis
63+
$COMPOSE exec -T db \
64+
sh -c 'until pg_isready -U mastodon; do sleep 1; done'
65+
66+
- name: Run DB setup and migrations
67+
run: |
68+
$COMPOSE run --rm -T \
69+
mastodon-web-backend bundle exec rails db:setup
70+
timeout-minutes: 5
71+
72+
- name: Start Mastodon stack
73+
run: $COMPOSE up --wait
74+
timeout-minutes: 12
75+
76+
- name: Provision Mastodon
77+
run: bash test/smoke/mastodon/provision-strict.sh
78+
79+
- name: Verify connectivity
80+
run: |
81+
echo "=== Harness health (from mastodon-web-backend, via Caddy TLS) ==="
82+
$COMPOSE exec -T mastodon-web-backend \
83+
curl -sf https://fedify-harness/_test/health
84+
echo " OK"
85+
86+
echo "=== Harness health (from mastodon-sidekiq, via Caddy TLS) ==="
87+
$COMPOSE exec -T mastodon-sidekiq \
88+
curl -sf https://fedify-harness/_test/health
89+
echo " OK"
90+
91+
- name: Run smoke tests
92+
run: |
93+
set -a && source test/smoke/.env.test && set +a
94+
deno run --allow-net --allow-env --unstable-temporal \
95+
test/smoke/orchestrator.ts
96+
97+
- name: Collect logs on failure
98+
if: failure()
99+
run: |
100+
echo "=== Docker Compose logs ==="
101+
$COMPOSE logs --tail=500
102+
103+
- name: Teardown
104+
if: always()
105+
run: $COMPOSE down -v

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ node_modules/
88
package-lock.json
99
repomix-output.xml
1010
test/smoke/.env.test
11+
test/smoke/mastodon/.certs/
1112
test/smoke/mastodon/mastodon.env
13+
test/smoke/mastodon/mastodon-strict.env
1214
smoke.log
1315
t.ts
1416
t2.ts

test/smoke/harness/backdoor.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,15 @@ function json(data: unknown, status = 200): Response {
99
});
1010
}
1111

12-
// Build recipient manually — Mastodon's WebFinger requires HTTPS but our
13-
// harness only has HTTP. Parse the handle (user@domain) to construct the
14-
// actor URI and inbox URL directly.
12+
// Build recipient manually — in non-strict mode Mastodon's WebFinger requires
13+
// HTTPS but our harness only has HTTP, so we use http:// for the inbox URL.
14+
// In strict mode, Caddy terminates TLS, so we use https:// everywhere.
1515
function parseRecipient(
1616
handle: string,
1717
): { inboxId: URL; actorId: URL } {
1818
const [user, domain] = handle.split("@");
19-
const inboxId = new URL(`http://${domain}/users/${user}/inbox`);
19+
const scheme = Deno.env.get("STRICT_MODE") ? "https" : "http";
20+
const inboxId = new URL(`${scheme}://${domain}/users/${user}/inbox`);
2021
// Mastodon generates https:// actor URIs; use that as the canonical id
2122
const actorId = new URL(`https://${domain}/users/${user}`);
2223
return { inboxId, actorId };

test/smoke/harness/federation.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ const federation = createFederation<void>({
1212
kv: new MemoryKvStore(),
1313
origin: ORIGIN,
1414
allowPrivateAddress: true,
15-
skipSignatureVerification: true,
15+
skipSignatureVerification: !Deno.env.get("STRICT_MODE"),
1616
});
1717

1818
federation
@@ -48,12 +48,15 @@ federation
4848
if (!ctx.recipient || !followerUri) return;
4949

5050
// Build the recipient manually instead of calling getActor(), because
51-
// Mastodon generates https:// actor URIs but only serves HTTP.
52-
// Rewrite the scheme so sendActivity POSTs over plain HTTP.
53-
const httpActorUri = followerUri.href.replace(/^https:\/\//, "http://");
51+
// in non-strict mode Mastodon generates https:// actor URIs but only
52+
// serves HTTP. In strict mode the Caddy proxy handles TLS, so we
53+
// keep the original https:// scheme.
54+
const actorUri = Deno.env.get("STRICT_MODE")
55+
? followerUri.href
56+
: followerUri.href.replace(/^https:\/\//, "http://");
5457
const recipient = {
5558
id: followerUri,
56-
inboxId: new URL(`${httpActorUri}/inbox`),
59+
inboxId: new URL(`${actorUri}/inbox`),
5760
};
5861

5962
const accept = new Accept({
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
auto_https off
3+
}
4+
5+
:443 {
6+
tls /certs/fedify-harness.crt /certs/fedify-harness.key
7+
reverse_proxy fedify-harness-backend:3001
8+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
auto_https off
3+
}
4+
5+
:443 {
6+
tls /certs/mastodon.crt /certs/mastodon.key
7+
reverse_proxy mastodon-web-backend:3000
8+
}
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
# Standalone Docker Compose for strict-mode smoke tests (HTTPS + signatures).
2+
# Usage: docker compose -f docker-compose.strict.yml ...
3+
#
4+
# This is a standalone file (NOT an override) because Docker Compose merges
5+
# network aliases additively and cannot remove a service's own DNS name from
6+
# a network. Using an override would cause both the backend service and its
7+
# Caddy proxy to resolve to the same hostname, breaking TLS routing.
8+
#
9+
# Architecture:
10+
# - Backend services are renamed (e.g. fedify-harness-backend) so they
11+
# don't collide with the TLS hostnames on the network
12+
# - Caddy proxies claim the canonical hostnames (fedify-harness, mastodon)
13+
# via network aliases and terminate TLS
14+
# - All services share a single network for simplicity; DNS resolution
15+
# is unambiguous because backend names differ from Caddy aliases
16+
17+
volumes:
18+
harness-node-modules:
19+
20+
networks:
21+
smoke:
22+
driver: bridge
23+
24+
services:
25+
db:
26+
image: postgres:15-alpine
27+
environment:
28+
POSTGRES_DB: mastodon
29+
POSTGRES_USER: mastodon
30+
POSTGRES_PASSWORD: mastodon
31+
networks: [smoke]
32+
healthcheck:
33+
test: ["CMD", "pg_isready", "-U", "mastodon"]
34+
interval: 5s
35+
retries: 10
36+
37+
redis:
38+
image: redis:7-alpine
39+
networks: [smoke]
40+
healthcheck:
41+
test: ["CMD", "redis-cli", "ping"]
42+
interval: 5s
43+
retries: 10
44+
45+
# Fedify test harness — renamed to avoid colliding with the Caddy alias.
46+
fedify-harness-backend:
47+
image: denoland/deno:2.7.1
48+
working_dir: /workspace
49+
volumes:
50+
- ../../../:/workspace
51+
- harness-node-modules:/workspace/node_modules
52+
- ./.certs:/certs:ro
53+
command:
54+
- run
55+
- --allow-net
56+
- --allow-env
57+
- --allow-read
58+
- --allow-write
59+
- --unstable-temporal
60+
- test/smoke/harness/main.ts
61+
environment:
62+
HARNESS_ORIGIN: "https://fedify-harness"
63+
STRICT_MODE: "1"
64+
DENO_CERT: "/certs/ca.crt"
65+
networks: [smoke]
66+
ports: ["3001:3001"]
67+
healthcheck:
68+
test:
69+
[
70+
"CMD",
71+
"deno",
72+
"eval",
73+
"const r = await fetch('http://localhost:3001/_test/health'); if (!r.ok) Deno.exit(1);",
74+
]
75+
interval: 5s
76+
retries: 30
77+
78+
# Caddy TLS proxy for the Fedify harness.
79+
# Owns the "fedify-harness" hostname so other containers reach TLS.
80+
caddy-harness:
81+
image: caddy:2.11.2-alpine
82+
volumes:
83+
- ./Caddyfile.fedify-harness:/etc/caddy/Caddyfile:ro
84+
- ./.certs:/certs:ro
85+
networks:
86+
smoke:
87+
aliases: [fedify-harness]
88+
depends_on:
89+
fedify-harness-backend: { condition: service_healthy }
90+
healthcheck:
91+
test: ["CMD", "caddy", "version"]
92+
interval: 5s
93+
retries: 5
94+
95+
# Mastodon web — renamed to avoid colliding with the Caddy alias.
96+
mastodon-web-backend:
97+
image: ghcr.io/mastodon/mastodon:v4.3.9
98+
command:
99+
- bash
100+
- -c
101+
- |
102+
cat /usr/lib/ssl/cert.pem /certs/ca.crt > /tmp/ca-bundle.crt
103+
bundle exec rails s -p 3000 -b 0.0.0.0
104+
env_file: mastodon-strict.env
105+
environment:
106+
SSL_CERT_FILE: /tmp/ca-bundle.crt
107+
volumes:
108+
- ./disable_force_ssl.rb:/opt/mastodon/config/initializers/zz_disable_force_ssl.rb:ro
109+
- ./.certs:/certs:ro
110+
networks: [smoke]
111+
ports: ["3000:3000"]
112+
depends_on:
113+
db: { condition: service_healthy }
114+
redis: { condition: service_healthy }
115+
healthcheck:
116+
test:
117+
[
118+
"CMD-SHELL",
119+
"curl -sf http://localhost:3000/health | grep -q OK",
120+
]
121+
interval: 10s
122+
retries: 18
123+
124+
# Caddy TLS proxy for Mastodon.
125+
# Owns the "mastodon" hostname so other containers reach TLS.
126+
caddy-mastodon:
127+
image: caddy:2.11.2-alpine
128+
volumes:
129+
- ./Caddyfile.mastodon:/etc/caddy/Caddyfile:ro
130+
- ./.certs:/certs:ro
131+
networks:
132+
smoke:
133+
aliases: [mastodon]
134+
ports: ["4443:443"]
135+
depends_on:
136+
mastodon-web-backend: { condition: service_healthy }
137+
healthcheck:
138+
test: ["CMD", "caddy", "version"]
139+
interval: 5s
140+
retries: 5
141+
142+
mastodon-sidekiq:
143+
image: ghcr.io/mastodon/mastodon:v4.3.9
144+
command:
145+
- bash
146+
- -c
147+
- |
148+
cat /usr/lib/ssl/cert.pem /certs/ca.crt > /tmp/ca-bundle.crt
149+
bundle exec sidekiq -q ingress -q default -q push
150+
env_file: mastodon-strict.env
151+
environment:
152+
SSL_CERT_FILE: /tmp/ca-bundle.crt
153+
volumes:
154+
- ./disable_force_ssl.rb:/opt/mastodon/config/initializers/zz_disable_force_ssl.rb:ro
155+
- ./.certs:/certs:ro
156+
networks: [smoke]
157+
depends_on:
158+
mastodon-web-backend: { condition: service_healthy }
159+
caddy-harness: { condition: service_healthy }
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
#!/usr/bin/env bash
2+
# Generate a self-signed CA and leaf certificates for strict-mode smoke tests.
3+
# Usage: bash generate-certs.sh [output-dir]
4+
#
5+
# Output directory defaults to .certs/ (relative to this script).
6+
# The CA is ephemeral — generated fresh each run.
7+
set -euo pipefail
8+
9+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
10+
OUT="${1:-$SCRIPT_DIR/.certs}"
11+
mkdir -p "$OUT"
12+
13+
HOSTS=(fedify-harness mastodon)
14+
15+
echo "→ Generating CA key + certificate..."
16+
openssl genrsa -out "$OUT/ca.key" 2048 2>/dev/null
17+
openssl req -x509 -new -nodes \
18+
-key "$OUT/ca.key" \
19+
-sha256 -days 1 \
20+
-subj "/CN=Smoke Test CA" \
21+
-out "$OUT/ca.crt" 2>/dev/null
22+
23+
for HOST in "${HOSTS[@]}"; do
24+
echo "→ Generating certificate for $HOST..."
25+
openssl genrsa -out "$OUT/$HOST.key" 2048 2>/dev/null
26+
openssl req -new \
27+
-key "$OUT/$HOST.key" \
28+
-subj "/CN=$HOST" \
29+
-out "$OUT/$HOST.csr" 2>/dev/null
30+
31+
# Create a SAN extension config so the cert is valid for the hostname
32+
cat > "$OUT/$HOST.ext" <<EOF
33+
authorityKeyIdentifier=keyid,issuer
34+
basicConstraints=CA:FALSE
35+
subjectAltName=DNS:$HOST,DNS:localhost
36+
EOF
37+
38+
openssl x509 -req \
39+
-in "$OUT/$HOST.csr" \
40+
-CA "$OUT/ca.crt" -CAkey "$OUT/ca.key" -CAcreateserial \
41+
-days 1 -sha256 \
42+
-extfile "$OUT/$HOST.ext" \
43+
-out "$OUT/$HOST.crt" 2>/dev/null
44+
45+
rm -f "$OUT/$HOST.csr" "$OUT/$HOST.ext"
46+
done
47+
48+
rm -f "$OUT/ca.srl"
49+
50+
echo "✓ Certificates written to $OUT/"
51+
ls -la "$OUT"

0 commit comments

Comments
 (0)