teleo-infrastructure/docs/gcp-leoclean-runtime-reconciliation.md
2026-07-15 04:40:26 +02:00

267 lines
14 KiB
Markdown

# GCP Leo Runtime Reconciliation
## Scope
This runbook reconciles PR `#148`, the least-privilege follow-up to PR `#145`,
with the non-production GCP parallel Leo service and removes its dependency on
the PostgreSQL administrator credential.
It does not promote GCP, change the Telegram destination, copy newer VPS data,
or make `teleo_canonical` production-authoritative.
Target surfaces:
- project: `teleo-501523`;
- VM: `teleo-prod-1` in `europe-west6-a`;
- service: `leoclean-gcp-prod-parallel.service`;
- Cloud SQL database: `teleo_canonical` on private address `10.61.0.3`;
- runtime database role: `leoclean_kb_runtime`;
- staging-function owner: `leoclean_kb_stage_owner` (`NOLOGIN`);
- runtime secret: `gcp-teleo-pgvector-standby-leoclean-kb-runtime-password`.
Observed before reconciliation on 2026-07-14: the service was
`active/running`, but `/usr/local/bin/teleo-kb` matched unmerged PR `#145`, the
effective systemd environment selected database user `postgres` and the
administrator password secret, and a live `teleo-kb status` call exceeded a
20-second bound. Secret Manager access and private PostgreSQL reachability both
passed independently, isolating the timeout to the old helper path rather than
IAM or networking.
## Required End State
1. The live wrapper and Cloud SQL helper hashes match a merged repository
commit.
2. `TELEO_KB_MODE=cloudsql`; missing tools or credentials fail closed.
3. Canonical zero-hit searches do not consult `teleo_restore` unless an
operator explicitly opts in. The service is pinned to database
`teleo_canonical` with `TELEO_CLOUDSQL_ENABLE_AUDIT_FALLBACK=0`.
4. Leo can read the named canonical tables and stage only a
`pending_review` proposal through `kb_stage.stage_leoclean_proposal(...)`.
5. Leo cannot directly insert, update, or delete `public.*` or
`kb_stage.kb_proposals`, forge the proposer identity, `SET ROLE` into a
broader principal, or execute reviewer/apply functions.
6. The VM runtime identity can access only the scoped password secret needed by
this path, not the PostgreSQL administrator password secret.
7. Deployment proves the attached VM service account from the metadata server,
uses an empty phase-local Cloud SDK configuration, and rolls the complete
prior runtime set back if installation or post-restart verification fails.
## Safe Order Of Operations
### 1. Read-only access and IAM audit
Confirm the operator can read the project, use IAP/OS Login for the VM, inspect
Cloud SQL metadata, and inspect Secret Manager IAM. Identify the VM service
account and determine whether its secret access is project-wide or
secret-specific. Do not remove a project-wide binding until every legitimately
required runtime secret has an equivalent secret-level binding.
### 2. Merge reviewed repository code
Run CI on the completed PR `#148` repair before deployment. The deployment
source must be the resulting merged commit, not a working tree or unmerged
branch.
### 3. Create the scoped secret and grant only the VM runtime identity
Create the scoped secret without printing its value. Add one randomly generated
version, then grant `roles/secretmanager.secretAccessor` on that secret to the
VM runtime service account. Do not put `PGPASSWORD` in systemd, a repository,
an artifact, or a command transcript.
### 4. Provision the scoped PostgreSQL role once
Run `ops/gcp_leoclean_runtime_role.sql` as the Cloud SQL administrator from the
private VM path. Supply the new runtime password through
`TELEO_LEOCLEAN_DB_PASSWORD`; the SQL file does not contain it. The migration:
- creates or rotates `leoclean_kb_runtime`;
- keeps `leoclean_kb_runtime` `NOLOGIN` while both the `teleo_canonical` grant
phase and the legacy `teleo_kb` denial-verification phase run, and enables
`LOGIN` only in the final transaction after both phases pass;
- creates a dedicated `NOLOGIN` function owner with no role memberships;
- removes stale table, column, sequence, function, and role-membership grants;
- grants exact canonical reads;
- creates a locked security-definer staging function that hard-codes both
`pending_review` and canonical proposer `leo`;
- grants no direct table writes;
- removes all access to the legacy `teleo_restore` schema;
- aborts if either scoped role owns or can reach anything outside the explicit
allowlist.
PostgreSQL grants `TEMP` to `PUBLIC` by default. The migration removes any
direct scoped `TEMP` grant and reports the remaining effective privilege, but
does not revoke `TEMP` from `PUBLIC`: PostgreSQL has no per-role deny, so that
would be a database-wide behavior change requiring a separate inventory of
`kb_apply`, reviewer, and operator use. The staging function remains protected
by a `pg_catalog, pg_temp` search path, schema-qualified relations, a fixed
`session_user`, and a tested temporary-object shadowing denial.
The administrator password is used only for this bounded bootstrap. Retain no
password output.
`teleo_kb` can still be connectable through its existing `PUBLIC CONNECT`
grant. That database-wide default is not treated as runtime authority. The
migration denies the scoped role access to the `teleo_restore` schema, the
runtime client is pinned to `teleo_canonical`, and audit fallback remains
disabled with `TELEO_CLOUDSQL_ENABLE_AUDIT_FALLBACK=0`.
### 5. Preflight and deploy the merged runtime
From the exact merged checkout, run:
```bash
deploy/sync-gcp-leoclean-runtime.sh --dry-run
deploy/sync-gcp-leoclean-runtime.sh --preflight-only
deploy/sync-gcp-leoclean-runtime.sh --restart
```
`--preflight-only` extracts the candidate helper and permission verifier into a
root-created, root-owned, non-writable VM payload directory, verifies the actual
service `MainPID` environment, runs the candidate private Cloud SQL status path,
and runs the sanitized positive-and-negative permission verifier. A separate
`teleo`-owned directory holds only sanitized receipts; it never feeds an install
or rollback. The preflight does not stop or restart the service and does not
install or replace runtime files. It
captures `ActiveState`, `SubState`, `MainPID`, and `NRestarts` before and after
the checks and fails if any value changes. The preflight must pass before
`--restart` is attempted.
The deploy script refuses dirty, symlinked, or unmerged runtime files and
refuses a live install without `--restart`. Its payload and rollback backup are
root-owned and non-writable by `teleo`; existing runtime intermediates and
targets must be real root-owned, non-writable paths. After preflight it stops
the parallel service, atomically replaces the existing Cloud SQL systemd drop-in, installs the
reviewed wrapper, helper, and permission verifier into the root-controlled
`/usr/local/libexec/livingip/leoclean-kb` directory, records the Git revision,
and verifies every installed hash, owner, mode, path type, and revision before
systemd reloads the unit or starts the service. The wrapper uses fixed
`/bin/bash` and `/usr/bin/python3` interpreters, and every directory on the
service `PATH` must be root-owned and non-writable by `teleo`. Every ancestor of that
directory, the directory itself, the wrapper, helper, verifier, and revision
marker must be non-writable by `teleo`. The systemd service receives that
directory as a read-only bind at its existing
`/home/teleo/.hermes/profiles/leoclean/bin` path; verification compares the
source and service-namespace inode and hashes and proves the mount is read-only.
To prevent the `teleo` service user from renaming a writable ancestor and
recreating that absolute path, the service namespace mounts
`/home/teleo/.hermes` read-only, allows writes back only to the profile's
`state` and `workspace` subdirectories, and removes `CAP_SYS_ADMIN`. Live
verification requires `.hermes` and `bin` to be read-only, both allowed
subdirectories to be writable, `/home` to be non-writable by `teleo`, and the
effective capability set to exclude `CAP_SYS_ADMIN`.
Preflight, post-restart, and `--verify-only` checks inspect the environment of the actual
systemd `MainPID` through `/proc/<MainPID>/environ`, rather than trusting the
configured unit environment alone. A standalone fail-closed verifier requires
the exact reviewed private target and runtime role, requires
`TELEO_GCP_METADATA_ONLY=1`, and rejects all PostgreSQL, Cloud SDK, proxy, TLS
trust, broader Google credential, shell-startup, dynamic-loader, Python-loader,
and administrator-secret overrides without printing their values. Effective
`EnvironmentFile` use and drop-ins ordered after the reviewed configuration are
rejected before restart. This blocks
pre-wrapper injection through fields such as `BASH_ENV`, any `LD_*`,
`PYTHONPATH`, `PYTHONHTTPSVERIFY`, or `SSLKEYLOGFILE`. The checks also require
the reviewed drop-in to appear in `DropInPaths`. The wrapper status probe then
imports the validated environment from the actual `MainPID` with `nsenter
--env`, enters its mount namespace, drops to `teleo`, and invokes the bound
wrapper without reinjecting any database identity setting.
The service runtime bypasses Cloud SDK configuration and credential caches
entirely. The helper obtains the attached service-account email and a bounded
access token from the fixed GCE metadata endpoint, requires the exact expected
service account, and accesses only the named scoped Secret Manager version over
the fixed HTTPS API. Its URL opener disables proxies. The scoped status read
and administrator-secret IAM denial are rerun after restart. The denial probe
imports the actual systemd `MainPID` environment and mount namespace, drops to
the service's `teleo` identity, and must observe the exact
`secretmanager.versions.access` permission denial. No token or
secret value is retained. The status read executes the bound `teleo-kb` wrapper
from inside the service mount namespace, proving the wrapper and its adjacent
helper rather than bypassing them with a direct helper invocation. A failure
after mutation restores the prior runtime/drop-in directory existence and
metadata, wrapper, helper, both verifiers, drop-in, and revision as one set,
validates it against the root-only backup, and only then reloads systemd and
restarts the old service.
### 6. Verify positive and negative behavior
`--preflight-only`, `--restart`, and `--verify-only` run the verifier as the
`teleo` Unix user. To rerun the installed verifier directly on the VM and
retain its sanitized JSON receipt, use:
```bash
receipt_dir="$(mktemp -d /tmp/leoclean-permission-receipt.XXXXXX)"
sudo chown teleo:teleo "$receipt_dir"
sudo chmod 0700 "$receipt_dir"
run_id="$(date -u +%Y%m%d%H%M%S)-manual"
sudo -n -u teleo env -i HOME=/home/teleo \
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \
python3 /usr/local/libexec/livingip/leoclean-kb/verify_gcp_leoclean_runtime_permissions.py \
--run-id "$run_id" \
--output "$receipt_dir/permission-receipt.json"
sudo stat -c '%U:%G:%a' "$receipt_dir/permission-receipt.json"
```
The command must exit zero, print the same canonical JSON that it writes, and
leave the receipt as `teleo:teleo:600`. A passing receipt must report
`status=pass`, `mode=live_private_gcp_staging`, and both `required_tier` and
`current_tier` as `T3_live_readonly`. Those values are a receipt contract, not
a claim that this runbook has already been exercised successfully on the live
staging VM.
Retain a redacted receipt containing:
- merged Git commit and deployed file hashes;
- service `ActiveState`, `SubState`, `MainPID`, and `NRestarts`;
- metadata-server identity and an access token obtained with an empty Cloud SDK
configuration;
- `current_database()` and `current_user` showing
`teleo_canonical|leoclean_kb_runtime`;
- private server address `10.61.0.3`, port `5432`, and an active SSL session;
- exact safe runtime-role attributes and zero membership edges in
`pg_auth_members`;
- zero effective database/schema `CREATE`, persistent ownership, table/column
DML, sequence privileges, SELECT outside the exact table allowlist, or
executable unexpected `SECURITY DEFINER` functions, plus the exact staging
function owner/search-path/ACL contract;
- a real canonical status/search receipt;
- a transaction-rolled-back call to `stage_leoclean_proposal(...)` that returns
`pending_review` for canonical proposer `leo`, with zero matching canary rows
both before and after the transaction;
- denied direct insert, update, and delete on `kb_stage.kb_proposals`;
- denied insert, update, and delete on canonical `public.claims`;
- denied reviewer/apply security-definer functions;
- exact function-catalog posture: only the five-argument staging function is
executable, the forged six-argument overload is absent, and each expected
reviewer/apply function exists but is not executable by the runtime role;
- unavailable forged-proposer overload and denied `SET ROLE` escalation into
the staging owner, reviewer, apply, or administrator roles;
- denied access to `teleo_restore` even when `teleo_kb` remains connectable;
- readable scoped runtime secret and an IAM permission denial for the
administrator secret, with neither secret value retained;
- zero Telegram messages and zero committed canary rows.
### 7. Remove administrator-secret access
Only after the scoped service passes post-restart verification, remove the VM
runtime identity's access path to
`gcp-teleo-pgvector-standby-postgres-password`. Verify from the VM runtime
identity that the scoped secret is readable and the administrator secret is
denied. Never delete the administrator secret merely to enforce runtime least
privilege; backup/restore operators may still require it under a separate
identity.
## Stop Conditions
Stop without cutover if any of these are true:
- the deployed revision is not merged;
- the scoped status preflight fails;
- the role has direct proposal-table insert or canonical write permission;
- any approval/apply function is executable by the runtime role;
- removing broad Secret Manager access would break another required secret;
- GCP canonical rows are still stale relative to the chosen authority.
The last condition does not invalidate this runtime security repair. It means
GCP remains staging and data reconciliation stays a separate, explicitly
authorized slice.