PMM-15279 Productize the SEP nginx reverse proxy - #5759
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## PMM-15205-sep-fb #5759 +/- ##
===================================================
Coverage ? 44.22%
===================================================
Files ? 304
Lines ? 33119
Branches ? 0
===================================================
Hits ? 14646
Misses ? 16953
Partials ? 1520 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
@coderabbitai full review |
✅ Action performedFull review finished. |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Team Run ID: 📒 Files selected for processing (4)
🔗 Linked repositories identifiedCodeRabbit considers these linked repositories for cross-repo context during reviews:
🚧 Files skipped from review as they are similar to previous changes (1)
Included review availability: Your plan provides up to 4 included reviews per hour; 0 remain after this review. WalkthroughThe change adds SEP reverse-proxy configuration, startup validation and generation, the ChangesSEP reverse-proxy integration
Sequence Diagram(s)sequenceDiagram
participant Client
participant NGINX
participant SEP
Client->>NGINX: Request /sep/
NGINX->>SEP: Proxy request with forwarded headers
SEP-->>NGINX: Response
NGINX-->>Client: SEP response
Merge Risk: 🔵 Low · up to The opt-in /sep/ route exposes SEP through PMM's HTTPS listener while delegating authentication entirely to SEP, and invalid SEP configuration can prevent PMM Server from starting. The PR is mergeable with explicit owner awareness that SEP must protect every exposed endpoint and that deployment configuration errors affect overall server availability. 🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
Full details: Description checkExplanation The description is complete and directly related to the changes. It includes the ticket number, feature-build status, implementation details, testing evidence, dependencies, limitations, and the API documentation applicability note. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Pull request overview
Adds an opt-in nginx reverse proxy that exposes the SEP sidecar under /sep/.
Changes:
- Renders and validates SEP nginx configuration at container startup.
- Adds dynamic DNS resolution and SEP-unavailable handling.
- Documents the environment variables and includes SEP configuration in support bundles.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
.env.example |
Documents SEP settings. |
docker-compose.yml |
Passes SEP environment variables. |
build/docker/server/entrypoint.sh |
Renders the SEP proxy configuration. |
build/ansible/roles/nginx/tasks/main.yml |
Creates the SEP configuration directory. |
build/ansible/roles/nginx/files/sep/sep.conf.template |
Defines /sep/ proxy behavior. |
build/ansible/roles/nginx/files/conf.d/pmm.conf |
Includes SEP drop-ins. |
managed/utils/envvars/parser.go |
Recognizes the SEP address variable. |
managed/utils/envvars/parser_test.go |
Extends SEP environment parsing coverage. |
managed/services/server/logs.go |
Collects SEP configuration in support bundles. |
managed/services/server/logs_test.go |
Adjusts support-bundle expectations. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
Reviewed against PMM-15279's ten acceptance criteria and the repo conventions. This one is close to done, and the parts the ticket flagged as traps are all handled deliberately rather than by accident. The Deriving the resolver from I checked AC8 by reading the whole of VerdictApprove once the resolver value is validated and the title loses its colon. Both are small. Everything else below is comment-level, and one item is a question rather than a request. Must-fix1. Validate Defect one. A
Defect two. That closes the A single check — 2. The title's colon lands in Smaller things3. AC6 is partial — 4. 5. A SEP-only misconfiguration becomes a PMM-wide boot refusal. 6. Two comments overstate what the checks do. "The digit count is capped so the range test below cannot be handed a value that overflows the shell's integer parsing" — 7. Notes, not asks
|
#5755 (PMM-15280, Grafana service account) and #5768 (PMM-15331, the health gate that depended on it) were closed unmerged: provisioning the account by writing Grafana's rows directly was the wrong shape, and with that gone SEP provisioning is synchronous, leaving the gate nothing to report. The previous derivation still carried both, so a paired bring-up exercised code that will never ship - which is what the SEP side hit. This derivation is main (now carrying PMM-15238) plus #5762, #5759, #5653, #5739 and #5758. Recorded with -s ours so the branch moves forward without a force-push; the tree is the re-derivation. Signed-off-by: Yan Orestes <yan.orestes@percona.com>
|
Note GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer. |
|
Thanks marcuscruz-percona — checking AC8 by reading the whole of All of it is addressed in 5d7065d except item 6, where I think the measurement goes the other way. Two items were behaviour choices rather than defects, so I want to be explicit about which way I took them. 1. Your guard is in,
I swept the rest of the tree for the same construct: this template is the only 2. Title colon — fixed. The PR title is now 3. AC6 — yes, and now recorded in the template. SEP sets 4. 5. Boot refusal — kept fatal, deliberately. I weighed warn-and-skip and would rather the container refuse to start. An operator who set 6. The overflow comment — I think this one is wrong, with a caveat. Both halves of The caveat is your 7. Dead error branch — removed. On the notes: collapsing the three |
Route /sep/ to the SEP side-car as shipped configuration instead of the feature-build harness overlay, gated on PMM_ENABLE_SEP. pmm.conf gains one inert glob include of /etc/nginx/sep.d/*.conf; the entrypoint renders the drop-in there when the flag is on and clears the directory when it is off. The upstream is addressed through a variable so nginx resolves it at request time, which lets the config load with the side-car absent and pick up a restarted one without restarting pmm-server. The resolver is read from /etc/resolv.conf at start rather than hardcoded, because the AMI and OVF images run this same entrypoint under Podman, where embedded DNS lives at a different address. It is declared inside location /sep/ rather than at file scope: the drop-in is included in the server block, so at file scope it would also redirect DNS for every other request-time lookup, /percona-blog/feed among them. An IPv4 nameserver is preferred because nginx requires IPv6 resolver addresses bracketed, and a bare one fails nginx -t and so blocks the whole server from starting. PMM_SEP_ADDRESS is new and optional, defaulting to sep:9000. It is validated before being interpolated into the nginx config. Signed-off-by: Yan Orestes <yan.orestes@percona.com>
Stripping the %zone from a link-local nameserver produced a resolver that passes nginx -t and can never route DNS: nginx has no syntax for the interface scope, so every /sep/ request would time out into the 503. Skip scoped addresses instead and fail at start with a named message, and prefer a later unscoped nameserver over an earlier scoped one. Extract the support-bundle drop-in glob into sepConfigFiles so it can be exercised against a temporary directory, covering the present, absent and empty cases that TestFiles cannot reach outside a container. Signed-off-by: Yan Orestes <yan.orestes@percona.com>
godot runs with scope: toplevel and capital: true, and only skips the leading identifier for a comment bound to a named declaration -- one inside a const group is not, so opening with the constant's name failed the linter. Signed-off-by: Yan Orestes <yan.orestes@percona.com>
Validate SEP_RESOLVER the way SEP_ADDRESS already is. An addressless nameserver line rendered "resolver []", which is non-empty and so skipped both -z guards and the FATAL written for it, aborting nginx -t instead; and awk's $2 is whitespace-delimited, so a nameserver line carrying anything else was interpolated verbatim into the config. Drop ipv6=off. It suppressed AAAA lookups for the SEP name regardless of the resolver's own family, so the bracketed-IPv6 fallback picked an IPv6 resolver and then told nginx not to ask for IPv6 addresses. Record in the template that SEP applies X-Accel-Buffering from one frozen constant shared by every streaming route, which is what makes leaving proxy_buffering on safe. Drop the unreachable error branch in sepConfigFiles: filepath.Glob only returns ErrBadPattern, which a directory joined with a literal suffix cannot produce. Signed-off-by: Yan Orestes <yan.orestes@percona.com>
f66e47c to
31c82ba
Compare
## Summary Moves both halves of `sidecar/pmm-fb/compose.yaml` forward together. | Service | From | To | | --- | --- | --- | | `pmm-server` — `PMM_FB_TAG`, both defaults | `PR-4500-a2f83c2` (2026-08-25) | **`PR-4500-2c43912`** — cut 2026-09-03 from percona/pmm `PMM-15205-sep-fb` @ 53521917d | | `sep-sidecar` | 249f673 (2026-08-25) | **b97ee985fc64f841f611b3a057737814da308a61** — SEP `main`'s tip, published 2026-09-04 by Jenkins `SEP/Build` #235 | The PMM half was two builds behind — `PR-4500-d85ca73` also came and went. ### What the new PMM build carries `PMM-15205-sep-fb` was rebuilt as linear per-ticket history on `main`, and three PRs have merged into it since a2f83c2: | PR | What | | --- | --- | | percona/pmm#5759 | the `/sep/` nginx reverse proxy, productized out of this harness's overlay | | percona/pmm#5762 | SEP's secret files and the generated database password | | percona/pmm#5886 | the vendored SEP frontend synced to SEP `main` c16578d | ### The side-car moves with it This PR originally held `sep-sidecar` at 249f673, because nothing at or after c16578d had been published and there was no image to move to. That left the pair mismatched, PMM-new against SEP-old. It no longer is. `main` was merged into `pmm` as 1efbbe8, and `SEP/Build` #235 published `main`'s tip — 62 commits on from the old pin. c16578d is an ancestor of it, so the frontend sync in percona/pmm#5886 now has a side-car that answers it. The three features that would have degraded are live instead. Each symbol is present under `app/` at b97ee98 and absent at 249f673: - **Support-case autocomplete** issues requests. It is gated on `case_search_available` from `atw_config`, which this side-car sends. - **The executor co-location warning** mounts. It needs `target_service` on the host field's schema, which this side-car declares. - **The `unlaunchable` status** (SEP-1943) arrives, so its badge is exercised rather than unused. ## Tested Both pins were resolved against Docker Hub before merge: `perconalab/pmm-server-fb:PR-4500-2c43912` (pushed 2026-09-03) and `percona/percona-sep:b97ee985fc64f841f611b3a057737814da308a61` (pushed 2026-09-04). The side-car image was then held against the three repin checks the harness README requires on the artifact rather than on the commit that built it: | Check | Result | | --- | --- | | `SECRETS_DIR` references in `settings-env.sh` | 4 | | Grafana token mint | `grafana_service_account.py` present; the image's `state` directory is `drwx------ sep sep` | | `HEALTHCHECK`, read from the raw config blob | `CMD` runs `healthcheck.sh`, start period 150 s | No paired bring-up was run — this is a two-line pin change, and the artifact checks are what the README asks for at repin time. ## Checklist - [ ] New/modified functions have type hints and rST docstrings - [ ] New tests added for new features or bug fixes - [ ] All tests pass locally (`make test`) - [x] Pre-commit hooks pass (`make run-pre-commit`) - [ ] Database migrations generated if models changed (`make makemigrations`) - [ ] User-facing changes documented (README, inline help, UI text) - [ ] Configuration changes documented with examples - [x] Changelog fragment added under `changelog.d/` if the change is user-facing (`make changelog-add`), or confirmed N/A (internal-only change, or a same-release-cycle fix for an unreleased sibling ticket) --------- Signed-off-by: Yan Orestes <yan.orestes@percona.com>
Ticket number: PMM-15279
Feature build: pending — this branch cannot be exercised end-to-end alone; see Dependencies below.
Ships the nginx reverse-proxy configuration that routes
/sep/to the SEP side-car as first-class shipped configuration. Today this exists only as a hand-maintained overlay in SEP's feature-build harness, which mounts a renderedpmm.confover the stock one and reaches the side-car at a fixed Compose IP — neither of which can ship.Targets
PMM-15205-sep-fb, the PMM-15205 epic's integration branch, where every child of that epic lands before the epic goes tomain. It extends the samePMM_ENABLE_SEPgate and the sameparser.gocase as #5700 (PMM-15238), which is already merged tomain. Entirely opt-in: with the flag unset the only shipped delta is one inert globinclude.How it works
pmm.confgains a single line inside itsserverblock:A glob include whose directory is empty or absent is a no-op, so the shipped config is behaviourally inert with the flag off. The drop-in deliberately does not live in
conf.d/:nginx.confincludes that directory athttpcontext, wherelocationis not allowed, and nginx would refuse to start.The entrypoint renders
build/ansible/roles/nginx/files/sep/sep.conf.templateinto/etc/nginx/sep.d/sep.confwhenPMM_ENABLE_SEPis enabled, and clears the directory when it is not — idempotent in both directions. The template reaches the image via the existingCOPY ansible /opt/ansible, the same mechanism #5700 relies on forpostgres-sep. The existingnginx -tgate already runs before supervisord starts, so it validates the rendered drop-in for free.Two values are substituted at start:
set $sep_upstream "<host>:<port>";+proxy_pass http://$sep_upstream;. Because the upstream is a variable, nginx defers resolution to request time: the config loads with SEP absent, and a side-car that restarts on a new address is picked up within the resolver TTL without restarting pmm-server. The variable must carry no URI component — a trailing/replaces the request URI rather than stripping a prefix./etc/resolv.confat container start rather than hardcoded to Docker's127.0.0.11. The AMI and OVF images run this same entrypoint: their systemd unit ispodman run … --net pmm_default … ${PMM_IMAGE}with no command override, and the image'sCMDis the entrypoint. Under Podman that network is served by aardvark-dns at a different address — measured10.89.0.1there versus127.0.0.11under Docker — so a hardcoded resolver would have silently broken those distributions.Two details about the resolver that are easy to get wrong, and are the reason it looks the way it does:
location /sep/, not at the drop-in's top level. The drop-in is included in theserverblock, so a top-levelresolverwould override the http-levelresolver 8.8.8.8 8.8.4.4for every other location that resolves a name at request time —location = /percona-blog/feeddoes exactly that viaproxy_pass $feed. Scoping it keeps the flag from changing DNS for anything but/sep/.[emerg] invalid port in resolver, which failsnginx -tand therefore stops the whole server from booting — on a network the operator never configured for SEP. A scoped address (fe80::1%eth0) is skipped rather than stripped of its zone, and a resolv.conf offering nothing else is a named fatal: nginx rejects[fe80::1%eth0]as an invalid IPv6 address and has no other syntax for the interface scope, so dropping the zone would yield a config that passesnginx -tand can never route DNS — every/sep/request timing out into the 503 instead of the server refusing to start.PMM_SEP_ADDRESSis new, optional, and defaults tosep:9000. It is validated as<host>:<port>with the port bounded to 1–65535 before interpolation, because the value is written into an nginx config: an unvalidated one is a config-injection vector, and because a variableproxy_passis parsed per request, an out-of-range port would otherwise passnginx -tand surface only as a runtime 502.One
/sep/prefix, not five top-level namesThe issue left the namespace question open, and this settles it on a single
/sep/prefix. The alternative — proxying/api,/sep_app,/stream-logs,/execution-events,/files— claims five generic top-level names at pmm-server's document root for a side-car. PMM's own API lives at/v1/, so a top-level/apiowned by SEP forecloses the most obvious future name for PMM's own API surface./sep/reserves exactly one name and is self-describing.Stripping the prefix in nginx was considered and rejected: with SEP's
root_pathunset, itsurl_for-generated URLs in API payloads come back unprefixed, so stripping converts a one-line SEP change into an audit of every generated URL.The cost is that SEP must serve itself under the prefix — and it now does: SEP's app is constructed with
root_path=sep_settings.ROOT_PATH, and its shipped side-car profile setsROOT_PATH: /sep, which matches thelocation /sep/hardcoded here. PMM deliberately does not add aPMM_SEP_PREFIXvariable; the prefix is fixed topology on both sides, and a knob would create a second source of truth.One consequence worth stating: forwarding one prefix proxies SEP's whole surface, where the five-prefix design incidentally firewalled off everything it did not name. That is bounded by SEP's own auth (below) and shrinks further when SEP's legacy SSR layer is removed.
This made three of the issue's original acceptance criteria stale; they were realigned on the ticket before implementation, and the sibling FE tickets were updated to target
/sep/*.What changed
build/ansible/roles/nginx/files/conf.d/pmm.confincludeline insideserverbuild/ansible/roles/nginx/files/sep/sep.conf.template/sep/location, its resolver, and the unavailable-fallbackbuild/ansible/roles/nginx/tasks/main.yml/etc/nginx/sep.d/at build time (pmm:root,0775)build/docker/server/entrypoint.shmanaged/utils/envvars/parser.go(+ test)PMM_SEP_ADDRESSmanaged/services/server/logs.go(+ test)docker-compose.yml, the Compose sample environment filePMM_SEP_ADDRESSCreating
sep.d/at build time rather than only in the entrypoint is what makes the arbitrary-UID-with-GID-0 (OpenShift) case work.The support bundle globs
/etc/nginx/sep.d/*.confrather than naming the file: the collector records a read error as a zip entry and logs it at error level, so a statically listed path that is absent by design would put a spurious error in every bundle on the default configuration.TestFilesskips the drop-in for the same reason — it asserts an exact filename list, which cannot hold for a file whose presence depends on the flag.One line from #5700 is reworded: its warning read
ignoring PMM_ENABLE_SEP, the embedded PostgreSQL is not in use. That was accurate when the flag only drove the database exposure; now the flag also drives the reverse proxy, which is independent of which database SEP uses, so the message would over-claim.Trust boundary
/sep/setsauth_request off, so PMM contributes no authentication or authorization to anything under that prefix — all of it is SEP's. This is not incidental: pmm-managed's/auth_requestforwards the client'sAuthorizationheader to Grafana, so leaving it on would reject the SEP bearer the embedded UI is meant to send, even for a valid PMM session. AC-7 also forbids synthesizing headers here.The practical surface is narrow — SEP requires a bearer on its API and streaming routes, including GETs — but the enforcement lives entirely on SEP's side, and nothing in PMM would notice a SEP route added without it. Worth carrying into the Tech-Preview risk notes rather than leaving implicit.
Testing
shellcheck build/docker/server/entrypoint.shis clean (also at-S style), andgo test ./managed/utils/envvars/...passes with the newPMM_SEP_ADDRESSassertion added to the existing SEP subtest.make checkis clean.The rest was exercised in a container rig built from the real shipped configs — this branch's
nginx.conf,pmm.confandsep.conf.template, with the drop-in rendered by this branch's actual entrypoint block, onnginx:1.26-alpine(the versiontasks/main.ymlpins) — against an echo/stream stub standing in for the side-car, under Docker and Podman.nginx -t, flag off,sep.dempty and absentGET /sep/api/things?x=1&y=2/sep/api/things, queryx=1&y=2.confhost not found in upstream;/sep/returns the 503 JSON172.18.0.3→172.18.0.4, pmm-server not restarted101 Switching Protocolswith a bidirectional frameAuthorizationBearer …arrives byte-identical; server-levelX-Forwarded-Forstill reaches SEP/septemberand/sep-report, flag onPOST /sep/files/upload{"code":14,…}in 9 ms, not the maintenance pageBoth resolver behaviours were checked against a positive control rather than asserted:
/percona-blog/feed(set $probe http://sep:9000/health; proxy_pass $probe;) returns 502 with the shipped drop-in — public DNS cannot resolve a container name — and 200 when the sameresolveris moved to the drop-in's top level, i.e. the leak is real and the location scoping closes it, with/sep/still 200 either way./etc/resolv.confshapes. IPv4-only, IPv6-then-IPv4 and IPv6-only yield a resolver that passesnginx -t(10.0.0.53,10.0.0.53,[fd00::1]); scoped-then-unscoped now prefers the later usable nameserver ([fd00::1]); scoped-only and empty each exit 1 with their own message. Controls confirm the three forms nginx rejects: unbracketedfe80::1%eth0is[emerg] invalid port in resolver, bracketed-with-zone[fe80::1%eth0]is[emerg] invalid IPv6 address in resolver, and a barefd00::1isinvalid port. The scoped-only and scoped-then-unscoped rows were added after review — the first revision stripped the zone, which passednginx -tand produced a resolver that could not route.Also covered: percent-encoding survives un-decoded (
a%20b.txt); a 502 returned by SEP itself passes through unchanged rather than becoming the JSON body (proxy_intercept_errorsis off); bare/sep301s to/sep/; malformedPMM_SEP_ADDRESS, an out-of-range port, an empty/etc/resolv.confand a missing template each exit 1 with a named message and leave no partial drop-in; and the UID matrix —1000:0and4001:0succeed,2000:2000fails loudly. Under Podman on apmm_defaultnetwork the resolver was derived as10.89.0.1and requests proxied end-to-end, confirming the AMI/OVF path.Where this evidence stops. The rig is not a built PMM server image, so two claims are only partly discharged: AC-4's "the rest of PMM stays fully usable" and AC-8's "unaffected" were verified as non-shadowing (no stock path is captured by
/sep/, none leaks to the side-car) — the rig's stock upstreams are absent, so those locations return errors there by construction and their functional behaviour needs the feature build. AC-6's incremental delivery reproduced both with and withoutX-Accel-Buffering: no, so the rig confirms streaming works but does not isolate that header as the mechanism; real behaviour depends on SEP marking its own streams, which the no-proxy_buffering offdesign rests on. That dependency was subsequently checked against SEP's source rather than left as an assumption: the side-car runssep_app, and every streaming route that app serves — two inapp/sep/routes/stream_logs.py, one inapp/sep/routes/download_files.py— setsX-Accel-Buffering: no. TheStreamingResponses that do not are served by a different program on a different port and are unreachable through/sep/.TestFilesfails identically with and without this change (it needs a live PostgreSQL and container paths), so it neither covers nor is broken by thelogs.goedit; the glob extracted out of it is covered byTestSepConfigFiles, which needs neither.Known limitations
X-Forwarded-Protoreports the hop into nginx, not the client's original scheme. Behind an external TLS terminator forwarding to:8080, SEP seeshttpand may generatehttp://absolute URLs.mapcannot fix this from the drop-in, sincemapis http-context only and the drop-in is included insideserver. No other PMM location sends this header at all, so nothing regresses — but nothing tracks it either.proxy_passcannot reference anupstreamblock, andConnection $connection_upgradeisclosefor non-WebSocket requests, so every/sep/request opens a fresh connection to the side-car. Acceptable at Tech-Preview scale, worth knowing before anyone benchmarks it.client_max_body_size 100mis a bound, not a measurement. It clears the server-level 10m for SEP uploads; real SEP payload sizes have not been measured.proxy_read_timeoutis a blanket 3600s for the whole prefix; there is no per-route knowledge available to scope it, and the streams need it. A hung request holds a connection for an hour rather than 60s.nginx -s reload. This matches every other entrypoint-consumedPMM_*flag.maintenance.htmlexists, the server-level check runs in the rewrite phase, before location selection, so/sep/requests get the maintenance page too. Correct behaviour, but it bounds AC-10.PMM_SEP_ADDRESSaccepts no IPv6 literal. The validation regex admits a host of[A-Za-z0-9._-]plus a port, so[::1]:9000is rejected and the container exits 1 — before nginx, which would accept a bracketed literal in a variableproxy_pass, is ever consulted. A side-car reachable only over IPv6 cannot be addressed. Widening the regex is small, but it adds a branch to a container-start hard gate, so it belongs in the rig rather than in a late edit.postgres-sepexits 1 whenPMM_ENABLE_SEPis set andPMM_SEP_POSTGRES_PASSWORDis empty, and it runs earlier in the entrypoint underset -o errexit. So an operator who wants only the reverse proxy — SEP on its own database, PMM still on its builtin one — must supply a password nothing will use, or setPMM_DISABLE_BUILTIN_POSTGRESand accept its wider consequences. The reworded warning above covers the HA and disabled-builtin cases, not this one.X-Accel-Bufferingcontract. Leavingproxy_bufferingon rests on SEP marking its own streams. That holds for every streaming route the side-car serves today (verified above), but a route added later without the header is silently buffered, and the failure is invisible from PMM — no log, no warning, no test. Forwarding one prefix instead of five is what makes the set of routes this has to hold for open-ended.TestSepConfigFiles, over a temporary directory: drop-ins collected, non-.confignored, absent and empty each yielding nothing). What is still uncovered is that the collected path is read into the zip — that runs through the shared append loop infiles(), whose dozen-plus config paths are all hardcoded, so reaching it needs an injectable config root.Dependencies
Nothing is blocking merge into the epic branch any more: #5700 (PMM-15238) is on
main, and the FE re-pathing onto/sep/*reached this PR's base with #5653 (PMM-15216). The SEP-side prefix support is no longer blocking — SEP's app now takesroot_pathfrom settings and its side-car profile shipsROOT_PATH: /sep.A working feature build additionally needs the FE token exchange, the Grafana service account, and the ticket delivering SEP's secret key and database credentials — the side-car will not start without a secret key. Ordering inside the FB matters even though merge order does not: retiring the harness overlay must not land ahead of the FE token exchange, or the stack reports as working while silently failing every write. The harness also names its service differently from the
sep:9000default, soPMM_SEP_ADDRESSmust be set there; the failure looks like a proxy fault rather than a missing variable.The feature build is where the rig's blind spots get covered: SSE incrementality against real SEP, AC-5 recovery against a real restarted side-car, resolver derivation on an actual AMI, and the arbitrary-UID write path into
sep.d/in the built image.