320 lines
18 KiB
Markdown
320 lines
18 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.
|
|
8. Post-restart status, permission, and administrator-secret denial probes join
|
|
both the actual service mount and network namespaces. Effective systemd
|
|
network, address-family, socket-bind, syscall-filter, and LSM properties must
|
|
match the reviewed unrestricted transport contract so a host-context helper
|
|
cannot produce a false runtime receipt.
|
|
|
|
## 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/provision_gcp_leoclean_runtime_role.sh` as the Cloud SQL administrator
|
|
from the private VM path. Supply the new runtime password through
|
|
`TELEO_LEOCLEAN_DB_PASSWORD` and the administrator password through
|
|
`PGPASSWORD`. The wrapper captures and unsets both fields before its first
|
|
external child, pins the private endpoint and reviewed server CA, detaches
|
|
`psql` from the controlling terminal, and feeds the runtime value only to
|
|
psql's client-side `\password` prompt. The SQL file and argv never contain the
|
|
cleartext runtime password. The migration:
|
|
|
|
- creates or rotates `leoclean_kb_runtime`;
|
|
- changes any pre-existing `leoclean_kb_runtime` to `NOLOGIN`, removes its
|
|
membership edges, and terminates its existing sessions before password
|
|
handling; an interruption therefore leaves the old login disabled;
|
|
- enables `LOGIN` only as the last statement in the same transaction as the
|
|
database ACL, canonical grant, routine, large-object, and topology checks;
|
|
- creates a dedicated `NOLOGIN` function owner with no role memberships;
|
|
- removes stale table, column, sequence, function, and role-membership grants;
|
|
- inventories existing principals, replaces `PUBLIC CONNECT` with explicit
|
|
preserved grants, gives the runtime `CONNECT` only to `teleo_canonical`, and
|
|
leaves the staging owner with no database connection target;
|
|
- inventories and preserves existing non-scoped application-routine and
|
|
large-object access while removing `PUBLIC` and both scoped roles from all
|
|
other application routines and every persistent large-object mutator;
|
|
- 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;
|
|
- 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`, `template1`, and every other noncanonical database are actual
|
|
negative connection checks. PostgreSQL's startup protocol does not expose a
|
|
SQLSTATE through `psql`, so the verifier requires the exact C-locale
|
|
`permission denied for database` and `does not have CONNECT privilege`
|
|
diagnostics in addition to the catalog proof that the runtime's sole effective
|
|
target is `teleo_canonical`. 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 the complete
|
|
`/home/teleo` tree read-only, allows writes back only to the profile's `state`
|
|
and `workspace` subdirectories, clears every bounding and ambient capability,
|
|
and enforces `NoNewPrivileges`. Live verification requires the service home and
|
|
bound `bin` to be read-only, both allowed subdirectories to be writable, the
|
|
exact `teleo` group set, and every process capability field to be zero.
|
|
|
|
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. It also rejects effective credential
|
|
properties, auxiliary `Exec*` hooks, any `EnvironmentFile`, and any drop-in
|
|
ordered after the reviewed configuration. 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,
|
|
administrator-secret, excerpt, credential-mode, claim-base, and retry-count
|
|
overrides without printing their values. The reviewed root-owned empty
|
|
`CLOUDSDK_CONFIG`, pinned `PGSSLMODE=verify-ca`,
|
|
server CA, and Python isolation fields must match exactly. 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 and network namespaces, drops to `teleo`, and invokes the bound
|
|
wrapper without reinjecting any database identity setting.
|
|
|
|
The effective unit must use the host network namespace (`PrivateNetwork=no`),
|
|
have no `NetworkNamespacePath`, `IPAddressDeny`, `IPAddressAllow`,
|
|
`RestrictAddressFamilies`, `SocketBindAllow`, `SocketBindDeny`,
|
|
`RestrictNetworkInterfaces`, `SystemCallFilter`, `AppArmorProfile`, or
|
|
`SELinuxContext` override, and load no
|
|
later drop-in. The executable regression harness injects each conflicting
|
|
property and requires the gate to fail. These checks cover restrictions that a
|
|
new `nsenter` process would not inherit merely by sharing the service network
|
|
namespace.
|
|
|
|
For cgroup/eBPF restrictions, post-restart probes do not rely on enumeration.
|
|
A root-only bounded runner verifies the service's single cgroup-v2 membership
|
|
against systemd's effective `ControlGroup`, forks a stopped child, moves only
|
|
that child into the service cgroup, confirms the move, and then resumes it to
|
|
join the service namespaces and drop to `teleo`. It propagates exit or signal
|
|
status and reaps the child on every path. The child installs a Linux
|
|
parent-death `SIGKILL` before stopping, with a parent-PID race check; the parent
|
|
blocks and handles `INT`, `TERM`, and `HUP` through cleanup before re-raising
|
|
the signal. Before resume, every supported Linux `RLIMIT_*` soft/hard pair is
|
|
copied from the live `MainPID` with a stable source and child readback. Live
|
|
`MainPID` verification also requires seccomp mode and filter count zero, the
|
|
exact `unconfined` LSM context, `PrivateUsers=no`, and the same user-namespace
|
|
inode as PID 1, preventing a per-process filter, profile, resource ceiling, or
|
|
user namespace from being mistaken for the reviewed service context.
|
|
|
|
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 and network
|
|
namespaces, 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 and permission verifier execute from
|
|
inside the service mount and network namespaces; the status path invokes the
|
|
bound `teleo-kb` 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 \
|
|
/usr/bin/python3 -I /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 `lo_creat`, `lo_create`, and `lo_from_bytea`, zero effective
|
|
large-object mutator execution, and zero scoped large-object ownership/ACLs;
|
|
- 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 startup connections to both `teleo_kb` and `template1`, with the
|
|
catalog showing `teleo_canonical` as the sole runtime connection target and
|
|
zero remaining `PUBLIC CONNECT` grants;
|
|
- 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.
|