Task: Finish IAM per-service pilot: JWT signing failure and HEALTHCHECK
Table of Contents
This page documents a task in the Containerize the ORE Studio service runtime and verify it on a remote WSL host story. It captures the goal, current status, acceptance, and any notes or results.
Goal
Root-cause and fix the "JWT token creation failed: failed to load key:
bio read failed" error surfacing when IAM runs standalone in its
per-service container (past the already-fixed
--userns=keep-id~/NATS-cert issue) -- an in-memory PEM parsing/signing
failure in ~ores.security's JWT code, not a file/uid problem. Then add
a HEALTHCHECK to the per-service image (tailing IAM's own log for
"Service ready.", automating today's wait_for_log_ready polling loop
as a container-native probe) and confirm IAM reaches podman ps
healthy end-to-end, standalone, with no controller exec'ing it.
Status
| Field | Value |
|---|---|
| State | DONE |
| Parent story | Containerize the ORE Studio service runtime and verify it on a remote WSL host |
| Now | Nothing. |
| Waiting on | Nothing. |
| Next | Nothing. |
| Last touched | 2026-07-29 |
Acceptance
- Root cause found and fixed for the JWT signing failure; IAM's service-account self-authentication succeeds standalone in the per-service container.
- A
HEALTHCHECKdirective added to the per-service Dockerfile/build, reporting healthy once "Service ready." appears in IAM's own log. podman psshows the IAM container reachinghealthy, standalone, with no controller process exec'ing or monitoring it.podman logsshows IAM's own output – confirmed to require--log-to-consoleafter all (see Notes; the original assumption that no extra flag was needed didn't hold up empirically).
Plan
- Reproduce the "bio read failed" JWT signing failure standalone,
without assuming it's the same class of bug as PR #1779's escaped-
\nJWT env-file corruption – confirm empirically rather than assuming, since the pilot's session predates that fix. - If it is the same root cause, verify the fix (already merged in
docker/run-pod.sh~/~docker/generate-env.sh) actually resolves it for IAM standalone, rather than re-deriving a new fix. - Add a
HEALTHCHECKtodocker/service-runtime.Dockerfile, tailing IAM's own log for "Service ready." – chainguard/glibc-dynamic has no shell, so this needs a real binary, not a shell one-liner. - Verify
podman psshows IAMhealthystandalone, no controller involved.
Notes
JWT signing failure was already fixed by PR #1779 – same root cause, verified empirically
Reproduced IAM standalone in its per-service container (fresh build,
docker/stage-runtime.sh --service ores.iam.service), passing NATS/DB
config via explicit CLI args (no args_template row for ores.iam.service
in this environment's DB, so the controller's own default template's
substitutions had to be reconstructed by hand) and
ORES_IAM_SERVICE_JWT_PRIVATE_KEY via docker/.env plus the --env
"$(cat build/keys/iam-rsa-private.pem)" override PR #1779 added to
docker/run-pod.sh.
Result: the JWT key parsed correctly and IAM reached "Service ready."
– no "bio read failed" error, confirming this was not a distinct
bug needing its own fix. It's the exact same escaped-\n-in-an-env-
file corruption already root-caused and fixed generically in PR #1779
(docker/generate-env.sh's quote-stripping plus the --env
passthrough for this one variable); the per-service pilot's session
simply predated that fix. Confirmed by first reproducing the original
failure with the key passed only via a plain --env-file pointing
at docker/.env's escaped-\n value (fails, "Invalid tenant ID" was
actually a red herring encountered first – see below – but with the
--env override alone, in isolation, IAM signs and starts cleanly).
Along the way, hit and fixed two more standalone-specific setup issues, neither a real bug in shipped code:
--tenant system(the tenant code) is rejected byores.iam.service's own CLI parsing with "Invalid UUID string" – this binary's--tenantonly accepts a UUID directly, unlike some other services' tenant resolution. Needed the literal system-tenant UUID (ffffffff-ffff-ffff-ffff-ffffffffffff) instead. Not a code bug – just something the controller's own args_template building handles for the real fleet, that this manual standalone run had to replicate by hand.- The Newton-specific storage
EIOfinding (PR #1779) reproduces locally too, on this box's own filesystem, once--userns=keep-idremaps the container's uid – confirms that finding was never Newton-specific, just uid-remap-dependent (a mode-777 dir baked into the image's own overlay layer, written to as a different uid than built it, hits the sameEIOhere as on Newton). Host bind-mounting/app/log~/~/app/runinstead (same fix as PR #1779) resolves it.
Corrected assumption from the design task: podman logs needs --log-to-console after all
The design task's acceptance ("podman logs shows IAM's own output
directly … confirming no –log-to-console plumbing is needed") was
untested and turned out wrong: every service logs to a file only
(--log-enabled, no console output) by convention, so podman logs
captures nothing at all without --log-to-console also being passed
– podman's log driver only captures stdout/stderr, and none of our
services write there unless told to. Verified: with
--log-to-console added, podman logs shows the exact same output as
the log file, live. Not a blocker for the HEALTHCHECK itself (which
reads the log file directly, not podman logs), but worth recording
as the corrected understanding for future per-service rollout: each
service's container should pass --log-to-console if operators are
expected to use podman logs day-to-day.
HEALTHCHECK: a real binary, not a shell script
cgr.dev/chainguard/glibc-dynamic has no shell – no grep, no
test, nothing to script a log-tail check with, and Docker/podman's
HEALTHCHECK CMD exec form runs the given program directly rather
than through a shell regardless. Wrote a small standalone C program
(docker/healthcheck.c), statically linked in its own debian
healthcheck-build stage (so it carries no runtime dependency on the
final image's glibc at all), that scans every *.log file under
/app/log for the literal string "Service ready." and exits 0/1
accordingly – the same marker every service logs via
ores.service's *_runner_impl.hpp once it's registered its NATS
handlers, i.e. the exact string wait_for_log_ready already polls for
natively. Copied into /app/bin/healthcheck in the final stage,
wired via HEALTHCHECK --interval=2s --timeout=2s --start-period=60s
--retries=3 CMD ["./healthcheck"].
One gotcha: podman silently ignores HEALTHCHECK entirely (with only
a build-time warning, easy to miss) unless the image is built with
--format docker – the default OCI format has no health-check
concept. Confirmed via podman ps showing (healthy) only once
rebuilt with that flag.
Verified end-to-end: IAM standalone (no controller exec'ing it) reaches
Up ... (healthy) per podman ps, confirmed via
podman inspect --format '{{json .State.Health}}' showing 5+
consecutive passing health-check runs.
This is deliberately a pragmatic first cut, not the final design –
reading a log file from outside the process is fragile (couples every
container's health probe to log wording staying stable) and
duplicates state the service already holds. Filed as a follow-up
capture rather than building it now, since this task's job was to
prove HEALTHCHECK works at all, not to design the ideal probe:
Replace log-grep HEALTHCHECK with an in-process NATS health endpoint.
Test Scenarios
Manual QA scenarios (scaffolded via compass add test_scenario, run
through the QA Validation Runner panel) that verify this task. Link
new ones here as they're created; the scenario doc itself links back
via its "Verifies task" field.
| Scenario | State | Notes |
|---|---|---|
PRs
| PR | Title |
|---|---|
| #1787 | [docker] Finish IAM per-service pilot: JWT signing fix confirmed, HEALTHCHECK added |
Review
| # | Comment summary | File | Decision | Notes |
|---|---|---|---|---|
| 1 | –format docker gotcha only documented in agile notes, not next to the code | service-runtime.Dockerfile | Accepted | Added a comment directly above the HEALTHCHECK directive |
| 2 | Unexplained "18/22" in rollout task's acceptance | task_rollout-per-service-containers-remaining-17.org | Accepted | Clarified: 18 services, 22 containers counting compute.wrapper's 5 replicas |
| 3 | Unused #include <stdlib.h> | healthcheck.c | Accepted | Removed |
| 4 | Rescans whole log file every 2s indefinitely, even once already healthy | healthcheck.c | Declined (for now) | Accepted stopgap per the filed follow-up (service_health_endpoint_not_log_grep.org); not worth optimising a probe already slated for replacement |
| 5 | Scans every *.log file, not scoped to the current process instance like wait_for_log_ready | healthcheck.c | Declined (for now) | Correctly identified as currently safe (no in-container respawn logic exists); noted as a "watch for this" if the pattern is copied to services with different lifecycle |
Result
Both acceptance items resolved:
- The "bio read failed" JWT signing failure is fixed – turned out to
be the exact same escaped-
\nenv-file corruption already root- caused and fixed generically in PR #1779 (merged before this task started), not a distinct bug. Verified empirically standalone: IAM's own service-account JWT signing now succeeds in its per-service container, no source change needed here. docker/service-runtime.Dockerfilegained aHEALTHCHECK, backed by a small statically-linked C binary (docker/healthcheck.c, compiled in its own debian stage) since the final chainguard image has no shell to script a log-grep with. Verified: IAM standalone (no controller exec'ing it) reachesUp ... (healthy)perpodman ps, confirmed viapodman inspectshowing repeated passing health-check runs. Requires building withpodman build --format docker– podman silently dropsHEALTHCHECKunder the default OCI format.
Also corrected an untested assumption carried over from the design
task: podman logs shows nothing without --log-to-console also
passed, since every service logs to file only by convention – not
"already just works" as originally assumed. See Notes.
Not attempted here, as scoped from the start: the dependency-graph-to-
orchestration-spec generator, the podman events DB-mirroring
watcher, and rollout to the remaining 17 services.