ADR-0150: monitoring.moosequest.net is gated by an ALB Google OIDC action, not Cloudflare Access (proposed as a dedicated ALB; built as a host rule on the shared Woodpecker ALB)
Status: Accepted (accepted 2026-10-30; Decision 2 amended by operator decision 2026-09-03, see "As built") Date: 2026-08-31 Refs: docs/architecture/monitoring-public-ingress.md, #4547, #4568, [ADR-0149](https://internal-docs.raxx.app/architecture/adr/0149-session-agent-credential-model.html), ADR-0134
Context
Grafana on ci-monitoring is loopback-only today (SSM port-forward access only).
We need a public hostname, monitoring.moosequest.net, with real authentication —
not a bare login-form exposure (see FreeScout finding: open /login port bound to
0.0.0.0/0 was already flagged as a mistake class in this estate). Two existing
precedents exist in the org for gating a hostname with SSO:
vault.raxx.app→ Cloudflare Access (Google Workspace SSO) — requires the hostname's DNS to be proxied through Cloudflare.ci.moosequest.net→ AWS ALBauthenticate-oidcaction (Google Workspace) — used becausemoosequest.netis NOT on Cloudflare (Oracle Dyn zone, hand-typed records, confirmed live viadig +short moosequest.net NS→ns[1-4].dns.dyn.com).
monitoring.moosequest.net is under the Dyn-managed moosequest.net zone.
Decision
- Auth gate: AWS ALB
authenticate-oidcagainst Google Workspace, theci.moosequest.netpattern — not Cloudflare Access. Cloudflare Access is architecturally unavailable here because the zone isn't proxied through Cloudflare; forcing it would require either movingmoosequest.netto Cloudflare (a much larger, unscoped change affecting every record in that zone) or a Cloudflare-for-SaaS custom-hostname setup (added complexity with no precedent in this codebase). ALB OIDC is a known-good, already-running pattern. - A new, dedicated ALB (
monitoring-grafana-alb), not a second hostname/cert on the existingci-woodpecker-alb. Reasons: (a) blast-radius isolation — the WP ALB already carries a live OIDC-bypass listener-rule set tuned to Woodpecker's own auth quirks (/hook*,/api/badge*,/api/*,/authorize,/login,/logout) and an in-flight ASG-of-1 cutover; adding Grafana routing here risks entangling two unrelated availability domains; (b) Grafana needs zero bypass rules (no webhook/badge-equivalent unauthenticated path), so it doesn't benefit from sharing that rule set; (c) cost delta is ~$16/mo for a second ALB, which is cheap insurance against a shared-ALB regression touching CI login. - Grafana's own login stays enabled as a second, independent factor behind the OIDC gate — not because we distrust the ALB gate, but because "OIDC gate is the only defense" is exactly the single-point-of-failure shape this ADR is trying to avoid repeating from the FreeScout finding, just one layer removed.
- The box-side change is minimal and isolated: Grafana's docker-compose port
publish moves from
127.0.0.1:3000:3000to3000:3000, gated by a new, narrowly-scoped security-group rule (TCP 3000 from the new ALB's SG only). No other config onci-monitoringchanges as part of enabling ingress — scrape/alert config changes are sequenced into separate cards (see design doc §8).
Alternatives considered
- Cloudflare Access, migrating
moosequest.netto Cloudflare DNS. Rejected for this card: moving an entire hand-typed Dyn zone to Cloudflare is a large, independently-risky change (every existing record needs review) — out of scope for "expose one Grafana instance," and not requested by the operator. - Reuse
ci-woodpecker-albwith a second ACM cert (SAN) + host-header listener rule. Technically simpler (one fewer ALB) but couples Grafana's public-ingress availability and any future WAF/rate-limit change to Woodpecker's ALB, which is mid-cutover (Option D, ASG-of-1) perinfra/ci/alb.tf. Rejected on blast-radius grounds, consistent with this design's explicit goal of not repeating the CI incident's fragility. - Grafana's own OAuth (Google) instead of an outer ALB gate. Grafana supports a native Google OAuth provider. Rejected as the sole gate because it reintroduces "app's own login form is the only defense," the exact FreeScout-class mistake; kept as a defense-in-depth inner layer instead, with the ALB as the outer gate.
- VPN / bastion instead of public ALB. No existing VPN infra in this estate; would be a much larger net-new investment for one dashboard. SSM port-forward already serves the "no public access needed" use case for anyone who doesn't need the public hostname; this ADR is specifically about the cases that do (mobile checks, sharing a link with a non-SSM-privileged stakeholder).
Consequences
- (Superseded, see "As built") New Terraform root
infra/monitoring-ingress/(ALB, TG, SGs, ACM cert, OIDC SSM reads) — separate state from bothterraform/ci-monitoring/andinfra/ci/, so aplan/applyhere cannot accidentally touch either existing stack. - (Path superseded, see "As built") New SSM secrets under
/ci/monitoring/oidc/(client_id, client_secret) — can reuse the existing Google Workspace OAuth app used forci.moosequest.netif its redirect-URI allowlist can addhttps://monitoring.moosequest.net/oauth2/idpresponse, or a new OAuth client if that's cleaner for revocation-blast-radius reasons — left to the implementing developer, flagged as an implementation-sized decision in the sub-card, not an architectural one. - One SG rule added to the existing
ci-monitoring-sg(additive, single port, single source SG) and one docker-compose line changed on the shared box — the only shared-box touch in this whole design. - A manual, unautomatable Dyn DNS step is now the sole cutover point — matches the
existing
ci.moosequest.netoperational reality; no new class of manual-step risk introduced, but see design doc §5 / card #4547 for the pre-existing gap this doesn't solve (no existence monitor for hand-typed Dyn records). - Sizing risk (flagged by TradeMasterAPI review, 2026-08-31):
ci-monitoringis a singlet4g.small, already flagged for memory pressure (#4500/#4435) and also hosting the CI build caches (devpi/registry mirror). Card B's change makes Grafana reachable via the ALB in addition to SSM port-forward, so this box now carries CI-cache traffic + all monitoring services + public-ish dashboard traffic on one small instance. Not blocking this design, but a resize (or splitting Grafana onto its own instance) should be revisited if dashboard usage or cache load grows — treat sustained memory pressure after this ships as the trigger, not a fixed timeline.
Language/runtime choice rationale
N/A — no new service/process is introduced. Grafana, Prometheus, Alertmanager, and blackbox-exporter are unchanged; this ADR only adds an ALB (managed AWS resource, no runtime to choose) in front of the existing stack.
As built (verified against develop, 2026-10-30 UTC)
Status moved from Proposed to Accepted on 2026-10-30 because the decision it records
(gate the hostname with an ALB authenticate-oidc action against Google Workspace)
is implemented in infra/ci/monitoring-ingress.tf (Card A, #4570). Implementation
departed from the text above in several places; this section is authoritative.
| Item | As decided above | As built |
|---|---|---|
| Auth gate (Decision 1) | ALB authenticate-oidc, Google Workspace, not Cloudflare Access |
Same. A new, dedicated Google OAuth client (not the ci.moosequest.net one) so the two logins can be revoked independently. Credentials in SSM at /ci/monitoring-oidc/google-client-id and /ci/monitoring-oidc/google-client-secret (the text above said /ci/monitoring/oidc/) |
| Load balancer (Decision 2) | New dedicated ALB monitoring-grafana-alb |
Superseded by an operator decision on 2026-09-03: a host-header listener rule (priority 30) on the existing ci-woodpecker-alb HTTPS listener, forwarding to target group ci-monitoring-grafana-tg (IP target, port 3000). Saves about $20/month. The blast-radius isolation reason in Decision 2 was traded away: the monitoring rule now shares the ALB, security group and Terraform root with the CI control plane |
| Terraform location | New root infra/monitoring-ingress/ with separate state |
infra/ci/monitoring-ingress.tf in the existing infra/ci root. It reads terraform/ci-monitoring state through terraform_remote_state for the instance IP and security group ID |
| ACM certificate | Not specified | New cert for monitoring.moosequest.net, DNS validation. The zone is on Oracle Dyn, so validation is a manual CNAME (monitoring_acm_validation_cname_* outputs) |
| Box-side change (Decision 4) | Compose port publish 3000:3000 plus one SG rule on ci-monitoring-sg |
Not on develop yet (Card B, #4571, open). terraform/ci-monitoring/templates/cloud-init.sh.tpl still publishes 127.0.0.1:3000:3000 and terraform/ci-monitoring has no ingress rule for the ALB. Only the ALB-side egress rule exists. Until Card B lands the target group has no healthy targets and the hostname answers 503 |
| DNS | Manual Dyn CNAME | Unchanged. No record is created by Terraform; the monitoring_cname_record output gives the target (the shared ALB's DNS name) |
| Grafana login kept (Decision 3) | Yes | Not changed by Card A |
Related cards: Card C (#4572, blackbox probes for console, tickets and vault) and Card D
(#4573, the product-status-overview dashboard) are in the ci-monitoring cloud-init
template. Cards E (#4574, Grafana role mapping) and F (#4575, ALB access logs and WAF)
are open and waiting on operator decisions. The parent epic is #4568. Whether the Card A
Terraform has been applied is not visible from the repo.
The "Consequences" bullets about a new Terraform root and the /ci/monitoring/oidc/ SSM
path above describe the original proposal and are superseded by this table.