Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
73b04c8
feat(certgen): generate farm CA, server and client certificates
uberware Aug 22, 2026
725837c
feat(certgen): write CA and leaf material with safe file modes
uberware Aug 22, 2026
9e6ac24
feat(config): add http.tls and nats.tls with load-time validation
uberware Aug 22, 2026
6c8c017
feat(server): terminate TLS on the REST and WebSocket listener
uberware Aug 22, 2026
fa2891e
feat(bus): add TLS and optional client-certificate auth to the embedd…
uberware Aug 22, 2026
13a6f21
test(server): pin that credential reload preserves broker TLS
uberware Aug 22, 2026
52b5361
feat(discovery): advertise TLS state, and pin cookie behavior under TLS
uberware Aug 22, 2026
6f9263a
feat(worker): enroll over HTTPS against a private CA and force-enable…
uberware Aug 22, 2026
ca33d4f
feat(cli): add sqi-server tls init certificate generator
uberware Aug 22, 2026
2d81d71
test: cover TLS end to end in integration and smoke
uberware Aug 22, 2026
10ab898
test(server): pin the plaintext default configuration
uberware Aug 22, 2026
d9b0531
docs: document TLS configuration, tls init, and the enablement flag-day
uberware Aug 22, 2026
5e5eb77
refactor(tls): share TLS loading in internal/tlsutil and unfork the t…
uberware Aug 22, 2026
626ee5d
feat(worker): make nats.tls_enabled tri-state and act on the advertis…
uberware Aug 22, 2026
fb0a88d
test(discovery): cover the mDNS path end to end over real multicast
uberware Aug 22, 2026
26253e3
test(discovery): restrict test advertisements to loopback
uberware Aug 22, 2026
2aceebe
fix(tls): drop the in-process TLS workaround and close review gaps
uberware Aug 22, 2026
da2b359
feat(cli): add sqi-server tls issue for certificates from an existing CA
uberware Aug 22, 2026
0612cb7
docs: correct the TLS scope claim and cover the plaintext worker list…
uberware Aug 22, 2026
3baff52
fix(discovery): fall back to the advertised IP and add TLS to the wor…
uberware Aug 22, 2026
c64641f
fix(ci): give Linux loopback a non-loopback address so mDNS can register
uberware Aug 22, 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
119 changes: 117 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -510,7 +510,7 @@ jobs:
# Reuses scripts/smoke.sh (the same script developers run via `make smoke`).
# Passing the prebuilt binaries skips its internal `make build`, so no GNU
# make is required on the Windows runner.
- name: End-to-end smoke (REST + WebSocket)
- name: End-to-end smoke (REST + WebSocket, plain / broker-auth / TLS)
run: |
ext=""; [ "$RUNNER_OS" = "Windows" ] && ext=".exe"
export SQI_SERVER_BIN="bin/sqi-server${ext}"
Expand All @@ -519,7 +519,24 @@ jobs:
# status is not masked by `export` (shellcheck SC2155).
SQI_SMOKE_PYTHON="$(command -v python)"
export SQI_SMOKE_PYTHON
bash scripts/smoke.sh
bash scripts/smoke.sh 2>&1 | tee smoke-output.log
status=${PIPESTATUS[0]}

# The script runs three modes. Assert each one actually reported a
# pass by name: a filter or branch regression that silently skipped a
# mode would otherwise leave this job green while covering less.
missing=0
for mode in noauth brokerauth tls; do
if ! grep -q "SMOKE TEST PASSED (mode=$mode)" smoke-output.log; then
echo "::error::smoke mode $mode did not report a pass"
missing=1
fi
done
if [ "$status" -ne 0 ] || [ "$missing" -ne 0 ]; then
echo "::error::the smoke test did not pass in all three modes (exit=$status)."
exit 1
fi
echo "Confirmed all three smoke modes passed."

# ── Docker smoke test ────────────────────────────────────────────────────────
# Builds both Docker images from source (no registry push) and runs them
Expand Down Expand Up @@ -1045,6 +1062,8 @@ jobs:
TestWorkerBinaryStagingFailureReason
TestProductSubmit_RetryOverrides
TestAutoRetry_RetryThenSucceed
TestTLSEndToEnd
TestTLSWorkerWithoutCAIsRefused
)
missing=0
for name in "${expected[@]}"; do
Expand All @@ -1059,6 +1078,102 @@ jobs:
fi
echo "Confirmed all ${#expected[@]} named integration tests passed, and the whole suite exited 0."

# ── mDNS discovery over real multicast ───────────────────────────────────────
# A SEPARATE job, not folded into integration-tests, for two reasons. Whether
# a hosted runner can do multicast on loopback is the one thing about this
# suite that could not be verified before it was written, so isolating it
# means such a runner fails this job precisely instead of taking the whole
# integration suite down with an unrelated-looking error. And the real-binary
# test binds the test broker to all interfaces, which should happen only when
# discovery testing was asked for — `make test-integration` skips it.
#
# SQI_TEST_REQUIRE_MULTICAST=1 (set by `make test-discovery`) turns the
# capability skip into a failure, so this job cannot go green while covering
# nothing — the same guard ldap-integration and isolation-integration use.
discovery-integration:
name: mDNS discovery (real multicast)
runs-on: ubuntu-latest
needs: test
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod
cache: true

# The advertisement carries "<hostname>.local", which the worker dials.
# macOS answers that through mDNSResponder; a stock ubuntu runner has no
# mDNS NSS module, so the worker would discover the server and then fail
# to dial it.
#
# A hosts entry rather than libnss-mdns + avahi: avahi-daemon would want
# port 5353, which is the port the test's own responder binds, so
# installing it to fix resolution risks breaking advertisement. Mapping
# the name to loopback needs no daemon and cannot conflict.
- name: Resolve this host's mDNS name locally
run: |
echo "127.0.0.1 $(hostname).local" | sudo tee -a /etc/hosts
getent hosts "$(hostname).local" || {
echo "::error::$(hostname).local still does not resolve"; exit 1; }

# Two things a Linux loopback lacks that macOS lo0 has by default, and the
# loopback-only advertisement needs both. The tests advertise on loopback
# only — deliberately, so a run never announces a service on the network it
# happens to be on — and refuse to fall back to a real interface, so
# neither of these can be skipped: without them the suite cannot run at all.
#
# 1. The MULTICAST flag. Linux `lo` ships without it, and the tests select
# their interfaces by that flag, so an untouched `lo` looks to them like
# a host that cannot do multicast.
# 2. A non-loopback address. zeroconf fills the advertisement's address
# records from the interface's own addresses and discards loopback ones
# (addrsForInterface: `!ipnet.IP.IsLoopback()`); a Linux `lo` carries
# only 127.0.0.1 and ::1, so registering on loopback alone fails
# outright with "Could not determine host IP addresses". macOS lo0 also
# carries fe80::1, which is the whole reason this never shows up there.
# Adding that same link-local address makes the two behave alike — it
# is scoped to the link and is not routed anywhere.
- name: Prepare loopback for mDNS
run: |
ip -o addr show lo
sudo ip link set lo multicast on
ip link show lo | grep -q MULTICAST || {
echo "::error::could not enable MULTICAST on lo"; exit 1; }
sudo ip -6 addr add fe80::1/64 dev lo || true
ip -o -6 addr show lo scope link | grep -q fe80 || {
echo "::error::lo has no link-local address, so a loopback-only mDNS"
echo "::error::advertisement cannot register — see the comment above"
exit 1; }

- name: Run the discovery suite over real multicast
run: |
set +e
make test-discovery 2>&1 | tee discovery-output.log
status=${PIPESTATUS[0]}
set -e

expected=(
TestDiscovery_TLSRecordsCrossTheWire
TestDiscovery_AdvertisedBrokerTLSReachesWorkerConfig
TestDiscovery_RealBinaryFindsItsServerOverMDNS
)
missing=0
for name in "${expected[@]}"; do
if ! grep -q -- "--- PASS: $name" discovery-output.log; then
echo "::error::$name did not pass"
missing=1
fi
done
if [ "$status" -ne 0 ] || [ "$missing" -ne 0 ]; then
echo "::error::the mDNS discovery suite did not pass (exit=$status)."
echo "::error::if the runner cannot do multicast, the suite says so explicitly."
exit 1
fi
echo "Confirmed all ${#expected[@]} discovery tests passed over real multicast."

# Runs the official OpenJD conformance suite (a pinned submodule under
# third_party/) against internal/openjd. Tagged `conformance`, so it does not
# run in the default `make test`.
Expand Down
11 changes: 11 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,17 @@ test-isolation: ## Run run-as-user isolation tests as root against real OS accou
docker run --rm --init sqi-isolation-test \
go test $(TEST_FLAGS) -tags integration -run 'TestIsolation_' -v -timeout 15m ./test/integration/

.PHONY: test-discovery
test-discovery: ## Run the mDNS discovery tests over REAL multicast (fails rather than skips if multicast is unavailable)
@echo "note: advertisements are restricted to loopback, so nothing is announced"
@echo " on your network. One test (RealBinary...) binds the test broker to"
@echo " all interfaces for ~10s; make test-integration skips that one."
@echo " On Linux, loopback needs both: sudo ip link set lo multicast on"
@echo " and: sudo ip -6 addr add fe80::1/64 dev lo (zeroconf discards"
@echo " loopback addresses, so lo has nothing to advertise without it)"
SQI_TEST_REQUIRE_MULTICAST=1 go test $(TEST_FLAGS) -tags integration \
-run 'TestDiscovery_' -v -timeout 10m ./test/integration/

.PHONY: test-isolation-windows
test-isolation-windows: ## Run windows run-as-user isolation tests as SYSTEM against real local accounts (needs an elevated shell)
@powershell -NoProfile -ExecutionPolicy Bypass -File scripts/test-isolation-windows.ps1
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ sqi-worker # run on each render node — finds the server automatically

On a local network, workers discover the server via mDNS and connect without any configuration. Open a browser, start submitting. That's it.

There is no authentication in this mode — the server trusts the local network, by design. To require logins, set `auth.enabled: true` and see [docs/auth.md](docs/auth.md), which covers the first-admin bootstrap, roles, API keys, and connecting a directory or SSO provider.
There is no authentication in this mode — the server trusts the local network, by design. To require logins, set `auth.enabled: true` and see [docs/auth.md](docs/auth.md), which covers the first-admin bootstrap, roles, API keys, and connecting a directory or SSO provider. Traffic is unencrypted in this mode too; [docs/tls.md](docs/tls.md) covers turning on TLS for the API and the worker transport, which is a separate opt-in setting.

See the [Quickstart](docs/quickstart.md) for a full walkthrough (binary or Docker Compose), including creating a farm and queue and submitting your first job.

Expand Down
1 change: 1 addition & 0 deletions cmd/sqi-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,4 +202,5 @@ internal/
- [`docs/architecture.md`](../../docs/architecture.md) — Component layout and job lifecycle data flow.
- [`docs/configuration.md`](../../docs/configuration.md) — Complete configuration reference.
- [`docs/api.md`](../../docs/api.md) — REST API reference with worked examples.
- [`docs/tls.md`](../../docs/tls.md) — In-process TLS for the API and broker, `tls init` / `tls issue`, and worker mTLS.
- [`docs/development.md`](../../docs/development.md) — Local setup and contribution guide.
1 change: 1 addition & 0 deletions cmd/sqi-server/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ func init() {
configCmd,
backupCmd,
workerCmd,
tlsCmd,
)
}

Expand Down
2 changes: 2 additions & 0 deletions cmd/sqi-server/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,8 @@ func runServe(cmd *cobra.Command, _ []string) error {
func serverConfig(cfg config.Config, schedCfg scheduler.Config) server.Config {
return server.Config{
HTTPAddr: cfg.HTTP.Addr,
HTTPTLS: cfg.HTTP.TLS,
NATSTLS: cfg.NATS.TLS,
CORSOrigins: cfg.HTTP.CORSOrigins,
NATSAddr: cfg.NATS.Addr,
NATSAuthEnabled: cfg.NATS.Auth.Enabled,
Expand Down
Loading
Loading