Skip to content

device_monitoring: publish to ingest API; token from Secret Manager; hardened MSI cron - #134

Merged
runck014 merged 7 commits into
feature/device-monitoring-updatesfrom
devmon/msi-secret-fetch
Oct 16, 2026
Merged

runck014 merged 7 commits into
feature/device-monitoring-updatesfrom
devmon/msi-secret-fetch

Conversation

@runck014

Copy link
Copy Markdown
Contributor

devmon MSI publisher: ingest key from Secret Manager (FRI 2026-10-16)

MSI never holds the ingest token. Each run the publisher reads it from Secret Manager with a
service-account key that can read the two token secrets and nothing else. Nothing below has been
run against GCP or MSI; the patch is not pushed.

Flow

scrontab (agate, runck014) -> scripts/scheduled_device_monitoring.sh
  -> parse credentials/.rtgs_credentials as data (allowlist; DEVICEMON_INGEST_URL, GEMS DB,
     Particle, Gmail)
  -> checkout the tag+SHA in ~/.config/devmon/release (git hooks off)
  -> refuse if the script, its dir, the checkout, or the venv is group/world-writable
  -> rtgs device-monitoring check-ingest   (preflight: token fetch + /v1/status)
  -> rtgs device-monitoring monitor -> batch -> logs/batches/ -> POST /v1/batches
token fetch: ~/.config/devmon/sa.json (600, ours) -> device-monitoring-msi@rtgslab
  -> GET secretmanager .../secrets/<secret>/versions/latest:access -> memory only
secret follows the URL: test host -> devicemon-ingest-token-test,
                        prod -> devicemon-ingest-token.
The token is sent only to the 3 known ingest hostnames.

What MSI holds

  • ~/.config/devmon/sa.json: SA key, 600, Bryan's home. Can read the 2 token secrets, nothing
    else (secret-level bindings; no project roles).
  • ~/.config/devmon/release: devmon-vX.Y.Z <40-hex sha>, 600. Editing this line deploys.
  • shared credentials/.rtgs_credentials (660): URL + GEMS/Particle/Gmail. No token. A
    DEVICEMON_INGEST_TOKEN line is ignored and logged as "delete it".
  • code + venv + scripts/: Bryan-owned 2750 dirs, 640/750 files. logs/ + logs/batches/: 2770.

Order (Bryan)

  1. Here: review 03-output-20261016-devmon-rtgs-lab-tools.patch (7 commits on 7098865); push
    the branch, PR, merge, tag devmon-v0.1.0; note the tag's commit SHA.
  2. Here: bash 06-output-20261016-devmon-msi-sa-setup.sh (dry run), then
    DRY_RUN=0 bash 06-output-20261016-devmon-msi-sa-setup.sh (y/N per change).
  3. Run the ssh / scp / chmod lines it prints (Duo on each).
  4. agate: (umask 077; printf 'devmon-v0.1.0 <sha>\n' > ~/.config/devmon/release)
  5. agate, shared workspace (dir 04's install): move the radli009-owned scripts/ and
    rtgs-lab-tools-cron/ aside, re-create them as yours 2750; chmod 2770 logs logs/batches.
    The script refuses to run until the code side is yours and not group-writable.
  6. agate: set DEVICEMON_INGEST_URL to the TEST host in the credentials file, delete any token
    line, check: scripts/scheduled_device_monitoring.sh --check-creds credentials/.rtgs_credentials
  7. agate: test sequence below; one manual run; then the prod URL; then scrontab.

Test sequence (agate shell, venv active, against test)

T=https://device-monitoring-ingest-test-685278787484.us-central1.run.app
curl -s -o /dev/null -w '%{http_code}\n' $T/health                        # 200
curl -s -o /dev/null -w '%{http_code}\n' -X POST $T/v1/batches \
     -H 'Content-Type: application/json' -d '{}'                         # 401
DEVICEMON_INGEST_URL=$T rtgs device-monitoring check-ingest
  # token from projects/685278787484/secrets/devicemon-ingest-token-test/versions/1
  # /health HTTP 200; /v1/status HTTP 200
DEVICEMON_INGEST_URL=$T python -c '
import datetime as d; from rtgs_lab_tools.device_monitoring import publisher as p
n = d.datetime.now(d.timezone.utc); day = n.strftime("%Y-%m-%d")
print(p.publish(p.build_batch([], [], {}, n.strftime("%Y-%m-%d %H:%M"), day, day,
                              full_fleet=False)))'
  # 201 {"status": "stored", "monitoring_rows": 0, ...}; same minute again -> 200 duplicate

The empty batch writes one row to ingest_batches in device_monitoring_test (shows in
/v1/status); full_fleet=False so no logger is marked inactive. Token never on a command line.

Rotate the API key

Services pin token versions by number (checked 14:35: test -> devicemon-ingest-token-test:1,
prod -> devicemon-ingest-token:1; 05-context). MSI reads latest. So a version must never
be added without a redeploy, or MSI gets 401 until the service is redeployed.

  • Use workflow devmon-rotate-ingest-token.yml (rtgs-infrastructure): phase=add adds a
    version AND redeploys accepting new + previous. MSI picks up the new one next run; nothing to
    do on MSI. After one green run: phase=retire.
  • Its "Next step" summary still says to paste the token on MSI: stale, a one-line infra fix.

Rotate the SA key

gcloud iam service-accounts keys create ~/.config/devmon/sa.json.new \
  --iam-account=device-monitoring-msi@rtgslab.iam.gserviceaccount.com --project=rtgslab
(run under umask 077) ; scp it to agate:.config/devmon/sa.json.new ; on agate:
chmod 600 ~/.config/devmon/sa.json.new && mv ~/.config/devmon/sa.json{.new,}
rtgs device-monitoring check-ingest        # must say "token from ..."
gcloud iam service-accounts keys delete <old key id> --iam-account=... --project=rtgslab

gcloud iam service-accounts keys list --iam-account=... --managed-by=user shows the IDs.

Open

  • The credentials file's variable names were never read; --check-creds lists what it accepts.
  • LOGGING_INSTANCE_CONNECTION_NAME and the direct Cloud SQL path are not accepted (removed).
  • Local ~/.config/devmon/msi.env still holds the prod token from dir 02; not needed on MSI.

🤖 Generated with Claude Code

https://claude.ai/code/session_0149VSJ7dmCkXd4UcUjQbG9R

runck014 and others added 7 commits October 16, 2026 11:56
A fresh install resolves SQLAlchemy 2.1, whose default PostgreSQL driver
is psycopg (v3), which is not a dependency; every GEMS query then fails
with "No module named 'psycopg'" (seen 2026-10-14 in a clean venv of
7098865). uv.lock already holds 2.0.41, but install.sh runs `uv pip
install -e .[all]`, which does not read the lock, so a rebuilt MSI venv
would break the same way.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
GEMS is reachable only inside UMN, so the cron on MSI stays the puller.
With DEVICEMON_INGEST_URL and DEVICEMON_INGEST_TOKEN set, monitor() now
saves the run as a devmon-batch/1 JSON file and POSTs it to rtgs-
infrastructure's device-monitoring-ingest (retries with backoff on
network errors, 429 and 5xx) instead of writing Cloud SQL with a
service-account key. Without them the old direct write still runs.

produce_db gains monitoring_records / logger_info_records /
app_config_defaults so both paths store identical values. New command:
rtgs device-monitoring publish FILE re-sends a saved batch (idempotent).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
… versioned scrontab

The script passed --start_date/--end_date/--node_ids; the CLI defines
--start-date/--end-date/--node-ids, so any dated run failed with "No
such option".

The cron now runs a release tag instead of git pull on whatever branch
the clone is on: DEVICEMON_RELEASE=devmon-vX.Y.Z pins it (a one-line
edit on MSI deploys), track:devmon-v* follows the newest tag (a tag push
deploys). If the release carries a newer copy of this script it re-runs
that copy once. Credentials (main file plus ~/.rtgs_creds.d/*.env drop-
ins) load before the checkout; the run refuses to start with no publish
target instead of writing SQLite inside the clone. The scrontab line
lives in scrontab.msi.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
bash -n on the cron script, publisher tests (schema, batch id, retry
policy, flag names), and the CLI flags the cron passes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Defaults to /projects/standard/runck014/shared/device_monitoring (Bryan's
scrontab): DEVICEMON_BASE_DIR, DEVICEMON_WORK_DIR, DEVICEMON_LOG_DIR,
DEVICEMON_CREDS_FILE (credentials/.rtgs_credentials, 660 group runck014),
DEVICEMON_BATCH_DIR (logs/batches). umask 007. Per-user ~/.rtgs_credentials
and ~/.rtgs_creds remain fallbacks; ~/.rtgs_creds.d is dropped.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_0149VSJ7dmCkXd4UcUjQbG9R
The publisher reads devicemon-ingest-token (prod URL) or devicemon-ingest-token-test (test URL)
from Secret Manager at run time with a service-account key (DEVICEMON_SA_KEY, default
~/.config/devmon/sa.json) and holds it in memory only, so rotating the token never touches
MSI. The key must be a regular file owned by the running user, mode 600; anything else fails
closed. The token is sent only to the known ingest hosts, and the secret must match the host.
DEVICEMON_INGEST_TOKEN stays as an explicit test override.

- new `rtgs device-monitoring check-ingest`: fetch the token, call /health and /v1/status,
  print the secret version and HTTP statuses (exit 2 setup, 3 transient)
- save_batch creates files with O_EXCL|O_NOFOLLOW (the batch dir is group-writable)
…logs

- the shared credentials file is parsed (KEY=VALUE, allowlist), never sourced; *_DIR, *_FILE,
  PATH, LD_*, PYTHON*, GOOGLE_*, DEVICEMON_RELEASE and the token are refused from it
- release pin from ~/.config/devmon/release ("<tag> <sha>"), both checked; git runs with
  core.hooksPath=/dev/null; no fetch --tags --force; track: mode removed
- refuses to run if this script, its dir, the checkout or the venv is group/world-writable
  or not owned by the running user; umask 027
- log file created by mktemp and held open on fd 3; lock in ~/.cache/devmon
- check-ingest preflight before the pipeline; DEVICEMON_INGEST_TOKEN unset in the cron
- --check-creds <file> lists accepted/rejected names (value lengths only)
- CI runs all device_monitoring tests
@runck014
runck014 merged commit 80ce479 into feature/device-monitoring-updates Oct 16, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant