fix: prove least-privilege GCP Leo runtime context
This commit is contained in:
parent
d299f1cdf5
commit
7b16c6d3cc
18 changed files with 2840 additions and 536 deletions
File diff suppressed because it is too large
Load diff
|
|
@ -44,6 +44,11 @@ IAM or networking.
|
||||||
7. Deployment proves the attached VM service account from the metadata server,
|
7. Deployment proves the attached VM service account from the metadata server,
|
||||||
uses an empty phase-local Cloud SDK configuration, and rolls the complete
|
uses an empty phase-local Cloud SDK configuration, and rolls the complete
|
||||||
prior runtime set back if installation or post-restart verification fails.
|
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
|
## Safe Order Of Operations
|
||||||
|
|
||||||
|
|
@ -70,21 +75,33 @@ an artifact, or a command transcript.
|
||||||
|
|
||||||
### 4. Provision the scoped PostgreSQL role once
|
### 4. Provision the scoped PostgreSQL role once
|
||||||
|
|
||||||
Run `ops/gcp_leoclean_runtime_role.sql` as the Cloud SQL administrator from the
|
Run `ops/provision_gcp_leoclean_runtime_role.sh` as the Cloud SQL administrator
|
||||||
private VM path. Supply the new runtime password through
|
from the private VM path. Supply the new runtime password through
|
||||||
`TELEO_LEOCLEAN_DB_PASSWORD`; the SQL file does not contain it. The migration:
|
`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`;
|
- creates or rotates `leoclean_kb_runtime`;
|
||||||
- keeps `leoclean_kb_runtime` `NOLOGIN` while both the `teleo_canonical` grant
|
- changes any pre-existing `leoclean_kb_runtime` to `NOLOGIN`, removes its
|
||||||
phase and the legacy `teleo_kb` denial-verification phase run, and enables
|
membership edges, and terminates its existing sessions before password
|
||||||
`LOGIN` only in the final transaction after both phases pass;
|
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;
|
- creates a dedicated `NOLOGIN` function owner with no role memberships;
|
||||||
- removes stale table, column, sequence, function, and role-membership grants;
|
- 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;
|
- grants exact canonical reads;
|
||||||
- creates a locked security-definer staging function that hard-codes both
|
- creates a locked security-definer staging function that hard-codes both
|
||||||
`pending_review` and canonical proposer `leo`;
|
`pending_review` and canonical proposer `leo`;
|
||||||
- grants no direct table writes;
|
- 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
|
- aborts if either scoped role owns or can reach anything outside the explicit
|
||||||
allowlist.
|
allowlist.
|
||||||
|
|
||||||
|
|
@ -99,11 +116,13 @@ by a `pg_catalog, pg_temp` search path, schema-qualified relations, a fixed
|
||||||
The administrator password is used only for this bounded bootstrap. Retain no
|
The administrator password is used only for this bounded bootstrap. Retain no
|
||||||
password output.
|
password output.
|
||||||
|
|
||||||
`teleo_kb` can still be connectable through its existing `PUBLIC CONNECT`
|
`teleo_kb`, `template1`, and every other noncanonical database are actual
|
||||||
grant. That database-wide default is not treated as runtime authority. The
|
negative connection checks. PostgreSQL's startup protocol does not expose a
|
||||||
migration denies the scoped role access to the `teleo_restore` schema, the
|
SQLSTATE through `psql`, so the verifier requires the exact C-locale
|
||||||
runtime client is pinned to `teleo_canonical`, and audit fallback remains
|
`permission denied for database` and `does not have CONNECT privilege`
|
||||||
disabled with `TELEO_CLOUDSQL_ENABLE_AUDIT_FALLBACK=0`.
|
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
|
### 5. Preflight and deploy the merged runtime
|
||||||
|
|
||||||
|
|
@ -143,41 +162,71 @@ directory as a read-only bind at its existing
|
||||||
`/home/teleo/.hermes/profiles/leoclean/bin` path; verification compares the
|
`/home/teleo/.hermes/profiles/leoclean/bin` path; verification compares the
|
||||||
source and service-namespace inode and hashes and proves the mount is read-only.
|
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
|
To prevent the `teleo` service user from renaming a writable ancestor and
|
||||||
recreating that absolute path, the service namespace mounts
|
recreating that absolute path, the service namespace mounts the complete
|
||||||
`/home/teleo/.hermes` read-only, allows writes back only to the profile's
|
`/home/teleo` tree read-only, allows writes back only to the profile's `state`
|
||||||
`state` and `workspace` subdirectories, and removes `CAP_SYS_ADMIN`. Live
|
and `workspace` subdirectories, clears every bounding and ambient capability,
|
||||||
verification requires `.hermes` and `bin` to be read-only, both allowed
|
and enforces `NoNewPrivileges`. Live verification requires the service home and
|
||||||
subdirectories to be writable, `/home` to be non-writable by `teleo`, and the
|
bound `bin` to be read-only, both allowed subdirectories to be writable, the
|
||||||
effective capability set to exclude `CAP_SYS_ADMIN`.
|
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
|
Preflight, post-restart, and `--verify-only` checks inspect the environment of the actual
|
||||||
systemd `MainPID` through `/proc/<MainPID>/environ`, rather than trusting the
|
systemd `MainPID` through `/proc/<MainPID>/environ`, rather than trusting the
|
||||||
configured unit environment alone. A standalone fail-closed verifier requires
|
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
|
the exact reviewed private target and runtime role, requires
|
||||||
`TELEO_GCP_METADATA_ONLY=1`, and rejects all PostgreSQL, Cloud SDK, proxy, TLS
|
`TELEO_GCP_METADATA_ONLY=1`, and rejects all PostgreSQL, Cloud SDK, proxy, TLS
|
||||||
trust, broader Google credential, shell-startup, dynamic-loader, Python-loader,
|
trust, broader Google credential, shell-startup, dynamic-loader, Python-loader,
|
||||||
and administrator-secret overrides without printing their values. Effective
|
administrator-secret, excerpt, credential-mode, claim-base, and retry-count
|
||||||
`EnvironmentFile` use and drop-ins ordered after the reviewed configuration are
|
overrides without printing their values. The reviewed root-owned empty
|
||||||
rejected before restart. This blocks
|
`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_*`,
|
pre-wrapper injection through fields such as `BASH_ENV`, any `LD_*`,
|
||||||
`PYTHONPATH`, `PYTHONHTTPSVERIFY`, or `SSLKEYLOGFILE`. The checks also require
|
`PYTHONPATH`, `PYTHONHTTPSVERIFY`, or `SSLKEYLOGFILE`. The checks also require
|
||||||
the reviewed drop-in to appear in `DropInPaths`. The wrapper status probe then
|
the reviewed drop-in to appear in `DropInPaths`. The wrapper status probe then
|
||||||
imports the validated environment from the actual `MainPID` with `nsenter
|
imports the validated environment from the actual `MainPID` with `nsenter
|
||||||
--env`, enters its mount namespace, drops to `teleo`, and invokes the bound
|
--env`, enters its mount and network namespaces, drops to `teleo`, and invokes the bound
|
||||||
wrapper without reinjecting any database identity setting.
|
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
|
The service runtime bypasses Cloud SDK configuration and credential caches
|
||||||
entirely. The helper obtains the attached service-account email and a bounded
|
entirely. The helper obtains the attached service-account email and a bounded
|
||||||
access token from the fixed GCE metadata endpoint, requires the exact expected
|
access token from the fixed GCE metadata endpoint, requires the exact expected
|
||||||
service account, and accesses only the named scoped Secret Manager version over
|
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
|
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
|
and administrator-secret IAM denial are rerun after restart. The denial probe
|
||||||
imports the actual systemd `MainPID` environment and mount namespace, drops to
|
imports the actual systemd `MainPID` environment and mount and network
|
||||||
|
namespaces, drops to
|
||||||
the service's `teleo` identity, and must observe the exact
|
the service's `teleo` identity, and must observe the exact
|
||||||
`secretmanager.versions.access` permission denial. No token or
|
`secretmanager.versions.access` permission denial. No token or
|
||||||
secret value is retained. The status read executes the bound `teleo-kb` wrapper
|
secret value is retained. The status read and permission verifier execute from
|
||||||
from inside the service mount namespace, proving the wrapper and its adjacent
|
inside the service mount and network namespaces; the status path invokes the
|
||||||
helper rather than bypassing them with a direct helper invocation. A failure
|
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
|
after mutation restores the prior runtime/drop-in directory existence and
|
||||||
metadata, wrapper, helper, both verifiers, drop-in, and revision as one set,
|
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
|
validates it against the root-only backup, and only then reloads systemd and
|
||||||
|
|
@ -196,7 +245,7 @@ sudo chmod 0700 "$receipt_dir"
|
||||||
run_id="$(date -u +%Y%m%d%H%M%S)-manual"
|
run_id="$(date -u +%Y%m%d%H%M%S)-manual"
|
||||||
sudo -n -u teleo env -i HOME=/home/teleo \
|
sudo -n -u teleo env -i HOME=/home/teleo \
|
||||||
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \
|
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 \
|
/usr/bin/python3 -I /usr/local/libexec/livingip/leoclean-kb/verify_gcp_leoclean_runtime_permissions.py \
|
||||||
--run-id "$run_id" \
|
--run-id "$run_id" \
|
||||||
--output "$receipt_dir/permission-receipt.json"
|
--output "$receipt_dir/permission-receipt.json"
|
||||||
sudo stat -c '%U:%G:%a' "$receipt_dir/permission-receipt.json"
|
sudo stat -c '%U:%G:%a' "$receipt_dir/permission-receipt.json"
|
||||||
|
|
@ -230,13 +279,17 @@ Retain a redacted receipt containing:
|
||||||
both before and after the transaction;
|
both before and after the transaction;
|
||||||
- denied direct insert, update, and delete on `kb_stage.kb_proposals`;
|
- denied direct insert, update, and delete on `kb_stage.kb_proposals`;
|
||||||
- denied insert, update, and delete on canonical `public.claims`;
|
- 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;
|
- denied reviewer/apply security-definer functions;
|
||||||
- exact function-catalog posture: only the five-argument staging function is
|
- exact function-catalog posture: only the five-argument staging function is
|
||||||
executable, the forged six-argument overload is absent, and each expected
|
executable, the forged six-argument overload is absent, and each expected
|
||||||
reviewer/apply function exists but is not executable by the runtime role;
|
reviewer/apply function exists but is not executable by the runtime role;
|
||||||
- unavailable forged-proposer overload and denied `SET ROLE` escalation into
|
- unavailable forged-proposer overload and denied `SET ROLE` escalation into
|
||||||
the staging owner, reviewer, apply, or administrator roles;
|
the staging owner, reviewer, apply, or administrator roles;
|
||||||
- denied access to `teleo_restore` even when `teleo_kb` remains connectable;
|
- 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
|
- readable scoped runtime secret and an IAM permission denial for the
|
||||||
administrator secret, with neither secret value retained;
|
administrator secret, with neither secret value retained;
|
||||||
- zero Telegram messages and zero committed canary rows.
|
- zero Telegram messages and zero committed canary rows.
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/python3 -I
|
||||||
"""Cloud SQL memory and KB-proposal bridge for the GCP leoclean profile.
|
"""Cloud SQL memory and KB-proposal bridge for the GCP leoclean profile.
|
||||||
|
|
||||||
Canonical claim/strategy application remains review-gated. The production-like
|
Canonical claim/strategy application remains review-gated. The production-like
|
||||||
|
|
@ -17,8 +17,10 @@ import hashlib
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
import stat
|
||||||
import subprocess
|
import subprocess
|
||||||
import urllib.request
|
import urllib.request
|
||||||
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
STOPWORDS = {
|
STOPWORDS = {
|
||||||
|
|
@ -84,6 +86,12 @@ RUNTIME_GCP_PROJECT = "teleo-501523"
|
||||||
RUNTIME_CLOUDSQL_HOST = "10.61.0.3"
|
RUNTIME_CLOUDSQL_HOST = "10.61.0.3"
|
||||||
RUNTIME_CLOUDSQL_PORT = "5432"
|
RUNTIME_CLOUDSQL_PORT = "5432"
|
||||||
RUNTIME_CLOUDSQL_DB = "teleo_canonical"
|
RUNTIME_CLOUDSQL_DB = "teleo_canonical"
|
||||||
|
RUNTIME_SYSTEM_IDENTIFIER = "7659718422914359312"
|
||||||
|
RUNTIME_CLOUDSDK_CONFIG = "/usr/local/libexec/livingip/leoclean-kb/gcloud-config"
|
||||||
|
RUNTIME_PSQL_BIN = "/usr/bin/psql"
|
||||||
|
RUNTIME_SSL_MODE = "verify-ca"
|
||||||
|
RUNTIME_SSL_ROOT_CERT = "/usr/local/libexec/livingip/leoclean-kb/cloudsql-server-ca.pem"
|
||||||
|
RUNTIME_SSL_ROOT_CERT_SHA256 = "80701e768f0e1f6b9d621aa0b53f6e851daaa276c6d9a8e51a300fbc015539cb"
|
||||||
RUNTIME_SERVICE_ACCOUNT = "sa-teleo-prod-vm@teleo-501523.iam.gserviceaccount.com"
|
RUNTIME_SERVICE_ACCOUNT = "sa-teleo-prod-vm@teleo-501523.iam.gserviceaccount.com"
|
||||||
GCE_METADATA_EMAIL_URL = "http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/email"
|
GCE_METADATA_EMAIL_URL = "http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/email"
|
||||||
GCE_METADATA_TOKEN_URL = "http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/token"
|
GCE_METADATA_TOKEN_URL = "http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/token"
|
||||||
|
|
@ -105,13 +113,60 @@ RUNTIME_TLS_TRUST_ENVIRONMENT = frozenset(
|
||||||
"SSL_CERT_DIR",
|
"SSL_CERT_DIR",
|
||||||
"SSL_CERT_FILE",
|
"SSL_CERT_FILE",
|
||||||
"SSLKEYLOGFILE",
|
"SSLKEYLOGFILE",
|
||||||
|
"OPENSSL_CONF",
|
||||||
|
"OPENSSL_ENGINES",
|
||||||
|
"OPENSSL_MODULES",
|
||||||
|
"GCONV_PATH",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
RUNTIME_PSQL_ENV_BASE = {
|
RUNTIME_PSQL_ENV_BASE = {
|
||||||
"PATH": "/usr/bin:/bin",
|
"PATH": "/usr/bin:/bin",
|
||||||
"PGSSLMODE": "require",
|
"PGSSLMODE": RUNTIME_SSL_MODE,
|
||||||
"PGCONNECT_TIMEOUT": "8",
|
"PGCONNECT_TIMEOUT": "8",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def runtime_ssl_root_cert() -> str:
|
||||||
|
"""Return the reviewed installed or ephemeral preflight CA path."""
|
||||||
|
|
||||||
|
raw = os.environ.get("TELEO_GCP_PREFLIGHT_SSL_ROOT_CERT", RUNTIME_SSL_ROOT_CERT)
|
||||||
|
candidate = Path(raw)
|
||||||
|
try:
|
||||||
|
if not candidate.is_absolute() or candidate.is_symlink():
|
||||||
|
raise OSError
|
||||||
|
resolved = candidate.resolve(strict=True)
|
||||||
|
if resolved != candidate or not resolved.is_file():
|
||||||
|
raise OSError
|
||||||
|
for part in (resolved, *resolved.parents):
|
||||||
|
metadata = part.stat()
|
||||||
|
if metadata.st_uid != 0 or metadata.st_mode & (stat.S_IWGRP | stat.S_IWOTH):
|
||||||
|
raise OSError
|
||||||
|
certificate = resolved.read_bytes()
|
||||||
|
except OSError:
|
||||||
|
raise SystemExit("Reviewed Cloud SQL server CA path is unavailable or untrusted.") from None
|
||||||
|
if hashlib.sha256(certificate).hexdigest() != RUNTIME_SSL_ROOT_CERT_SHA256:
|
||||||
|
raise SystemExit("Reviewed Cloud SQL server CA does not match the pinned certificate.")
|
||||||
|
return str(resolved)
|
||||||
|
|
||||||
|
|
||||||
|
def runtime_cloudsdk_config() -> str:
|
||||||
|
"""Return a root-owned empty SDK configuration directory."""
|
||||||
|
|
||||||
|
raw = os.environ.get("TELEO_GCP_PREFLIGHT_CLOUDSDK_CONFIG", RUNTIME_CLOUDSDK_CONFIG)
|
||||||
|
candidate = Path(raw)
|
||||||
|
try:
|
||||||
|
if not candidate.is_absolute() or candidate.is_symlink():
|
||||||
|
raise OSError
|
||||||
|
resolved = candidate.resolve(strict=True)
|
||||||
|
if resolved != candidate or not resolved.is_dir() or any(resolved.iterdir()):
|
||||||
|
raise OSError
|
||||||
|
for part in (resolved, *resolved.parents):
|
||||||
|
metadata = part.stat()
|
||||||
|
if metadata.st_uid != 0 or metadata.st_mode & (stat.S_IWGRP | stat.S_IWOTH):
|
||||||
|
raise OSError
|
||||||
|
except OSError:
|
||||||
|
raise SystemExit("Reviewed empty Cloud SDK configuration is unavailable or untrusted.") from None
|
||||||
|
return str(resolved)
|
||||||
CLONE_READ_ONLY_PGOPTIONS = "-c default_transaction_read_only=on"
|
CLONE_READ_ONLY_PGOPTIONS = "-c default_transaction_read_only=on"
|
||||||
RECEIPTED_READ_COMMANDS = frozenset(
|
RECEIPTED_READ_COMMANDS = frozenset(
|
||||||
{
|
{
|
||||||
|
|
@ -290,6 +345,8 @@ def validate_runtime_connection(args: argparse.Namespace) -> None:
|
||||||
password_secret = str(args.password_secret or "")
|
password_secret = str(args.password_secret or "")
|
||||||
mode = str(getattr(args, "credential_mode", "runtime"))
|
mode = str(getattr(args, "credential_mode", "runtime"))
|
||||||
if mode == "runtime":
|
if mode == "runtime":
|
||||||
|
ssl_root_cert = runtime_ssl_root_cert()
|
||||||
|
cloudsdk_config = runtime_cloudsdk_config()
|
||||||
if user != RUNTIME_DB_USER:
|
if user != RUNTIME_DB_USER:
|
||||||
raise SystemExit(f"Leo runtime requires database user {RUNTIME_DB_USER}.")
|
raise SystemExit(f"Leo runtime requires database user {RUNTIME_DB_USER}.")
|
||||||
if password_secret != RUNTIME_PASSWORD_SECRET:
|
if password_secret != RUNTIME_PASSWORD_SECRET:
|
||||||
|
|
@ -310,12 +367,31 @@ def validate_runtime_connection(args: argparse.Namespace) -> None:
|
||||||
if os.environ.get("TELEO_GCP_METADATA_ONLY") != "1":
|
if os.environ.get("TELEO_GCP_METADATA_ONLY") != "1":
|
||||||
raise SystemExit("Leo runtime requires TELEO_GCP_METADATA_ONLY=1.")
|
raise SystemExit("Leo runtime requires TELEO_GCP_METADATA_ONLY=1.")
|
||||||
forbidden_environment = any(
|
forbidden_environment = any(
|
||||||
normalized_key.startswith("PG")
|
(
|
||||||
or normalized_key.startswith("CLOUDSDK_")
|
normalized_key.startswith("PG")
|
||||||
|
and not (
|
||||||
|
(normalized_key == "PGSSLMODE" and value == RUNTIME_SSL_MODE)
|
||||||
|
or (normalized_key == "PGSSLROOTCERT" and value == ssl_root_cert)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
or (
|
||||||
|
normalized_key.startswith("CLOUDSDK_")
|
||||||
|
and not (
|
||||||
|
normalized_key == "CLOUDSDK_CONFIG"
|
||||||
|
and value == cloudsdk_config
|
||||||
|
)
|
||||||
|
)
|
||||||
or normalized_key == "GOOGLE_APPLICATION_CREDENTIALS"
|
or normalized_key == "GOOGLE_APPLICATION_CREDENTIALS"
|
||||||
or normalized_key.startswith("LD_")
|
or normalized_key.startswith("LD_")
|
||||||
or normalized_key.startswith("BASH_FUNC_")
|
or normalized_key.startswith("BASH_FUNC_")
|
||||||
or (normalized_key.startswith("PYTHON") and normalized_key != "PYTHONUNBUFFERED")
|
or (
|
||||||
|
normalized_key.startswith("PYTHON")
|
||||||
|
and not (
|
||||||
|
(normalized_key == "PYTHONNOUSERSITE" and value == "1")
|
||||||
|
or (normalized_key == "PYTHONSAFEPATH" and value == "1")
|
||||||
|
or normalized_key == "PYTHONUNBUFFERED"
|
||||||
|
)
|
||||||
|
)
|
||||||
or normalized_key in {
|
or normalized_key in {
|
||||||
"BASH_ENV",
|
"BASH_ENV",
|
||||||
"BASHOPTS",
|
"BASHOPTS",
|
||||||
|
|
@ -329,7 +405,8 @@ def validate_runtime_connection(args: argparse.Namespace) -> None:
|
||||||
or normalized_key == "PROXY"
|
or normalized_key == "PROXY"
|
||||||
or normalized_key.endswith("_PROXY")
|
or normalized_key.endswith("_PROXY")
|
||||||
or normalized_key in RUNTIME_TLS_TRUST_ENVIRONMENT
|
or normalized_key in RUNTIME_TLS_TRUST_ENVIRONMENT
|
||||||
for normalized_key in (key.upper() for key in os.environ)
|
for key, value in os.environ.items()
|
||||||
|
for normalized_key in (key.upper(),)
|
||||||
)
|
)
|
||||||
if forbidden_environment:
|
if forbidden_environment:
|
||||||
raise SystemExit(
|
raise SystemExit(
|
||||||
|
|
@ -515,6 +592,7 @@ def run_psql(args: argparse.Namespace, sql: str, db: str | None = None) -> str:
|
||||||
credential = password(args)
|
credential = password(args)
|
||||||
if args.credential_mode == "runtime":
|
if args.credential_mode == "runtime":
|
||||||
env = dict(RUNTIME_PSQL_ENV_BASE)
|
env = dict(RUNTIME_PSQL_ENV_BASE)
|
||||||
|
env["PGSSLROOTCERT"] = runtime_ssl_root_cert()
|
||||||
else:
|
else:
|
||||||
env = {key: value for key, value in os.environ.items() if not key.startswith("PG")}
|
env = {key: value for key, value in os.environ.items() if not key.startswith("PG")}
|
||||||
env.update(
|
env.update(
|
||||||
|
|
@ -529,7 +607,7 @@ def run_psql(args: argparse.Namespace, sql: str, db: str | None = None) -> str:
|
||||||
if args.credential_mode == "clone-readonly":
|
if args.credential_mode == "clone-readonly":
|
||||||
env["PGOPTIONS"] = CLONE_READ_ONLY_PGOPTIONS
|
env["PGOPTIONS"] = CLONE_READ_ONLY_PGOPTIONS
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
["psql", "-X", "-At", "-q", "-v", "ON_ERROR_STOP=1"],
|
[RUNTIME_PSQL_BIN, "-X", "-At", "-q", "-v", "ON_ERROR_STOP=1"],
|
||||||
text=True,
|
text=True,
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
input=sql,
|
input=sql,
|
||||||
|
|
@ -583,6 +661,8 @@ select jsonb_build_object(
|
||||||
raise SystemExit("Canonical database read marker returned an unexpected server port.")
|
raise SystemExit("Canonical database read marker returned an unexpected server port.")
|
||||||
if marker.get("ssl") is not True:
|
if marker.get("ssl") is not True:
|
||||||
raise SystemExit("Canonical database read marker did not prove an encrypted connection.")
|
raise SystemExit("Canonical database read marker did not prove an encrypted connection.")
|
||||||
|
if marker.get("system_identifier") != RUNTIME_SYSTEM_IDENTIFIER:
|
||||||
|
raise SystemExit("Canonical database read marker returned an unexpected system identifier.")
|
||||||
return marker
|
return marker
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
52
hermes-agent/leoclean-bin/teleo-kb-gcp
Executable file
52
hermes-agent/leoclean-bin/teleo-kb-gcp
Executable file
|
|
@ -0,0 +1,52 @@
|
||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# This wrapper is installed only into the GCP service's root-controlled bind.
|
||||||
|
# Ignore per-invocation mode and endpoint overrides so a service child cannot
|
||||||
|
# select the VPS/local backend or a different database credential context.
|
||||||
|
SCRIPT_PATH=${BASH_SOURCE[0]}
|
||||||
|
SCRIPT_DIR=${SCRIPT_PATH%/*}
|
||||||
|
if [ "$SCRIPT_DIR" = "$SCRIPT_PATH" ]; then
|
||||||
|
SCRIPT_DIR=.
|
||||||
|
fi
|
||||||
|
SCRIPT_DIR="$(cd "$SCRIPT_DIR" && pwd)"
|
||||||
|
CLOUDSQL_TOOL="$SCRIPT_DIR/cloudsql_memory_tool.py"
|
||||||
|
|
||||||
|
for argument in "$@"; do
|
||||||
|
case "$argument" in
|
||||||
|
--host|--host=*|--port|--port=*|--db|--db=*|--canonical-db|--canonical-db=*|\
|
||||||
|
--user|--user=*|--password-secret|--password-secret=*|--project|--project=*|\
|
||||||
|
--credential-mode|--credential-mode=*)
|
||||||
|
echo "teleo-kb: GCP runtime identity and endpoint overrides are forbidden" >&2
|
||||||
|
exit 64
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ ! -x "$CLOUDSQL_TOOL" ]; then
|
||||||
|
echo "teleo-kb: reviewed GCP Cloud SQL tool is unavailable" >&2
|
||||||
|
exit 69
|
||||||
|
fi
|
||||||
|
if [ ! -x /usr/bin/python3 ] || [ ! -x /usr/bin/psql ]; then
|
||||||
|
echo "teleo-kb: required GCP runtime dependency is unavailable" >&2
|
||||||
|
exit 69
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec /usr/bin/env -i \
|
||||||
|
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \
|
||||||
|
LANG=C LC_ALL=C \
|
||||||
|
PYTHONNOUSERSITE=1 PYTHONSAFEPATH=1 \
|
||||||
|
CLOUDSDK_CONFIG=/usr/local/libexec/livingip/leoclean-kb/gcloud-config \
|
||||||
|
PGSSLMODE=verify-ca \
|
||||||
|
PGSSLROOTCERT=/usr/local/libexec/livingip/leoclean-kb/cloudsql-server-ca.pem \
|
||||||
|
TELEO_KB_MODE=cloudsql \
|
||||||
|
TELEO_GCP_METADATA_ONLY=1 \
|
||||||
|
TELEO_GCP_PROJECT=teleo-501523 \
|
||||||
|
TELEO_CLOUDSQL_HOST=10.61.0.3 \
|
||||||
|
TELEO_CLOUDSQL_PORT=5432 \
|
||||||
|
TELEO_CLOUDSQL_DB=teleo_canonical \
|
||||||
|
TELEO_CANONICAL_CLOUDSQL_DB=teleo_canonical \
|
||||||
|
TELEO_CLOUDSQL_USER=leoclean_kb_runtime \
|
||||||
|
TELEO_CLOUDSQL_PASSWORD_SECRET=gcp-teleo-pgvector-standby-leoclean-kb-runtime-password \
|
||||||
|
TELEO_CLOUDSQL_ENABLE_AUDIT_FALLBACK=0 \
|
||||||
|
/usr/bin/python3 -I "$CLOUDSQL_TOOL" "$@"
|
||||||
21
ops/gcp-teleo-pgvector-standby-server-ca.pem
Normal file
21
ops/gcp-teleo-pgvector-standby-server-ca.pem
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDcTCCAlmgAwIBAgIBADANBgkqhkiG9w0BAQsFADBwMS0wKwYDVQQuEyQzNTcw
|
||||||
|
NmEzOC03MGRjLTQ3YTQtOWExOS04NGMxYTk0YzQ3ZjMxHDAaBgNVBAMTE0Nsb3Vk
|
||||||
|
IFNRTCBTZXJ2ZXIgQ0ExFDASBgNVBAoTC0dvb2dsZSwgSW5jMQswCQYDVQQGEwJV
|
||||||
|
UzAeFw0yNjA3MDcwOTM3NTJaFw0zNjA3MDQwOTM4NTJaMHAxLTArBgNVBC4TJDM1
|
||||||
|
NzA2YTM4LTcwZGMtNDdhNC05YTE5LTg0YzFhOTRjNDdmMzEcMBoGA1UEAxMTQ2xv
|
||||||
|
dWQgU1FMIFNlcnZlciBDQTEUMBIGA1UEChMLR29vZ2xlLCBJbmMxCzAJBgNVBAYT
|
||||||
|
AlVTMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0HwhrUzFIi9bqPbC
|
||||||
|
9FQjCalxEVYsu7tw+YzBtUaKOy+u/Y1e6TKJzFng/9wS8pXgY1Ul1/mn1S205yKR
|
||||||
|
5ydEESq7UL4pp7onSb2vGeu4NDTXDbcimZz++35dPbhFw7wtm1QSRMBHsONEDGaS
|
||||||
|
7/qIB3EN79SS9GxbSUZNsZOAn4DM9RSahtQrXa6NLlP7I9gPxvqEKRjFqUcBkWsE
|
||||||
|
Qzoz/yyxYQybZQo1UR9XCbGWFphi85cW15ITpfs7czoOVUk39bPNJKAVQFRqPUWK
|
||||||
|
xBC+1pHb7fe7xYk5j3jJbtoL6ixunQIFDCGRCy7/SEfIj5dWriDajVHwctilUmu1
|
||||||
|
Xy/A0QIDAQABoxYwFDASBgNVHRMBAf8ECDAGAQH/AgEAMA0GCSqGSIb3DQEBCwUA
|
||||||
|
A4IBAQBi8MoiT4kJAut4mLPibKldRB9s2Of7Jw8PmKNS50BIOZf8zrSrNMX5yC2i
|
||||||
|
/y9nmc8k9OCIYbzC6F/mTACmtHM1phoBtPYDjWYp+YS9cdo0HHGU2hwEP7PTfEOg
|
||||||
|
11ua2J1gjFbgVZVb7qNGV4n1QjCcAh1hWnJ2gLa55hiZBGZ0fuHTwBagWwRtom+u
|
||||||
|
YdD1d1NETInnCxnhzu7q+r0sjFUJABIj4qU/q/uVMHaAuy4Zy4PMctQofKSJ4ixf
|
||||||
|
vbU2/Sm7cbzgzcg2TJwpSqdDM/EdDMi5JtmMyZnRoKLZjH3CYI1091dyTBdk8RhK
|
||||||
|
MB4pqjJurXS8xKeA9dQjuvnIQvot
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
\set ON_ERROR_STOP on
|
\set ON_ERROR_STOP on
|
||||||
|
|
||||||
-- One-time GCP staging provisioning for Leo's Cloud SQL identity. The password
|
-- One-time GCP staging provisioning for Leo's Cloud SQL identity. Run this
|
||||||
-- is supplied only through the process environment and is never stored here.
|
-- only through ops/provision_gcp_leoclean_runtime_role.sh; psql's \password
|
||||||
|
-- command encrypts the supplied value client-side, so cleartext is never SQL.
|
||||||
--
|
--
|
||||||
-- The login can read the canonical allowlist and can stage a pending proposal
|
-- The login can read the canonical allowlist and can stage a pending proposal
|
||||||
-- through one narrowly-scoped SECURITY DEFINER function. It cannot inherit or
|
-- through one narrowly-scoped SECURITY DEFINER function. It cannot inherit or
|
||||||
|
|
@ -24,19 +25,34 @@ alter role leoclean_kb_stage_owner
|
||||||
with nologin nosuperuser nocreatedb nocreaterole noinherit noreplication nobypassrls connection limit -1
|
with nologin nosuperuser nocreatedb nocreaterole noinherit noreplication nobypassrls connection limit -1
|
||||||
password null;
|
password null;
|
||||||
|
|
||||||
-- Rotate the credential only after an idempotent rerun has disabled any
|
-- Snapshot every non-superuser principal connected to either scoped role before
|
||||||
-- pre-existing LOGIN role. This ordering has an intentional autocommit
|
-- removing the direct membership edges. Existing sessions can retain an
|
||||||
-- boundary: interruption can leave NOLOGIN, never a still-enabled old role.
|
-- already-selected SET ROLE identity after NOLOGIN/revocation, so all other
|
||||||
\getenv runtime_password TELEO_LEOCLEAN_DB_PASSWORD
|
-- connected sessions must be terminated before password or privilege work.
|
||||||
select format('alter role leoclean_kb_runtime password %L', :'runtime_password')
|
create temporary table leoclean_fenced_roles (
|
||||||
\gexec
|
role_oid oid primary key,
|
||||||
|
role_name name unique not null
|
||||||
|
) on commit preserve rows;
|
||||||
|
|
||||||
alter role leoclean_kb_runtime reset all;
|
with recursive connected(role_oid) as (
|
||||||
alter role leoclean_kb_stage_owner reset all;
|
values
|
||||||
|
('leoclean_kb_runtime'::pg_catalog.regrole::oid),
|
||||||
|
('leoclean_kb_stage_owner'::pg_catalog.regrole::oid)
|
||||||
|
union
|
||||||
|
select membership.member
|
||||||
|
from connected
|
||||||
|
join pg_catalog.pg_auth_members membership
|
||||||
|
on membership.roleid = connected.role_oid
|
||||||
|
)
|
||||||
|
insert into pg_temp.leoclean_fenced_roles (role_oid, role_name)
|
||||||
|
select role_row.oid, role_row.rolname
|
||||||
|
from connected
|
||||||
|
join pg_catalog.pg_roles role_row on role_row.oid = connected.role_oid
|
||||||
|
where not role_row.rolsuper;
|
||||||
|
|
||||||
-- NOINHERIT does not prevent SET ROLE. Remove every membership edge in either
|
-- NOINHERIT does not prevent SET ROLE. Remove every direct membership edge in
|
||||||
-- direction so neither scoped principal can reach, or be reached through, a
|
-- either direction before terminating the snapshotted sessions, closing the
|
||||||
-- broader role left behind by an earlier deployment.
|
-- race for newly connected membership principals.
|
||||||
select format('revoke %I from %I', granted_role.rolname, member_role.rolname)
|
select format('revoke %I from %I', granted_role.rolname, member_role.rolname)
|
||||||
from pg_catalog.pg_auth_members membership
|
from pg_catalog.pg_auth_members membership
|
||||||
join pg_catalog.pg_roles granted_role on granted_role.oid = membership.roleid
|
join pg_catalog.pg_roles granted_role on granted_role.oid = membership.roleid
|
||||||
|
|
@ -45,11 +61,62 @@ select format('revoke %I from %I', granted_role.rolname, member_role.rolname)
|
||||||
or member_role.rolname in ('leoclean_kb_runtime', 'leoclean_kb_stage_owner')
|
or member_role.rolname in ('leoclean_kb_runtime', 'leoclean_kb_stage_owner')
|
||||||
\gexec
|
\gexec
|
||||||
|
|
||||||
revoke all privileges on database teleo_canonical from leoclean_kb_runtime;
|
do $session_fence$
|
||||||
revoke all privileges on database teleo_canonical from leoclean_kb_stage_owner;
|
declare
|
||||||
revoke all privileges on database teleo_kb from leoclean_kb_runtime;
|
target record;
|
||||||
revoke all privileges on database teleo_kb from leoclean_kb_stage_owner;
|
remaining text;
|
||||||
grant connect on database teleo_canonical to leoclean_kb_runtime;
|
begin
|
||||||
|
for target in
|
||||||
|
select activity.pid
|
||||||
|
from pg_catalog.pg_stat_activity activity
|
||||||
|
join pg_temp.leoclean_fenced_roles fenced on fenced.role_name = activity.usename
|
||||||
|
where activity.pid <> pg_catalog.pg_backend_pid()
|
||||||
|
and activity.backend_type = 'client backend'
|
||||||
|
loop
|
||||||
|
if not pg_catalog.pg_terminate_backend(target.pid, 5000) then
|
||||||
|
raise exception 'could not terminate a pre-existing scoped-role session';
|
||||||
|
end if;
|
||||||
|
end loop;
|
||||||
|
|
||||||
|
select pg_catalog.string_agg(activity.pid::text, ',' order by activity.pid)
|
||||||
|
into remaining
|
||||||
|
from pg_catalog.pg_stat_activity activity
|
||||||
|
join pg_temp.leoclean_fenced_roles fenced on fenced.role_name = activity.usename
|
||||||
|
where activity.pid <> pg_catalog.pg_backend_pid()
|
||||||
|
and activity.backend_type = 'client backend';
|
||||||
|
if remaining is not null then
|
||||||
|
raise exception 'pre-existing scoped-role sessions remain after termination';
|
||||||
|
end if;
|
||||||
|
|
||||||
|
select pg_catalog.string_agg(prepared.gid, ',' order by prepared.gid)
|
||||||
|
into remaining
|
||||||
|
from pg_catalog.pg_prepared_xacts prepared
|
||||||
|
join pg_temp.leoclean_fenced_roles fenced on fenced.role_name = prepared.owner;
|
||||||
|
if remaining is not null then
|
||||||
|
raise exception 'scoped-role prepared transactions must be resolved before provisioning';
|
||||||
|
end if;
|
||||||
|
end
|
||||||
|
$session_fence$;
|
||||||
|
|
||||||
|
-- This is deliberately after the autocommitted NOLOGIN/session/membership
|
||||||
|
-- fence. Interruption can leave a disabled role, never a still-enabled old
|
||||||
|
-- login or an old connected session. The wrapper supplies the two prompts.
|
||||||
|
\password leoclean_kb_runtime
|
||||||
|
|
||||||
|
alter role leoclean_kb_runtime reset all;
|
||||||
|
alter role leoclean_kb_stage_owner reset all;
|
||||||
|
drop table pg_temp.leoclean_fenced_roles;
|
||||||
|
|
||||||
|
select pg_catalog.format(
|
||||||
|
'alter role %I in database %I reset all',
|
||||||
|
role_row.rolname,
|
||||||
|
database_row.datname
|
||||||
|
)
|
||||||
|
from pg_catalog.pg_db_role_setting setting_row
|
||||||
|
join pg_catalog.pg_roles role_row on role_row.oid = setting_row.setrole
|
||||||
|
join pg_catalog.pg_database database_row on database_row.oid = setting_row.setdatabase
|
||||||
|
where role_row.rolname in ('leoclean_kb_runtime', 'leoclean_kb_stage_owner')
|
||||||
|
\gexec
|
||||||
|
|
||||||
-- PostgreSQL normally grants TEMP to PUBLIC. A role-specific REVOKE cannot
|
-- PostgreSQL normally grants TEMP to PUBLIC. A role-specific REVOKE cannot
|
||||||
-- override that inherited grant, while REVOKE TEMP FROM PUBLIC would be a
|
-- override that inherited grant, while REVOKE TEMP FROM PUBLIC would be a
|
||||||
|
|
@ -77,6 +144,47 @@ alter role leoclean_kb_runtime in database teleo_canonical
|
||||||
|
|
||||||
begin;
|
begin;
|
||||||
|
|
||||||
|
-- CONNECT granted to PUBLIC cannot be denied to one role with a role-specific
|
||||||
|
-- REVOKE. Preserve every existing non-scoped principal's effective CONNECT
|
||||||
|
-- set as explicit ACLs, remove the cluster-wide PUBLIC grant from every
|
||||||
|
-- database (including templates), and then grant the runtime its sole target.
|
||||||
|
-- This runs in the same transaction as every remaining privilege/topology
|
||||||
|
-- assertion, so a later failure restores the original ACLs atomically.
|
||||||
|
create temporary table leoclean_preserved_database_connect (
|
||||||
|
role_name name not null,
|
||||||
|
database_name name not null,
|
||||||
|
primary key (role_name, database_name)
|
||||||
|
) on commit drop;
|
||||||
|
|
||||||
|
insert into pg_temp.leoclean_preserved_database_connect (role_name, database_name)
|
||||||
|
select role_row.rolname, database_row.datname
|
||||||
|
from pg_catalog.pg_roles role_row
|
||||||
|
cross join pg_catalog.pg_database database_row
|
||||||
|
where role_row.rolname not in ('leoclean_kb_runtime', 'leoclean_kb_stage_owner')
|
||||||
|
and pg_catalog.has_database_privilege(role_row.oid, database_row.oid, 'CONNECT');
|
||||||
|
|
||||||
|
select pg_catalog.format('revoke connect on database %I from public', database_row.datname)
|
||||||
|
from pg_catalog.pg_database database_row
|
||||||
|
\gexec
|
||||||
|
|
||||||
|
select pg_catalog.format(
|
||||||
|
'revoke all privileges on database %I from leoclean_kb_runtime, leoclean_kb_stage_owner',
|
||||||
|
database_row.datname
|
||||||
|
)
|
||||||
|
from pg_catalog.pg_database database_row
|
||||||
|
\gexec
|
||||||
|
|
||||||
|
select pg_catalog.format(
|
||||||
|
'grant connect on database %I to %I',
|
||||||
|
preserved.database_name,
|
||||||
|
preserved.role_name
|
||||||
|
)
|
||||||
|
from pg_temp.leoclean_preserved_database_connect preserved
|
||||||
|
order by preserved.database_name, preserved.role_name
|
||||||
|
\gexec
|
||||||
|
|
||||||
|
grant connect on database teleo_canonical to leoclean_kb_runtime;
|
||||||
|
|
||||||
-- The deploy principal needs temporary ownership authority to replace an
|
-- The deploy principal needs temporary ownership authority to replace an
|
||||||
-- existing function on idempotent reruns. This membership is transaction-local
|
-- existing function on idempotent reruns. This membership is transaction-local
|
||||||
-- and is revoked before verification/commit.
|
-- and is revoked before verification/commit.
|
||||||
|
|
@ -95,6 +203,114 @@ revoke all privileges on all functions in schema public, kb_stage
|
||||||
revoke all privileges on all procedures in schema public, kb_stage
|
revoke all privileges on all procedures in schema public, kb_stage
|
||||||
from leoclean_kb_runtime, leoclean_kb_stage_owner;
|
from leoclean_kb_runtime, leoclean_kb_stage_owner;
|
||||||
|
|
||||||
|
-- Built-in defaults also grant PUBLIC EXECUTE on newly created application
|
||||||
|
-- routines. Preserve every existing non-scoped role's effective access,
|
||||||
|
-- remove PUBLIC/scoped execution from the complete application routine set,
|
||||||
|
-- and restore the inventory explicitly. The reviewed staging function is
|
||||||
|
-- normalized again after CREATE OR REPLACE below.
|
||||||
|
create temporary table leoclean_preserved_app_routine_execute (
|
||||||
|
role_name name not null,
|
||||||
|
function_oid oid not null,
|
||||||
|
primary key (role_name, function_oid)
|
||||||
|
) on commit drop;
|
||||||
|
|
||||||
|
insert into pg_temp.leoclean_preserved_app_routine_execute (role_name, function_oid)
|
||||||
|
select role_row.rolname, function_row.oid
|
||||||
|
from pg_catalog.pg_roles role_row
|
||||||
|
cross join pg_catalog.pg_proc function_row
|
||||||
|
join pg_catalog.pg_namespace namespace on namespace.oid = function_row.pronamespace
|
||||||
|
where role_row.rolname not in ('leoclean_kb_runtime', 'leoclean_kb_stage_owner')
|
||||||
|
and namespace.nspname in ('public', 'kb_stage')
|
||||||
|
and pg_catalog.has_function_privilege(role_row.oid, function_row.oid, 'EXECUTE');
|
||||||
|
|
||||||
|
select pg_catalog.format(
|
||||||
|
'revoke execute on %s %I.%I(%s) from public, leoclean_kb_runtime, leoclean_kb_stage_owner',
|
||||||
|
case when function_row.prokind = 'p' then 'procedure' else 'function' end,
|
||||||
|
namespace.nspname,
|
||||||
|
function_row.proname,
|
||||||
|
pg_catalog.pg_get_function_identity_arguments(function_row.oid)
|
||||||
|
)
|
||||||
|
from pg_catalog.pg_proc function_row
|
||||||
|
join pg_catalog.pg_namespace namespace on namespace.oid = function_row.pronamespace
|
||||||
|
where namespace.nspname in ('public', 'kb_stage')
|
||||||
|
\gexec
|
||||||
|
|
||||||
|
select pg_catalog.format(
|
||||||
|
'grant execute on %s %I.%I(%s) to %I',
|
||||||
|
case when function_row.prokind = 'p' then 'procedure' else 'function' end,
|
||||||
|
namespace.nspname,
|
||||||
|
function_row.proname,
|
||||||
|
pg_catalog.pg_get_function_identity_arguments(function_row.oid),
|
||||||
|
preserved.role_name
|
||||||
|
)
|
||||||
|
from pg_temp.leoclean_preserved_app_routine_execute preserved
|
||||||
|
join pg_catalog.pg_proc function_row on function_row.oid = preserved.function_oid
|
||||||
|
join pg_catalog.pg_namespace namespace on namespace.oid = function_row.pronamespace
|
||||||
|
order by namespace.nspname, function_row.proname, preserved.role_name
|
||||||
|
\gexec
|
||||||
|
|
||||||
|
select pg_catalog.format(
|
||||||
|
'revoke all privileges on large object %s from leoclean_kb_runtime, leoclean_kb_stage_owner',
|
||||||
|
metadata.oid
|
||||||
|
)
|
||||||
|
from pg_catalog.pg_largeobject_metadata metadata
|
||||||
|
\gexec
|
||||||
|
|
||||||
|
-- Default PostgreSQL grants EXECUTE on built-in large-object mutators to
|
||||||
|
-- PUBLIC, which would let the runtime persist data despite all relation ACLs.
|
||||||
|
-- Snapshot and explicitly preserve every existing non-scoped role's effective
|
||||||
|
-- access, then remove PUBLIC and both scoped roles from every mutator overload.
|
||||||
|
create temporary table leoclean_preserved_lo_execute (
|
||||||
|
role_name name not null,
|
||||||
|
function_oid oid not null,
|
||||||
|
primary key (role_name, function_oid)
|
||||||
|
) on commit drop;
|
||||||
|
|
||||||
|
insert into pg_temp.leoclean_preserved_lo_execute (role_name, function_oid)
|
||||||
|
select role_row.rolname, function_row.oid
|
||||||
|
from pg_catalog.pg_roles role_row
|
||||||
|
cross join pg_catalog.pg_proc function_row
|
||||||
|
join pg_catalog.pg_namespace namespace on namespace.oid = function_row.pronamespace
|
||||||
|
where role_row.rolname not in ('leoclean_kb_runtime', 'leoclean_kb_stage_owner')
|
||||||
|
and namespace.nspname = 'pg_catalog'
|
||||||
|
and function_row.proname in (
|
||||||
|
'lo_creat', 'lo_create', 'lo_export', 'lo_from_bytea', 'lo_import',
|
||||||
|
'lo_open', 'lo_put', 'lo_truncate', 'lo_truncate64', 'lo_unlink', 'lowrite'
|
||||||
|
)
|
||||||
|
and pg_catalog.has_function_privilege(role_row.oid, function_row.oid, 'EXECUTE');
|
||||||
|
|
||||||
|
select pg_catalog.format(
|
||||||
|
'revoke execute on function pg_catalog.%I(%s) from public, leoclean_kb_runtime, leoclean_kb_stage_owner',
|
||||||
|
function_row.proname,
|
||||||
|
pg_catalog.pg_get_function_identity_arguments(function_row.oid)
|
||||||
|
)
|
||||||
|
from pg_catalog.pg_proc function_row
|
||||||
|
join pg_catalog.pg_namespace namespace on namespace.oid = function_row.pronamespace
|
||||||
|
where namespace.nspname = 'pg_catalog'
|
||||||
|
and function_row.proname in (
|
||||||
|
'lo_creat', 'lo_create', 'lo_export', 'lo_from_bytea', 'lo_import',
|
||||||
|
'lo_open', 'lo_put', 'lo_truncate', 'lo_truncate64', 'lo_unlink', 'lowrite'
|
||||||
|
)
|
||||||
|
\gexec
|
||||||
|
|
||||||
|
select pg_catalog.format(
|
||||||
|
'grant execute on function pg_catalog.%I(%s) to %I',
|
||||||
|
function_row.proname,
|
||||||
|
pg_catalog.pg_get_function_identity_arguments(function_row.oid),
|
||||||
|
preserved.role_name
|
||||||
|
)
|
||||||
|
from pg_temp.leoclean_preserved_lo_execute preserved
|
||||||
|
join pg_catalog.pg_proc function_row on function_row.oid = preserved.function_oid
|
||||||
|
order by function_row.proname, preserved.role_name
|
||||||
|
\gexec
|
||||||
|
|
||||||
|
select pg_catalog.format(
|
||||||
|
'revoke all privileges on parameter %I from leoclean_kb_runtime, leoclean_kb_stage_owner',
|
||||||
|
parameter_acl.parname
|
||||||
|
)
|
||||||
|
from pg_catalog.pg_parameter_acl parameter_acl
|
||||||
|
\gexec
|
||||||
|
|
||||||
-- Table-level REVOKE does not clear column ACLs. Clear every column ACL for both
|
-- Table-level REVOKE does not clear column ACLs. Clear every column ACL for both
|
||||||
-- principals before rebuilding the exact allowlist below.
|
-- principals before rebuilding the exact allowlist below.
|
||||||
select format(
|
select format(
|
||||||
|
|
@ -310,6 +526,264 @@ begin
|
||||||
raise exception 'scoped Leo roles retain role memberships: %', unexpected;
|
raise exception 'scoped Leo roles retain role memberships: %', unexpected;
|
||||||
end if;
|
end if;
|
||||||
|
|
||||||
|
if not exists (
|
||||||
|
select 1
|
||||||
|
from pg_catalog.pg_class relation
|
||||||
|
join pg_catalog.pg_namespace namespace on namespace.oid = relation.relnamespace
|
||||||
|
where namespace.nspname = 'kb_stage'
|
||||||
|
and relation.relname = 'kb_proposals'
|
||||||
|
and relation.relkind = 'r'
|
||||||
|
and relation.relpersistence = 'p'
|
||||||
|
and not relation.relrowsecurity
|
||||||
|
and not relation.relforcerowsecurity
|
||||||
|
) then
|
||||||
|
raise exception 'kb_stage.kb_proposals is not the reviewed permanent base table';
|
||||||
|
end if;
|
||||||
|
|
||||||
|
select pg_catalog.string_agg(
|
||||||
|
pg_catalog.format('%I.%I:%s', namespace.nspname, relation.relname, relation.relkind),
|
||||||
|
', '
|
||||||
|
order by namespace.nspname, relation.relname
|
||||||
|
)
|
||||||
|
into unexpected
|
||||||
|
from pg_catalog.pg_class relation
|
||||||
|
join pg_catalog.pg_namespace namespace on namespace.oid = relation.relnamespace
|
||||||
|
where (namespace.nspname, relation.relname) in (
|
||||||
|
('public', 'agents'), ('public', 'claims'), ('public', 'claim_evidence'),
|
||||||
|
('public', 'claim_edges'), ('public', 'sources'), ('public', 'personas'),
|
||||||
|
('public', 'strategies'), ('public', 'beliefs'), ('public', 'blindspots'),
|
||||||
|
('public', 'agent_roles'), ('public', 'peer_models'), ('public', 'behavioral_rules'),
|
||||||
|
('public', 'contributor_rules'), ('public', 'reasoning_tools'),
|
||||||
|
('public', 'governance_gates'), ('kb_stage', 'kb_proposals')
|
||||||
|
)
|
||||||
|
and relation.relkind <> 'r';
|
||||||
|
if unexpected is not null then
|
||||||
|
raise exception 'Leo read allowlist contains a non-base relation: %', unexpected;
|
||||||
|
end if;
|
||||||
|
|
||||||
|
select pg_catalog.string_agg(
|
||||||
|
pg_catalog.format('%s->%s', inheritance.inhparent::pg_catalog.regclass, inheritance.inhrelid::pg_catalog.regclass),
|
||||||
|
', '
|
||||||
|
)
|
||||||
|
into unexpected
|
||||||
|
from pg_catalog.pg_inherits inheritance
|
||||||
|
where inheritance.inhparent in (
|
||||||
|
'public.agents'::pg_catalog.regclass, 'public.claims'::pg_catalog.regclass,
|
||||||
|
'public.claim_evidence'::pg_catalog.regclass, 'public.claim_edges'::pg_catalog.regclass,
|
||||||
|
'public.sources'::pg_catalog.regclass, 'public.personas'::pg_catalog.regclass,
|
||||||
|
'public.strategies'::pg_catalog.regclass, 'public.beliefs'::pg_catalog.regclass,
|
||||||
|
'public.blindspots'::pg_catalog.regclass, 'public.agent_roles'::pg_catalog.regclass,
|
||||||
|
'public.peer_models'::pg_catalog.regclass, 'public.behavioral_rules'::pg_catalog.regclass,
|
||||||
|
'public.contributor_rules'::pg_catalog.regclass, 'public.reasoning_tools'::pg_catalog.regclass,
|
||||||
|
'public.governance_gates'::pg_catalog.regclass, 'kb_stage.kb_proposals'::pg_catalog.regclass
|
||||||
|
)
|
||||||
|
or inheritance.inhrelid in (
|
||||||
|
'public.agents'::pg_catalog.regclass, 'public.claims'::pg_catalog.regclass,
|
||||||
|
'public.claim_evidence'::pg_catalog.regclass, 'public.claim_edges'::pg_catalog.regclass,
|
||||||
|
'public.sources'::pg_catalog.regclass, 'public.personas'::pg_catalog.regclass,
|
||||||
|
'public.strategies'::pg_catalog.regclass, 'public.beliefs'::pg_catalog.regclass,
|
||||||
|
'public.blindspots'::pg_catalog.regclass, 'public.agent_roles'::pg_catalog.regclass,
|
||||||
|
'public.peer_models'::pg_catalog.regclass, 'public.behavioral_rules'::pg_catalog.regclass,
|
||||||
|
'public.contributor_rules'::pg_catalog.regclass, 'public.reasoning_tools'::pg_catalog.regclass,
|
||||||
|
'public.governance_gates'::pg_catalog.regclass, 'kb_stage.kb_proposals'::pg_catalog.regclass
|
||||||
|
);
|
||||||
|
if unexpected is not null then
|
||||||
|
raise exception 'Leo read allowlist has inheritance edges: %', unexpected;
|
||||||
|
end if;
|
||||||
|
|
||||||
|
with expected(ordinal, attname, typname, not_null, has_default, collated) as (
|
||||||
|
values
|
||||||
|
(1, 'id', 'uuid', true, true, false),
|
||||||
|
(2, 'proposal_type', 'text', true, false, true),
|
||||||
|
(3, 'status', 'text', true, true, true),
|
||||||
|
(4, 'proposed_by_handle', 'text', false, false, true),
|
||||||
|
(5, 'proposed_by_agent_id', 'uuid', false, false, false),
|
||||||
|
(6, 'channel', 'text', true, true, true),
|
||||||
|
(7, 'source_ref', 'text', false, false, true),
|
||||||
|
(8, 'rationale', 'text', true, false, true),
|
||||||
|
(9, 'payload', 'jsonb', true, false, false),
|
||||||
|
(10, 'reviewed_by_handle', 'text', false, false, true),
|
||||||
|
(11, 'reviewed_by_agent_id', 'uuid', false, false, false),
|
||||||
|
(12, 'reviewed_at', 'timestamptz', false, false, false),
|
||||||
|
(13, 'review_note', 'text', false, false, true),
|
||||||
|
(14, 'applied_by_handle', 'text', false, false, true),
|
||||||
|
(15, 'applied_by_agent_id', 'uuid', false, false, false),
|
||||||
|
(16, 'applied_at', 'timestamptz', false, false, false),
|
||||||
|
(17, 'created_at', 'timestamptz', true, true, false),
|
||||||
|
(18, 'updated_at', 'timestamptz', true, true, false)
|
||||||
|
), actual as (
|
||||||
|
select attribute.attnum::int as ordinal,
|
||||||
|
attribute.attname,
|
||||||
|
type_row.typname,
|
||||||
|
attribute.attnotnull as not_null,
|
||||||
|
attribute.atthasdef as has_default,
|
||||||
|
attribute.attidentity,
|
||||||
|
attribute.attgenerated,
|
||||||
|
attribute.atttypmod,
|
||||||
|
type_namespace.nspname as type_namespace,
|
||||||
|
attribute.attcollation = 'pg_catalog.default'::pg_catalog.regcollation as collated
|
||||||
|
from pg_catalog.pg_attribute attribute
|
||||||
|
join pg_catalog.pg_type type_row on type_row.oid = attribute.atttypid
|
||||||
|
join pg_catalog.pg_namespace type_namespace on type_namespace.oid = type_row.typnamespace
|
||||||
|
where attribute.attrelid = 'kb_stage.kb_proposals'::pg_catalog.regclass
|
||||||
|
and attribute.attnum > 0
|
||||||
|
and not attribute.attisdropped
|
||||||
|
)
|
||||||
|
select pg_catalog.string_agg(coalesce(expected.attname, actual.attname), ', ' order by coalesce(expected.ordinal, actual.ordinal))
|
||||||
|
into unexpected
|
||||||
|
from expected
|
||||||
|
full join actual using (ordinal, attname)
|
||||||
|
where expected.ordinal is null
|
||||||
|
or actual.ordinal is null
|
||||||
|
or expected.typname is distinct from actual.typname
|
||||||
|
or expected.not_null is distinct from actual.not_null
|
||||||
|
or expected.has_default is distinct from actual.has_default
|
||||||
|
or expected.collated is distinct from actual.collated
|
||||||
|
or actual.attidentity <> ''
|
||||||
|
or actual.attgenerated <> ''
|
||||||
|
or actual.atttypmod <> -1
|
||||||
|
or actual.type_namespace <> 'pg_catalog';
|
||||||
|
if unexpected is not null then
|
||||||
|
raise exception 'kb_stage.kb_proposals column contract drifted: %', unexpected;
|
||||||
|
end if;
|
||||||
|
|
||||||
|
if exists (
|
||||||
|
select 1
|
||||||
|
from pg_catalog.pg_trigger trigger_row
|
||||||
|
where trigger_row.tgrelid = 'kb_stage.kb_proposals'::pg_catalog.regclass
|
||||||
|
and not trigger_row.tgisinternal
|
||||||
|
) or exists (
|
||||||
|
select 1
|
||||||
|
from pg_catalog.pg_rewrite rewrite_row
|
||||||
|
where rewrite_row.ev_class = 'kb_stage.kb_proposals'::pg_catalog.regclass
|
||||||
|
) or exists (
|
||||||
|
select 1
|
||||||
|
from pg_catalog.pg_policy policy_row
|
||||||
|
where policy_row.polrelid = 'kb_stage.kb_proposals'::pg_catalog.regclass
|
||||||
|
) or exists (
|
||||||
|
select 1
|
||||||
|
from pg_catalog.pg_attribute attribute
|
||||||
|
where attribute.attrelid = 'kb_stage.kb_proposals'::pg_catalog.regclass
|
||||||
|
and attribute.attnum > 0
|
||||||
|
and not attribute.attisdropped
|
||||||
|
and attribute.attgenerated <> ''
|
||||||
|
) then
|
||||||
|
raise exception 'kb_stage.kb_proposals has unsafe trigger/rule/RLS/generated topology';
|
||||||
|
end if;
|
||||||
|
|
||||||
|
with expected(attname, expression) as (
|
||||||
|
values
|
||||||
|
('id', 'gen_random_uuid()'),
|
||||||
|
('status', '''pending_review''::text'),
|
||||||
|
('channel', '''cli''::text'),
|
||||||
|
('created_at', 'now()'),
|
||||||
|
('updated_at', 'now()')
|
||||||
|
), actual as (
|
||||||
|
select attribute.attname,
|
||||||
|
pg_catalog.pg_get_expr(default_row.adbin, default_row.adrelid) as expression
|
||||||
|
from pg_catalog.pg_attribute attribute
|
||||||
|
join pg_catalog.pg_attrdef default_row
|
||||||
|
on default_row.adrelid = attribute.attrelid
|
||||||
|
and default_row.adnum = attribute.attnum
|
||||||
|
where attribute.attrelid = 'kb_stage.kb_proposals'::pg_catalog.regclass
|
||||||
|
and attribute.attnum > 0
|
||||||
|
and not attribute.attisdropped
|
||||||
|
)
|
||||||
|
select pg_catalog.string_agg(coalesce(expected.attname, actual.attname), ', ')
|
||||||
|
into unexpected
|
||||||
|
from expected
|
||||||
|
full join actual using (attname)
|
||||||
|
where expected.expression is distinct from actual.expression;
|
||||||
|
if unexpected is not null then
|
||||||
|
raise exception 'kb_stage.kb_proposals default topology drifted: %', unexpected;
|
||||||
|
end if;
|
||||||
|
|
||||||
|
select pg_catalog.string_agg(
|
||||||
|
pg_catalog.format('%s:%s', dependency.refclassid::pg_catalog.regclass, dependency.refobjid),
|
||||||
|
', '
|
||||||
|
)
|
||||||
|
into unexpected
|
||||||
|
from pg_catalog.pg_attrdef default_row
|
||||||
|
join pg_catalog.pg_depend dependency
|
||||||
|
on dependency.classid = 'pg_catalog.pg_attrdef'::pg_catalog.regclass
|
||||||
|
and dependency.objid = default_row.oid
|
||||||
|
where default_row.adrelid = 'kb_stage.kb_proposals'::pg_catalog.regclass
|
||||||
|
and (
|
||||||
|
dependency.refclassid = 'pg_catalog.pg_proc'::pg_catalog.regclass
|
||||||
|
and dependency.refobjid not in (
|
||||||
|
'pg_catalog.gen_random_uuid()'::pg_catalog.regprocedure,
|
||||||
|
'pg_catalog.now()'::pg_catalog.regprocedure
|
||||||
|
)
|
||||||
|
or dependency.refclassid = 'pg_catalog.pg_type'::pg_catalog.regclass
|
||||||
|
and not exists (
|
||||||
|
select 1
|
||||||
|
from pg_catalog.pg_type type_row
|
||||||
|
join pg_catalog.pg_namespace namespace on namespace.oid = type_row.typnamespace
|
||||||
|
where type_row.oid = dependency.refobjid
|
||||||
|
and namespace.nspname = 'pg_catalog'
|
||||||
|
)
|
||||||
|
or dependency.refclassid = 'pg_catalog.pg_operator'::pg_catalog.regclass
|
||||||
|
and not exists (
|
||||||
|
select 1
|
||||||
|
from pg_catalog.pg_operator operator_row
|
||||||
|
join pg_catalog.pg_namespace namespace on namespace.oid = operator_row.oprnamespace
|
||||||
|
where operator_row.oid = dependency.refobjid
|
||||||
|
and namespace.nspname = 'pg_catalog'
|
||||||
|
)
|
||||||
|
or dependency.refclassid = 'pg_catalog.pg_collation'::pg_catalog.regclass
|
||||||
|
and not exists (
|
||||||
|
select 1
|
||||||
|
from pg_catalog.pg_collation collation_row
|
||||||
|
join pg_catalog.pg_namespace namespace on namespace.oid = collation_row.collnamespace
|
||||||
|
where collation_row.oid = dependency.refobjid
|
||||||
|
and namespace.nspname = 'pg_catalog'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
if unexpected is not null then
|
||||||
|
raise exception 'kb_stage.kb_proposals defaults depend on unreviewed objects: %', unexpected;
|
||||||
|
end if;
|
||||||
|
|
||||||
|
if (
|
||||||
|
select pg_catalog.count(*) <> 6
|
||||||
|
or pg_catalog.count(*) filter (
|
||||||
|
where (constraint_row.conname, constraint_row.contype) in (
|
||||||
|
('kb_proposals_pkey', 'p'),
|
||||||
|
('kb_proposals_proposal_type_check', 'c'),
|
||||||
|
('kb_proposals_status_check', 'c'),
|
||||||
|
('kb_proposals_applied_by_agent_id_fkey', 'f'),
|
||||||
|
('kb_proposals_proposed_by_agent_id_fkey', 'f'),
|
||||||
|
('kb_proposals_reviewed_by_agent_id_fkey', 'f')
|
||||||
|
)
|
||||||
|
) <> 6
|
||||||
|
from pg_catalog.pg_constraint constraint_row
|
||||||
|
where constraint_row.conrelid = 'kb_stage.kb_proposals'::pg_catalog.regclass
|
||||||
|
) or exists (
|
||||||
|
select 1
|
||||||
|
from pg_catalog.pg_constraint constraint_row
|
||||||
|
join pg_catalog.pg_depend dependency
|
||||||
|
on dependency.classid = 'pg_catalog.pg_constraint'::pg_catalog.regclass
|
||||||
|
and dependency.objid = constraint_row.oid
|
||||||
|
and dependency.refclassid = 'pg_catalog.pg_proc'::pg_catalog.regclass
|
||||||
|
join pg_catalog.pg_proc function_row on function_row.oid = dependency.refobjid
|
||||||
|
join pg_catalog.pg_namespace namespace on namespace.oid = function_row.pronamespace
|
||||||
|
where constraint_row.conrelid = 'kb_stage.kb_proposals'::pg_catalog.regclass
|
||||||
|
and namespace.nspname <> 'pg_catalog'
|
||||||
|
) or exists (
|
||||||
|
select 1
|
||||||
|
from pg_catalog.pg_index index_row
|
||||||
|
where index_row.indrelid = 'kb_stage.kb_proposals'::pg_catalog.regclass
|
||||||
|
and (
|
||||||
|
index_row.indexprs is not null
|
||||||
|
or (
|
||||||
|
index_row.indpred is not null
|
||||||
|
and pg_catalog.pg_get_expr(index_row.indpred, index_row.indrelid)
|
||||||
|
<> '(proposed_by_handle IS NOT NULL)'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) then
|
||||||
|
raise exception 'kb_stage.kb_proposals constraint/index topology drifted';
|
||||||
|
end if;
|
||||||
|
|
||||||
select pg_catalog.string_agg(
|
select pg_catalog.string_agg(
|
||||||
pg_catalog.format(
|
pg_catalog.format(
|
||||||
'%I:%s:%s:grantable=%s',
|
'%I:%s:%s:grantable=%s',
|
||||||
|
|
@ -330,8 +804,7 @@ begin
|
||||||
)
|
)
|
||||||
) as acl
|
) as acl
|
||||||
join pg_catalog.pg_roles grantee on grantee.oid = acl.grantee
|
join pg_catalog.pg_roles grantee on grantee.oid = acl.grantee
|
||||||
where database_row.datname in ('teleo_canonical', 'teleo_kb')
|
where grantee.rolname in ('leoclean_kb_runtime', 'leoclean_kb_stage_owner')
|
||||||
and grantee.rolname in ('leoclean_kb_runtime', 'leoclean_kb_stage_owner')
|
|
||||||
and not (
|
and not (
|
||||||
database_row.datname = 'teleo_canonical'
|
database_row.datname = 'teleo_canonical'
|
||||||
and grantee.rolname = 'leoclean_kb_runtime'
|
and grantee.rolname = 'leoclean_kb_runtime'
|
||||||
|
|
@ -342,11 +815,86 @@ begin
|
||||||
raise exception 'scoped Leo roles retain unexpected direct database ACLs: %', unexpected;
|
raise exception 'scoped Leo roles retain unexpected direct database ACLs: %', unexpected;
|
||||||
end if;
|
end if;
|
||||||
|
|
||||||
if has_database_privilege('leoclean_kb_runtime', 'teleo_canonical', 'CREATE')
|
select pg_catalog.string_agg(database_row.datname, ', ' order by database_row.datname)
|
||||||
or has_database_privilege('leoclean_kb_runtime', 'teleo_kb', 'CREATE')
|
into unexpected
|
||||||
or has_database_privilege('leoclean_kb_stage_owner', 'teleo_canonical', 'CREATE')
|
from pg_catalog.pg_database database_row
|
||||||
or has_database_privilege('leoclean_kb_stage_owner', 'teleo_kb', 'CREATE') then
|
where database_row.datname <> 'teleo_canonical'
|
||||||
raise exception 'a scoped Leo role unexpectedly has database CREATE';
|
and has_database_privilege('leoclean_kb_runtime', database_row.oid, 'CONNECT');
|
||||||
|
if unexpected is not null then
|
||||||
|
raise exception 'runtime CONNECT reaches a noncanonical database: %', unexpected;
|
||||||
|
end if;
|
||||||
|
|
||||||
|
select pg_catalog.string_agg(database_row.datname, ', ' order by database_row.datname)
|
||||||
|
into unexpected
|
||||||
|
from pg_catalog.pg_database database_row
|
||||||
|
where has_database_privilege('leoclean_kb_stage_owner', database_row.oid, 'CONNECT');
|
||||||
|
if unexpected is not null then
|
||||||
|
raise exception 'stage owner CONNECT reaches a database: %', unexpected;
|
||||||
|
end if;
|
||||||
|
|
||||||
|
select pg_catalog.string_agg(database_row.datname, ', ' order by database_row.datname)
|
||||||
|
into unexpected
|
||||||
|
from pg_catalog.pg_database database_row
|
||||||
|
cross join lateral pg_catalog.aclexplode(
|
||||||
|
coalesce(database_row.datacl, pg_catalog.acldefault('d', database_row.datdba))
|
||||||
|
) acl
|
||||||
|
where acl.grantee = 0
|
||||||
|
and acl.privilege_type = 'CONNECT';
|
||||||
|
if unexpected is not null then
|
||||||
|
raise exception 'PUBLIC retains database CONNECT: %', unexpected;
|
||||||
|
end if;
|
||||||
|
|
||||||
|
select pg_catalog.string_agg(
|
||||||
|
pg_catalog.format('%s:%s', setting_row.setdatabase, setting_row.setconfig),
|
||||||
|
', '
|
||||||
|
)
|
||||||
|
into unexpected
|
||||||
|
from pg_catalog.pg_db_role_setting setting_row
|
||||||
|
where setting_row.setrole in (runtime_oid, owner_oid)
|
||||||
|
and not (
|
||||||
|
setting_row.setrole = runtime_oid
|
||||||
|
and setting_row.setdatabase = (
|
||||||
|
select database_row.oid
|
||||||
|
from pg_catalog.pg_database database_row
|
||||||
|
where database_row.datname = 'teleo_canonical'
|
||||||
|
)
|
||||||
|
and pg_catalog.cardinality(setting_row.setconfig) = 3
|
||||||
|
and setting_row.setconfig @> array[
|
||||||
|
'search_path=pg_catalog, public, kb_stage',
|
||||||
|
'statement_timeout=15s',
|
||||||
|
'lock_timeout=2s'
|
||||||
|
]::text[]
|
||||||
|
);
|
||||||
|
if unexpected is not null or not exists (
|
||||||
|
select 1
|
||||||
|
from pg_catalog.pg_db_role_setting setting_row
|
||||||
|
where setting_row.setrole = runtime_oid
|
||||||
|
and setting_row.setdatabase = (
|
||||||
|
select database_row.oid
|
||||||
|
from pg_catalog.pg_database database_row
|
||||||
|
where database_row.datname = 'teleo_canonical'
|
||||||
|
)
|
||||||
|
and pg_catalog.cardinality(setting_row.setconfig) = 3
|
||||||
|
and setting_row.setconfig @> array[
|
||||||
|
'search_path=pg_catalog, public, kb_stage',
|
||||||
|
'statement_timeout=15s',
|
||||||
|
'lock_timeout=2s'
|
||||||
|
]::text[]
|
||||||
|
) then
|
||||||
|
raise exception 'scoped Leo role settings differ from the reviewed contract';
|
||||||
|
end if;
|
||||||
|
|
||||||
|
select pg_catalog.string_agg(
|
||||||
|
pg_catalog.format('%I:%s', database_row.datname, scoped_role.name),
|
||||||
|
', '
|
||||||
|
order by database_row.datname, scoped_role.name
|
||||||
|
)
|
||||||
|
into unexpected
|
||||||
|
from pg_catalog.pg_database database_row
|
||||||
|
cross join (values ('leoclean_kb_runtime'), ('leoclean_kb_stage_owner')) scoped_role(name)
|
||||||
|
where has_database_privilege(scoped_role.name, database_row.oid, 'CREATE');
|
||||||
|
if unexpected is not null then
|
||||||
|
raise exception 'a scoped Leo role unexpectedly has database CREATE: %', unexpected;
|
||||||
end if;
|
end if;
|
||||||
|
|
||||||
select pg_catalog.string_agg(
|
select pg_catalog.string_agg(
|
||||||
|
|
@ -382,6 +930,75 @@ begin
|
||||||
raise exception 'leoclean_kb_stage_owner owns unexpected database objects: %', unexpected;
|
raise exception 'leoclean_kb_stage_owner owns unexpected database objects: %', unexpected;
|
||||||
end if;
|
end if;
|
||||||
|
|
||||||
|
select pg_catalog.string_agg(metadata.oid::text, ', ' order by metadata.oid)
|
||||||
|
into unexpected
|
||||||
|
from pg_catalog.pg_largeobject_metadata metadata
|
||||||
|
where metadata.lomowner in (runtime_oid, owner_oid)
|
||||||
|
or exists (
|
||||||
|
select 1
|
||||||
|
from pg_catalog.aclexplode(
|
||||||
|
coalesce(metadata.lomacl, pg_catalog.acldefault('L', metadata.lomowner))
|
||||||
|
) acl
|
||||||
|
where acl.grantee in (0, runtime_oid, owner_oid)
|
||||||
|
and acl.privilege_type in ('SELECT', 'UPDATE')
|
||||||
|
);
|
||||||
|
if unexpected is not null then
|
||||||
|
raise exception 'scoped Leo roles retain large-object ownership or ACL reachability: %', unexpected;
|
||||||
|
end if;
|
||||||
|
|
||||||
|
select pg_catalog.string_agg(
|
||||||
|
pg_catalog.format('%I(%s)', function_row.proname, pg_catalog.pg_get_function_identity_arguments(function_row.oid)),
|
||||||
|
', '
|
||||||
|
)
|
||||||
|
into unexpected
|
||||||
|
from pg_catalog.pg_proc function_row
|
||||||
|
join pg_catalog.pg_namespace namespace on namespace.oid = function_row.pronamespace
|
||||||
|
where namespace.nspname = 'pg_catalog'
|
||||||
|
and function_row.proname in (
|
||||||
|
'lo_creat', 'lo_create', 'lo_export', 'lo_from_bytea', 'lo_import',
|
||||||
|
'lo_open', 'lo_put', 'lo_truncate', 'lo_truncate64', 'lo_unlink', 'lowrite'
|
||||||
|
)
|
||||||
|
and (
|
||||||
|
has_function_privilege('leoclean_kb_runtime', function_row.oid, 'EXECUTE')
|
||||||
|
or has_function_privilege('leoclean_kb_stage_owner', function_row.oid, 'EXECUTE')
|
||||||
|
);
|
||||||
|
if unexpected is not null then
|
||||||
|
raise exception 'PUBLIC or inherited large-object mutators remain executable: %', unexpected;
|
||||||
|
end if;
|
||||||
|
|
||||||
|
select pg_catalog.string_agg(
|
||||||
|
pg_catalog.format('%I(%s)', function_row.proname, pg_catalog.pg_get_function_identity_arguments(function_row.oid)),
|
||||||
|
', '
|
||||||
|
order by function_row.proname, pg_catalog.pg_get_function_identity_arguments(function_row.oid)
|
||||||
|
)
|
||||||
|
into unexpected
|
||||||
|
from pg_catalog.pg_proc function_row
|
||||||
|
join pg_catalog.pg_namespace namespace on namespace.oid = function_row.pronamespace
|
||||||
|
cross join lateral pg_catalog.aclexplode(
|
||||||
|
coalesce(function_row.proacl, pg_catalog.acldefault('f', function_row.proowner))
|
||||||
|
) acl
|
||||||
|
where namespace.nspname = 'pg_catalog'
|
||||||
|
and function_row.proname in (
|
||||||
|
'lo_creat', 'lo_create', 'lo_export', 'lo_from_bytea', 'lo_import',
|
||||||
|
'lo_open', 'lo_put', 'lo_truncate', 'lo_truncate64', 'lo_unlink', 'lowrite'
|
||||||
|
)
|
||||||
|
and acl.grantee = 0
|
||||||
|
and acl.privilege_type = 'EXECUTE';
|
||||||
|
if unexpected is not null then
|
||||||
|
raise exception 'PUBLIC retains large-object mutator EXECUTE: %', unexpected;
|
||||||
|
end if;
|
||||||
|
|
||||||
|
select pg_catalog.string_agg(parameter_acl.parname, ', ' order by parameter_acl.parname)
|
||||||
|
into unexpected
|
||||||
|
from pg_catalog.pg_parameter_acl parameter_acl
|
||||||
|
where has_parameter_privilege('leoclean_kb_runtime', parameter_acl.parname, 'SET')
|
||||||
|
or has_parameter_privilege('leoclean_kb_runtime', parameter_acl.parname, 'ALTER SYSTEM')
|
||||||
|
or has_parameter_privilege('leoclean_kb_stage_owner', parameter_acl.parname, 'SET')
|
||||||
|
or has_parameter_privilege('leoclean_kb_stage_owner', parameter_acl.parname, 'ALTER SYSTEM');
|
||||||
|
if unexpected is not null then
|
||||||
|
raise exception 'scoped Leo roles retain parameter privileges: %', unexpected;
|
||||||
|
end if;
|
||||||
|
|
||||||
if has_schema_privilege('leoclean_kb_runtime', 'public', 'CREATE')
|
if has_schema_privilege('leoclean_kb_runtime', 'public', 'CREATE')
|
||||||
or has_schema_privilege('leoclean_kb_runtime', 'kb_stage', 'CREATE')
|
or has_schema_privilege('leoclean_kb_runtime', 'kb_stage', 'CREATE')
|
||||||
or has_schema_privilege('leoclean_kb_stage_owner', 'public', 'CREATE')
|
or has_schema_privilege('leoclean_kb_stage_owner', 'public', 'CREATE')
|
||||||
|
|
@ -698,137 +1315,9 @@ begin
|
||||||
end
|
end
|
||||||
$verification$;
|
$verification$;
|
||||||
|
|
||||||
commit;
|
-- LOGIN is the last state change in the same transaction as every database ACL,
|
||||||
|
-- canonical privilege, large-object, and topology assertion. Any failure rolls
|
||||||
-- The legacy restore database is not an audit fallback for the scoped runtime.
|
-- back the complete privilege migration and leaves the role NOLOGIN.
|
||||||
-- Strip direct ACLs from both scoped roles and fail if PUBLIC/group grants still
|
|
||||||
-- make any teleo_restore relation reachable.
|
|
||||||
\connect teleo_kb
|
|
||||||
|
|
||||||
begin;
|
|
||||||
|
|
||||||
select format('revoke all on schema %I from leoclean_kb_runtime, leoclean_kb_stage_owner', namespace.nspname)
|
|
||||||
from pg_catalog.pg_namespace namespace
|
|
||||||
where namespace.nspname = 'teleo_restore'
|
|
||||||
\gexec
|
|
||||||
|
|
||||||
select format('revoke all privileges on all tables in schema %I from leoclean_kb_runtime, leoclean_kb_stage_owner', namespace.nspname)
|
|
||||||
from pg_catalog.pg_namespace namespace
|
|
||||||
where namespace.nspname = 'teleo_restore'
|
|
||||||
\gexec
|
|
||||||
|
|
||||||
select format('revoke all privileges on all sequences in schema %I from leoclean_kb_runtime, leoclean_kb_stage_owner', namespace.nspname)
|
|
||||||
from pg_catalog.pg_namespace namespace
|
|
||||||
where namespace.nspname = 'teleo_restore'
|
|
||||||
\gexec
|
|
||||||
|
|
||||||
select format('revoke all privileges on all functions in schema %I from leoclean_kb_runtime, leoclean_kb_stage_owner', namespace.nspname)
|
|
||||||
from pg_catalog.pg_namespace namespace
|
|
||||||
where namespace.nspname = 'teleo_restore'
|
|
||||||
\gexec
|
|
||||||
|
|
||||||
select format('revoke all privileges on all procedures in schema %I from leoclean_kb_runtime, leoclean_kb_stage_owner', namespace.nspname)
|
|
||||||
from pg_catalog.pg_namespace namespace
|
|
||||||
where namespace.nspname = 'teleo_restore'
|
|
||||||
\gexec
|
|
||||||
|
|
||||||
select format(
|
|
||||||
'revoke all privileges (%s) on table %I.%I from %I',
|
|
||||||
pg_catalog.string_agg(pg_catalog.quote_ident(attribute.attname), ', ' order by attribute.attnum),
|
|
||||||
namespace.nspname,
|
|
||||||
relation.relname,
|
|
||||||
target.rolname
|
|
||||||
)
|
|
||||||
from pg_catalog.pg_class relation
|
|
||||||
join pg_catalog.pg_namespace namespace on namespace.oid = relation.relnamespace
|
|
||||||
join pg_catalog.pg_attribute attribute on attribute.attrelid = relation.oid
|
|
||||||
cross join (
|
|
||||||
values ('leoclean_kb_runtime'), ('leoclean_kb_stage_owner')
|
|
||||||
) as target(rolname)
|
|
||||||
where namespace.nspname = 'teleo_restore'
|
|
||||||
and relation.relkind in ('r', 'p', 'v', 'm', 'f')
|
|
||||||
and attribute.attnum > 0
|
|
||||||
and not attribute.attisdropped
|
|
||||||
group by namespace.nspname, relation.relname, target.rolname
|
|
||||||
\gexec
|
|
||||||
|
|
||||||
do $audit_verification$
|
|
||||||
declare
|
|
||||||
unexpected text;
|
|
||||||
scoped_role text;
|
|
||||||
begin
|
|
||||||
foreach scoped_role in array array['leoclean_kb_runtime', 'leoclean_kb_stage_owner'] loop
|
|
||||||
if to_regnamespace('teleo_restore') is not null
|
|
||||||
and (has_schema_privilege(scoped_role, 'teleo_restore', 'USAGE')
|
|
||||||
or has_schema_privilege(scoped_role, 'teleo_restore', 'CREATE')) then
|
|
||||||
raise exception '% unexpectedly has teleo_restore schema access', scoped_role;
|
|
||||||
end if;
|
|
||||||
|
|
||||||
select pg_catalog.string_agg(
|
|
||||||
pg_catalog.format('%I.%I:%s', namespace.nspname, relation.relname, privilege.name),
|
|
||||||
', '
|
|
||||||
order by relation.relname, privilege.name
|
|
||||||
)
|
|
||||||
into unexpected
|
|
||||||
from pg_catalog.pg_class relation
|
|
||||||
join pg_catalog.pg_namespace namespace on namespace.oid = relation.relnamespace
|
|
||||||
cross join (
|
|
||||||
values ('SELECT'), ('INSERT'), ('UPDATE'), ('DELETE'), ('TRUNCATE'), ('REFERENCES'), ('TRIGGER')
|
|
||||||
) privilege(name)
|
|
||||||
where namespace.nspname = 'teleo_restore'
|
|
||||||
and relation.relkind in ('r', 'p', 'v', 'm', 'f')
|
|
||||||
and has_table_privilege(scoped_role, relation.oid, privilege.name);
|
|
||||||
if unexpected is not null then
|
|
||||||
raise exception '% unexpectedly has teleo_restore table access: %', scoped_role, unexpected;
|
|
||||||
end if;
|
|
||||||
|
|
||||||
select pg_catalog.string_agg(
|
|
||||||
pg_catalog.format('%I.%I.%I:%s', namespace.nspname, relation.relname, attribute.attname, privilege.name),
|
|
||||||
', '
|
|
||||||
order by relation.relname, attribute.attnum, privilege.name
|
|
||||||
)
|
|
||||||
into unexpected
|
|
||||||
from pg_catalog.pg_class relation
|
|
||||||
join pg_catalog.pg_namespace namespace on namespace.oid = relation.relnamespace
|
|
||||||
join pg_catalog.pg_attribute attribute on attribute.attrelid = relation.oid
|
|
||||||
cross join (values ('SELECT'), ('INSERT'), ('UPDATE'), ('REFERENCES')) privilege(name)
|
|
||||||
where namespace.nspname = 'teleo_restore'
|
|
||||||
and relation.relkind in ('r', 'p', 'v', 'm', 'f')
|
|
||||||
and attribute.attnum > 0
|
|
||||||
and not attribute.attisdropped
|
|
||||||
and has_column_privilege(scoped_role, relation.oid, attribute.attnum, privilege.name);
|
|
||||||
if unexpected is not null then
|
|
||||||
raise exception '% unexpectedly has teleo_restore column access: %', scoped_role, unexpected;
|
|
||||||
end if;
|
|
||||||
|
|
||||||
select pg_catalog.string_agg(
|
|
||||||
pg_catalog.format('%I.%I:%s', namespace.nspname, relation.relname, privilege.name),
|
|
||||||
', '
|
|
||||||
order by relation.relname, privilege.name
|
|
||||||
)
|
|
||||||
into unexpected
|
|
||||||
from pg_catalog.pg_class relation
|
|
||||||
join pg_catalog.pg_namespace namespace on namespace.oid = relation.relnamespace
|
|
||||||
cross join (values ('USAGE'), ('SELECT'), ('UPDATE')) privilege(name)
|
|
||||||
where namespace.nspname = 'teleo_restore'
|
|
||||||
and relation.relkind = 'S'
|
|
||||||
and has_sequence_privilege(scoped_role, relation.oid, privilege.name);
|
|
||||||
if unexpected is not null then
|
|
||||||
raise exception '% unexpectedly has teleo_restore sequence access: %', scoped_role, unexpected;
|
|
||||||
end if;
|
|
||||||
end loop;
|
|
||||||
end
|
|
||||||
$audit_verification$;
|
|
||||||
|
|
||||||
commit;
|
|
||||||
|
|
||||||
\connect teleo_canonical
|
|
||||||
|
|
||||||
-- LOGIN is the last state change. If this verification fails, the transaction
|
|
||||||
-- rolls back and the role remains NOLOGIN while prior least-privilege grants
|
|
||||||
-- stay inert for direct authentication.
|
|
||||||
begin;
|
|
||||||
|
|
||||||
alter role leoclean_kb_runtime
|
alter role leoclean_kb_runtime
|
||||||
with login nosuperuser nocreatedb nocreaterole noinherit noreplication nobypassrls connection limit 8;
|
with login nosuperuser nocreatedb nocreaterole noinherit noreplication nobypassrls connection limit 8;
|
||||||
|
|
||||||
|
|
@ -838,14 +1327,16 @@ begin
|
||||||
select 1
|
select 1
|
||||||
from pg_catalog.pg_roles role_row
|
from pg_catalog.pg_roles role_row
|
||||||
where role_row.rolname = 'leoclean_kb_runtime'
|
where role_row.rolname = 'leoclean_kb_runtime'
|
||||||
and (not role_row.rolcanlogin
|
and (
|
||||||
or role_row.rolsuper
|
not role_row.rolcanlogin
|
||||||
or role_row.rolcreatedb
|
or role_row.rolsuper
|
||||||
or role_row.rolcreaterole
|
or role_row.rolcreatedb
|
||||||
or role_row.rolinherit
|
or role_row.rolcreaterole
|
||||||
or role_row.rolreplication
|
or role_row.rolinherit
|
||||||
or role_row.rolbypassrls
|
or role_row.rolreplication
|
||||||
or role_row.rolconnlimit <> 8)
|
or role_row.rolbypassrls
|
||||||
|
or role_row.rolconnlimit <> 8
|
||||||
|
)
|
||||||
) then
|
) then
|
||||||
raise exception 'leoclean_kb_runtime final login attributes are unsafe';
|
raise exception 'leoclean_kb_runtime final login attributes are unsafe';
|
||||||
end if;
|
end if;
|
||||||
|
|
@ -853,32 +1344,3 @@ end
|
||||||
$login_verification$;
|
$login_verification$;
|
||||||
|
|
||||||
commit;
|
commit;
|
||||||
|
|
||||||
select jsonb_build_object(
|
|
||||||
'role', rolname,
|
|
||||||
'superuser', rolsuper,
|
|
||||||
'create_db', rolcreatedb,
|
|
||||||
'create_role', rolcreaterole,
|
|
||||||
'inherit', rolinherit,
|
|
||||||
'replication', rolreplication,
|
|
||||||
'bypass_rls', rolbypassrls,
|
|
||||||
'has_role_memberships', exists (
|
|
||||||
select 1
|
|
||||||
from pg_catalog.pg_auth_members membership
|
|
||||||
where membership.roleid = pg_catalog.pg_roles.oid
|
|
||||||
or membership.member = pg_catalog.pg_roles.oid
|
|
||||||
),
|
|
||||||
'can_select_claims', has_table_privilege(rolname, 'public.claims', 'SELECT'),
|
|
||||||
'can_select_proposals', has_table_privilege(rolname, 'kb_stage.kb_proposals', 'SELECT'),
|
|
||||||
'can_insert_proposals_directly', has_table_privilege(rolname, 'kb_stage.kb_proposals', 'INSERT'),
|
|
||||||
'can_connect_legacy_database', has_database_privilege(rolname, 'teleo_kb', 'CONNECT'),
|
|
||||||
'can_create_database_objects', has_database_privilege(rolname, 'teleo_canonical', 'CREATE'),
|
|
||||||
'can_create_temp_objects', has_database_privilege(rolname, 'teleo_canonical', 'TEMP'),
|
|
||||||
'can_stage_proposal', has_function_privilege(
|
|
||||||
rolname,
|
|
||||||
'kb_stage.stage_leoclean_proposal(text,text,text,text,jsonb)',
|
|
||||||
'EXECUTE'
|
|
||||||
)
|
|
||||||
)::text
|
|
||||||
from pg_catalog.pg_roles
|
|
||||||
where rolname = 'leoclean_kb_runtime';
|
|
||||||
|
|
|
||||||
150
ops/provision_gcp_leoclean_runtime_role.sh
Executable file
150
ops/provision_gcp_leoclean_runtime_role.sh
Executable file
|
|
@ -0,0 +1,150 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Provision the scoped GCP staging role without placing its cleartext password
|
||||||
|
# in SQL text, argv, command history, or PostgreSQL statement logs.
|
||||||
|
set +x
|
||||||
|
set -euo pipefail
|
||||||
|
umask 077
|
||||||
|
ulimit -c 0
|
||||||
|
|
||||||
|
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||||
|
export PATH
|
||||||
|
runtime_password=${TELEO_LEOCLEAN_DB_PASSWORD-}
|
||||||
|
administrator_password=${PGPASSWORD-}
|
||||||
|
unset TELEO_LEOCLEAN_DB_PASSWORD PGPASSWORD
|
||||||
|
|
||||||
|
script_path=${BASH_SOURCE[0]}
|
||||||
|
case "$script_path" in
|
||||||
|
*/*) script_directory=${script_path%/*} ;;
|
||||||
|
*) script_directory=. ;;
|
||||||
|
esac
|
||||||
|
SCRIPT_DIR="$(cd -- "$script_directory" && pwd -P)"
|
||||||
|
SQL_FILE="$SCRIPT_DIR/gcp_leoclean_runtime_role.sql"
|
||||||
|
SERVER_CA_FILE="$SCRIPT_DIR/gcp-teleo-pgvector-standby-server-ca.pem"
|
||||||
|
PSQL_BIN=/usr/bin/psql
|
||||||
|
SETSID_BIN=/usr/bin/setsid
|
||||||
|
ENV_BIN=/usr/bin/env
|
||||||
|
SHA256SUM_BIN=/usr/bin/sha256sum
|
||||||
|
SERVER_CA_SHA256=80701e768f0e1f6b9d621aa0b53f6e851daaa276c6d9a8e51a300fbc015539cb
|
||||||
|
|
||||||
|
assert_root_owned_nonwritable() {
|
||||||
|
local target="$1" state uid mode
|
||||||
|
state="$(stat -Lc '%u:%a' -- "$target")"
|
||||||
|
uid=${state%%:*}
|
||||||
|
mode=${state#*:}
|
||||||
|
if [ "$uid" != 0 ] || (( (8#$mode & 8#022) != 0 )); then
|
||||||
|
echo "Refusing an untrusted PostgreSQL executable path." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_trusted_executable() {
|
||||||
|
local requested="$1" resolved current
|
||||||
|
[ -x "$requested" ] || {
|
||||||
|
echo "Required PostgreSQL client is unavailable." >&2
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
resolved="$(readlink -f -- "$requested")"
|
||||||
|
[ -n "$resolved" ] && [ -f "$resolved" ] && [ -x "$resolved" ] || {
|
||||||
|
echo "Required PostgreSQL client did not resolve to an executable file." >&2
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
assert_root_owned_nonwritable "$requested"
|
||||||
|
current="$(dirname "$requested")"
|
||||||
|
while :; do
|
||||||
|
assert_root_owned_nonwritable "$current"
|
||||||
|
[ "$current" = / ] && break
|
||||||
|
current="$(dirname "$current")"
|
||||||
|
done
|
||||||
|
current="$resolved"
|
||||||
|
while :; do
|
||||||
|
assert_root_owned_nonwritable "$current"
|
||||||
|
[ "$current" = / ] && break
|
||||||
|
current="$(dirname "$current")"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ ! -f "$SQL_FILE" ] || [ -L "$SQL_FILE" ] || [ ! -f "$SERVER_CA_FILE" ] || [ -L "$SERVER_CA_FILE" ]; then
|
||||||
|
echo "Reviewed GCP runtime-role SQL is unavailable." >&2
|
||||||
|
exit 66
|
||||||
|
fi
|
||||||
|
assert_trusted_executable "$PSQL_BIN"
|
||||||
|
assert_trusted_executable "$SETSID_BIN"
|
||||||
|
assert_trusted_executable "$ENV_BIN"
|
||||||
|
assert_trusted_executable "$SHA256SUM_BIN"
|
||||||
|
if [ "$("$SHA256SUM_BIN" "$SERVER_CA_FILE")" != "$SERVER_CA_SHA256 $SERVER_CA_FILE" ]; then
|
||||||
|
echo "Reviewed Cloud SQL server CA does not match the pinned certificate." >&2
|
||||||
|
exit 66
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$runtime_password" ] || [ "${#runtime_password}" -gt 4096 ]; then
|
||||||
|
echo "TELEO_LEOCLEAN_DB_PASSWORD must contain a bounded nonempty value." >&2
|
||||||
|
exit 64
|
||||||
|
fi
|
||||||
|
case "$runtime_password" in
|
||||||
|
*$'\n'*|*$'\r'*)
|
||||||
|
echo "TELEO_LEOCLEAN_DB_PASSWORD must not contain line breaks." >&2
|
||||||
|
exit 64
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
if [ -z "$administrator_password" ] || [ "${#administrator_password}" -gt 4096 ]; then
|
||||||
|
echo "PGPASSWORD must contain a bounded nonempty administrator credential." >&2
|
||||||
|
exit 64
|
||||||
|
fi
|
||||||
|
case "$administrator_password" in
|
||||||
|
*$'\n'*|*$'\r'*)
|
||||||
|
echo "PGPASSWORD must not contain line breaks." >&2
|
||||||
|
exit 64
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
cleanup_password() {
|
||||||
|
runtime_password=
|
||||||
|
administrator_password=
|
||||||
|
unset runtime_password administrator_password
|
||||||
|
}
|
||||||
|
provision_pid=
|
||||||
|
terminate_provisioning() {
|
||||||
|
local exit_status="$1"
|
||||||
|
trap - INT TERM
|
||||||
|
if [[ "$provision_pid" =~ ^[1-9][0-9]*$ ]]; then
|
||||||
|
kill -TERM -- "-$provision_pid" 2>/dev/null || kill -TERM -- "$provision_pid" 2>/dev/null || true
|
||||||
|
wait "$provision_pid" 2>/dev/null || true
|
||||||
|
provision_pid=
|
||||||
|
fi
|
||||||
|
cleanup_password
|
||||||
|
exit "$exit_status"
|
||||||
|
}
|
||||||
|
trap cleanup_password EXIT
|
||||||
|
trap 'terminate_provisioning 130' INT
|
||||||
|
trap 'terminate_provisioning 143' TERM
|
||||||
|
|
||||||
|
# \password encrypts client-side. --no-password ensures an administrator-login
|
||||||
|
# prompt cannot consume either of the two runtime-password records from stdin.
|
||||||
|
# setsid detaches psql from any controlling terminal without --fork, keeping
|
||||||
|
# its PID as the session/process-group leader so an interruption can terminate
|
||||||
|
# and reap the complete credential-bearing child before this wrapper returns.
|
||||||
|
{
|
||||||
|
printf '%s\n' "$runtime_password"
|
||||||
|
printf '%s\n' "$runtime_password"
|
||||||
|
} | "$ENV_BIN" -i \
|
||||||
|
PATH="$PATH" \
|
||||||
|
LANG=C LC_ALL=C \
|
||||||
|
PYTHONNOUSERSITE=1 PYTHONSAFEPATH=1 \
|
||||||
|
PGHOST=10.61.0.3 \
|
||||||
|
PGPORT=5432 \
|
||||||
|
PGDATABASE=teleo_canonical \
|
||||||
|
PGUSER=postgres \
|
||||||
|
PGPASSWORD="$administrator_password" \
|
||||||
|
PGSSLMODE=verify-ca \
|
||||||
|
PGSSLROOTCERT="$SERVER_CA_FILE" \
|
||||||
|
"$SETSID_BIN" -- "$PSQL_BIN" \
|
||||||
|
--no-psqlrc \
|
||||||
|
--no-password \
|
||||||
|
--set=ON_ERROR_STOP=1 \
|
||||||
|
--file="$SQL_FILE" &
|
||||||
|
provision_pid=$!
|
||||||
|
set +e
|
||||||
|
wait "$provision_pid"
|
||||||
|
status=$?
|
||||||
|
set -e
|
||||||
|
provision_pid=
|
||||||
|
exit "$status"
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/python3 -I
|
||||||
"""Retain a sanitized least-privilege receipt for the GCP Leo runtime role.
|
"""Retain a sanitized least-privilege receipt for the GCP Leo runtime role.
|
||||||
|
|
||||||
This verifier is intentionally service-independent. It must run on the private
|
This verifier is intentionally service-independent. It must run on the private
|
||||||
|
|
@ -19,8 +19,8 @@ import re
|
||||||
import stat
|
import stat
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
|
||||||
from collections.abc import Callable, Mapping
|
from collections.abc import Callable, Mapping
|
||||||
|
from contextlib import nullcontext
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
@ -29,8 +29,10 @@ from typing import Any
|
||||||
PROJECT_ID = "teleo-501523"
|
PROJECT_ID = "teleo-501523"
|
||||||
PRIVATE_CLOUDSQL_HOST = "10.61.0.3"
|
PRIVATE_CLOUDSQL_HOST = "10.61.0.3"
|
||||||
PRIVATE_CLOUDSQL_PORT = 5432
|
PRIVATE_CLOUDSQL_PORT = 5432
|
||||||
|
EXPECTED_SYSTEM_IDENTIFIER = "7659718422914359312"
|
||||||
CANONICAL_DATABASE = "teleo_canonical"
|
CANONICAL_DATABASE = "teleo_canonical"
|
||||||
LEGACY_DATABASE = "teleo_kb"
|
LEGACY_DATABASE = "teleo_kb"
|
||||||
|
TEMPLATE_DATABASE = "template1"
|
||||||
RUNTIME_DATABASE_ROLE = "leoclean_kb_runtime"
|
RUNTIME_DATABASE_ROLE = "leoclean_kb_runtime"
|
||||||
STAGE_OWNER_DATABASE_ROLE = "leoclean_kb_stage_owner"
|
STAGE_OWNER_DATABASE_ROLE = "leoclean_kb_stage_owner"
|
||||||
RUNTIME_UNIX_USER = "teleo"
|
RUNTIME_UNIX_USER = "teleo"
|
||||||
|
|
@ -38,6 +40,9 @@ SCOPED_PASSWORD_SECRET = "gcp-teleo-pgvector-standby-leoclean-kb-runtime-passwor
|
||||||
ADMINISTRATOR_PASSWORD_SECRET = "gcp-teleo-pgvector-standby-postgres-password"
|
ADMINISTRATOR_PASSWORD_SECRET = "gcp-teleo-pgvector-standby-postgres-password"
|
||||||
GCLOUD_BIN = "/usr/bin/gcloud"
|
GCLOUD_BIN = "/usr/bin/gcloud"
|
||||||
PSQL_BIN = "/usr/bin/psql"
|
PSQL_BIN = "/usr/bin/psql"
|
||||||
|
SERVER_CA_PATH = Path("/usr/local/libexec/livingip/leoclean-kb/cloudsql-server-ca.pem")
|
||||||
|
RUNTIME_CLOUDSDK_CONFIG = Path("/usr/local/libexec/livingip/leoclean-kb/gcloud-config")
|
||||||
|
SERVER_CA_SHA256 = "80701e768f0e1f6b9d621aa0b53f6e851daaa276c6d9a8e51a300fbc015539cb"
|
||||||
COMMAND_TIMEOUT_SECONDS = 30
|
COMMAND_TIMEOUT_SECONDS = 30
|
||||||
CHILD_PATH = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
CHILD_PATH = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
||||||
RUN_ID_RE = re.compile(r"[a-z0-9][a-z0-9-]{6,62}[a-z0-9]\Z")
|
RUN_ID_RE = re.compile(r"[a-z0-9][a-z0-9-]{6,62}[a-z0-9]\Z")
|
||||||
|
|
@ -195,8 +200,14 @@ CATALOG_ZERO_COUNT_FIELDS: tuple[str, ...] = (
|
||||||
"column_dml_grants",
|
"column_dml_grants",
|
||||||
"database_create_grants",
|
"database_create_grants",
|
||||||
"missing_allowed_table_selects",
|
"missing_allowed_table_selects",
|
||||||
|
"large_object_acl_privileges",
|
||||||
|
"large_object_mutation_routine_execute",
|
||||||
|
"public_large_object_mutation_routine_execute",
|
||||||
|
"other_scoped_backends",
|
||||||
"owned_database_objects",
|
"owned_database_objects",
|
||||||
|
"owned_large_objects",
|
||||||
"owner_database_create_grants",
|
"owner_database_create_grants",
|
||||||
|
"owner_database_connect_grants",
|
||||||
"owner_direct_database_acl_entries",
|
"owner_direct_database_acl_entries",
|
||||||
"owner_missing_agent_select_columns",
|
"owner_missing_agent_select_columns",
|
||||||
"owner_missing_proposal_insert_columns",
|
"owner_missing_proposal_insert_columns",
|
||||||
|
|
@ -210,38 +221,44 @@ CATALOG_ZERO_COUNT_FIELDS: tuple[str, ...] = (
|
||||||
"owner_unexpected_schema_usage",
|
"owner_unexpected_schema_usage",
|
||||||
"owner_unexpected_table_selects",
|
"owner_unexpected_table_selects",
|
||||||
"schema_create_grants",
|
"schema_create_grants",
|
||||||
|
"parameter_privileges",
|
||||||
|
"public_database_connect_grants",
|
||||||
|
"scoped_prepared_xacts",
|
||||||
"sequence_grants",
|
"sequence_grants",
|
||||||
"table_dml_grants",
|
"table_dml_grants",
|
||||||
"unexpected_column_selects",
|
"unexpected_column_selects",
|
||||||
"unexpected_direct_database_acl_entries",
|
"unexpected_direct_database_acl_entries",
|
||||||
|
"unexpected_connectable_databases",
|
||||||
"unexpected_routine_execute",
|
"unexpected_routine_execute",
|
||||||
|
"unexpected_role_settings",
|
||||||
"unexpected_schema_usage",
|
"unexpected_schema_usage",
|
||||||
"unexpected_security_definer_execute",
|
"unexpected_security_definer_execute",
|
||||||
"unexpected_table_selects",
|
"unexpected_table_selects",
|
||||||
|
"unsafe_allowed_relation_kinds",
|
||||||
|
"unsafe_allowed_relation_inheritance",
|
||||||
|
"unsafe_proposal_column_contract",
|
||||||
|
"unsafe_proposal_default_dependencies",
|
||||||
|
"unsafe_proposal_defaults",
|
||||||
|
"unsafe_proposal_constraint_dependencies",
|
||||||
|
"unsafe_proposal_index_expressions",
|
||||||
|
"unsafe_proposal_policies",
|
||||||
|
"unsafe_proposal_rewrite_rules",
|
||||||
|
"unsafe_proposal_triggers",
|
||||||
|
"unsafe_proposal_generated_columns",
|
||||||
)
|
)
|
||||||
|
|
||||||
CATALOG_TRUE_FIELDS: tuple[str, ...] = (
|
CATALOG_TRUE_FIELDS: tuple[str, ...] = (
|
||||||
"canonical_connect",
|
"canonical_connect",
|
||||||
"owner_grant_contract",
|
"owner_grant_contract",
|
||||||
"owner_schema_usage",
|
"owner_schema_usage",
|
||||||
|
"proposal_table_contract",
|
||||||
|
"proposal_constraint_shape",
|
||||||
"runtime_connect_acl_exact",
|
"runtime_connect_acl_exact",
|
||||||
"runtime_schema_usage",
|
"runtime_schema_usage",
|
||||||
|
"runtime_setting_contract",
|
||||||
|
"effective_setting_contract",
|
||||||
)
|
)
|
||||||
|
|
||||||
LEGACY_CATALOG_ZERO_COUNT_FIELDS: tuple[str, ...] = (
|
|
||||||
"owner_column_privileges",
|
|
||||||
"owner_routine_privileges",
|
|
||||||
"owner_schema_privileges",
|
|
||||||
"owner_sequence_privileges",
|
|
||||||
"owner_table_privileges",
|
|
||||||
"runtime_column_privileges",
|
|
||||||
"runtime_routine_privileges",
|
|
||||||
"runtime_schema_privileges",
|
|
||||||
"runtime_sequence_privileges",
|
|
||||||
"runtime_table_privileges",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class CommandResult:
|
class CommandResult:
|
||||||
returncode: int
|
returncode: int
|
||||||
|
|
@ -250,6 +267,7 @@ class CommandResult:
|
||||||
|
|
||||||
|
|
||||||
Runner = Callable[[list[str], Mapping[str, str], int, bool], CommandResult]
|
Runner = Callable[[list[str], Mapping[str, str], int, bool], CommandResult]
|
||||||
|
DependencyValidator = Callable[[], None]
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
|
|
@ -258,6 +276,7 @@ class NegativeCheck:
|
||||||
database: str
|
database: str
|
||||||
sql: str
|
sql: str
|
||||||
expected_sqlstate: str
|
expected_sqlstate: str
|
||||||
|
expected_connection_denial: bool = False
|
||||||
|
|
||||||
|
|
||||||
class VerificationError(RuntimeError):
|
class VerificationError(RuntimeError):
|
||||||
|
|
@ -328,14 +347,110 @@ def _base_child_environment(_source: Mapping[str, str]) -> dict[str, str]:
|
||||||
"LANG": "C",
|
"LANG": "C",
|
||||||
"LC_ALL": "C",
|
"LC_ALL": "C",
|
||||||
"PATH": CHILD_PATH,
|
"PATH": CHILD_PATH,
|
||||||
|
"PYTHONNOUSERSITE": "1",
|
||||||
|
"PYTHONSAFEPATH": "1",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _path_and_parents(path: Path) -> tuple[Path, ...]:
|
||||||
|
resolved = path if path.is_absolute() else path.absolute()
|
||||||
|
return (resolved, *resolved.parents)
|
||||||
|
|
||||||
|
|
||||||
|
def _assert_root_owned_nonwritable(path: Path, *, follow_symlinks: bool = True) -> None:
|
||||||
|
try:
|
||||||
|
metadata = path.stat() if follow_symlinks else path.lstat()
|
||||||
|
except OSError:
|
||||||
|
raise VerificationError("runtime_dependency_untrusted", "runtime_dependencies") from None
|
||||||
|
if metadata.st_uid != 0 or metadata.st_mode & (stat.S_IWGRP | stat.S_IWOTH):
|
||||||
|
raise VerificationError("runtime_dependency_untrusted", "runtime_dependencies")
|
||||||
|
if os.geteuid() != 0 and os.access(path, os.W_OK):
|
||||||
|
raise VerificationError("runtime_dependency_untrusted", "runtime_dependencies")
|
||||||
|
|
||||||
|
|
||||||
|
def _assert_trusted_executable(raw_path: str) -> None:
|
||||||
|
requested = Path(raw_path)
|
||||||
|
if not requested.is_absolute():
|
||||||
|
raise VerificationError("runtime_dependency_untrusted", "runtime_dependencies")
|
||||||
|
try:
|
||||||
|
resolved = requested.resolve(strict=True)
|
||||||
|
metadata = resolved.stat()
|
||||||
|
except OSError:
|
||||||
|
raise VerificationError("runtime_dependency_untrusted", "runtime_dependencies") from None
|
||||||
|
if not stat.S_ISREG(metadata.st_mode) or not os.access(resolved, os.X_OK):
|
||||||
|
raise VerificationError("runtime_dependency_untrusted", "runtime_dependencies")
|
||||||
|
try:
|
||||||
|
requested_metadata = requested.lstat()
|
||||||
|
except OSError:
|
||||||
|
raise VerificationError("runtime_dependency_untrusted", "runtime_dependencies") from None
|
||||||
|
if requested_metadata.st_uid != 0 or (
|
||||||
|
not stat.S_ISLNK(requested_metadata.st_mode)
|
||||||
|
and requested_metadata.st_mode & (stat.S_IWGRP | stat.S_IWOTH)
|
||||||
|
):
|
||||||
|
raise VerificationError("runtime_dependency_untrusted", "runtime_dependencies")
|
||||||
|
for candidate in (*_path_and_parents(requested.parent), *_path_and_parents(resolved)):
|
||||||
|
_assert_root_owned_nonwritable(candidate)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_runtime_dependencies() -> None:
|
||||||
|
_assert_trusted_executable(GCLOUD_BIN)
|
||||||
|
_assert_trusted_executable(PSQL_BIN)
|
||||||
|
runtime_cloudsdk_config_path()
|
||||||
|
server_ca = runtime_server_ca_path()
|
||||||
|
for candidate in _path_and_parents(server_ca):
|
||||||
|
_assert_root_owned_nonwritable(candidate)
|
||||||
|
try:
|
||||||
|
certificate = server_ca.read_bytes()
|
||||||
|
except OSError:
|
||||||
|
raise VerificationError("runtime_dependency_untrusted", "runtime_dependencies") from None
|
||||||
|
if hashlib.sha256(certificate).hexdigest() != SERVER_CA_SHA256:
|
||||||
|
raise VerificationError("runtime_dependency_untrusted", "runtime_dependencies")
|
||||||
|
|
||||||
|
|
||||||
|
def runtime_server_ca_path() -> Path:
|
||||||
|
raw = os.environ.get("TELEO_GCP_PREFLIGHT_SSL_ROOT_CERT", str(SERVER_CA_PATH))
|
||||||
|
candidate = Path(raw)
|
||||||
|
try:
|
||||||
|
if not candidate.is_absolute() or candidate.is_symlink():
|
||||||
|
raise OSError
|
||||||
|
resolved = candidate.resolve(strict=True)
|
||||||
|
except OSError:
|
||||||
|
raise VerificationError("runtime_dependency_untrusted", "runtime_dependencies") from None
|
||||||
|
if resolved != candidate or not resolved.is_file():
|
||||||
|
raise VerificationError("runtime_dependency_untrusted", "runtime_dependencies")
|
||||||
|
return resolved
|
||||||
|
|
||||||
|
|
||||||
|
def selected_server_ca_path() -> Path:
|
||||||
|
return Path(os.environ.get("TELEO_GCP_PREFLIGHT_SSL_ROOT_CERT", str(SERVER_CA_PATH)))
|
||||||
|
|
||||||
|
|
||||||
|
def runtime_cloudsdk_config_path() -> Path:
|
||||||
|
raw = os.environ.get("TELEO_GCP_PREFLIGHT_CLOUDSDK_CONFIG", str(RUNTIME_CLOUDSDK_CONFIG))
|
||||||
|
candidate = Path(raw)
|
||||||
|
try:
|
||||||
|
if not candidate.is_absolute() or candidate.is_symlink():
|
||||||
|
raise OSError
|
||||||
|
resolved = candidate.resolve(strict=True)
|
||||||
|
if resolved != candidate or not resolved.is_dir() or any(resolved.iterdir()):
|
||||||
|
raise OSError
|
||||||
|
except OSError:
|
||||||
|
raise VerificationError("runtime_dependency_untrusted", "runtime_dependencies") from None
|
||||||
|
for part in _path_and_parents(resolved):
|
||||||
|
_assert_root_owned_nonwritable(part)
|
||||||
|
return resolved
|
||||||
|
|
||||||
|
|
||||||
|
def selected_cloudsdk_config_path() -> Path:
|
||||||
|
return Path(os.environ.get("TELEO_GCP_PREFLIGHT_CLOUDSDK_CONFIG", str(RUNTIME_CLOUDSDK_CONFIG)))
|
||||||
|
|
||||||
|
|
||||||
def _gcloud_environment(source: Mapping[str, str], config_dir: Path) -> dict[str, str]:
|
def _gcloud_environment(source: Mapping[str, str], config_dir: Path) -> dict[str, str]:
|
||||||
env = _base_child_environment(source)
|
env = _base_child_environment(source)
|
||||||
env.update(
|
env.update(
|
||||||
{
|
{
|
||||||
"CLOUDSDK_CONFIG": str(config_dir),
|
"CLOUDSDK_CONFIG": str(config_dir),
|
||||||
|
"CLOUDSDK_CORE_DISABLE_FILE_LOGGING": "true",
|
||||||
"CLOUDSDK_CORE_DISABLE_PROMPTS": "1",
|
"CLOUDSDK_CORE_DISABLE_PROMPTS": "1",
|
||||||
"CLOUDSDK_CORE_PROJECT": PROJECT_ID,
|
"CLOUDSDK_CORE_PROJECT": PROJECT_ID,
|
||||||
}
|
}
|
||||||
|
|
@ -345,7 +460,13 @@ def _gcloud_environment(source: Mapping[str, str], config_dir: Path) -> dict[str
|
||||||
|
|
||||||
def _psql_environment(source: Mapping[str, str], password: str) -> dict[str, str]:
|
def _psql_environment(source: Mapping[str, str], password: str) -> dict[str, str]:
|
||||||
env = _base_child_environment(source)
|
env = _base_child_environment(source)
|
||||||
env["PGPASSWORD"] = password
|
env.update(
|
||||||
|
{
|
||||||
|
"PGPASSWORD": password,
|
||||||
|
"PGSSLMODE": "verify-ca",
|
||||||
|
"PGSSLROOTCERT": str(selected_server_ca_path()),
|
||||||
|
}
|
||||||
|
)
|
||||||
return env
|
return env
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -424,7 +545,7 @@ def _assert_administrator_secret_denied(*, env: Mapping[str, str], runner: Runne
|
||||||
def _conninfo(database: str) -> str:
|
def _conninfo(database: str) -> str:
|
||||||
return (
|
return (
|
||||||
f"host={PRIVATE_CLOUDSQL_HOST} port={PRIVATE_CLOUDSQL_PORT} dbname={database} "
|
f"host={PRIVATE_CLOUDSQL_HOST} port={PRIVATE_CLOUDSQL_PORT} dbname={database} "
|
||||||
f"user={RUNTIME_DATABASE_ROLE} sslmode=require connect_timeout=10 "
|
f"user={RUNTIME_DATABASE_ROLE} sslmode=verify-ca sslrootcert={selected_server_ca_path()} connect_timeout=10 "
|
||||||
"application_name=leoclean_runtime_permission_verifier"
|
"application_name=leoclean_runtime_permission_verifier"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -515,7 +636,8 @@ select pg_catalog.jsonb_build_object(
|
||||||
'ssl_version', coalesce(
|
'ssl_version', coalesce(
|
||||||
(select version from pg_catalog.pg_stat_ssl where pid = pg_catalog.pg_backend_pid()),
|
(select version from pg_catalog.pg_stat_ssl where pid = pg_catalog.pg_backend_pid()),
|
||||||
''
|
''
|
||||||
)
|
),
|
||||||
|
'system_identifier', (select system_identifier::text from pg_catalog.pg_control_system())
|
||||||
)::text;
|
)::text;
|
||||||
""".strip()
|
""".strip()
|
||||||
|
|
||||||
|
|
@ -689,17 +811,60 @@ with runtime_role as (
|
||||||
), allowed_relation as (
|
), allowed_relation as (
|
||||||
select allowed_select.nspname,
|
select allowed_select.nspname,
|
||||||
allowed_select.relname,
|
allowed_select.relname,
|
||||||
relation.oid
|
relation.oid,
|
||||||
|
relation.relkind
|
||||||
from allowed_select
|
from allowed_select
|
||||||
left join pg_catalog.pg_namespace namespace
|
left join pg_catalog.pg_namespace namespace
|
||||||
on namespace.nspname = allowed_select.nspname
|
on namespace.nspname = allowed_select.nspname
|
||||||
left join pg_catalog.pg_class relation
|
left join pg_catalog.pg_class relation
|
||||||
on relation.relnamespace = namespace.oid
|
on relation.relnamespace = namespace.oid
|
||||||
and relation.relname = allowed_select.relname
|
and relation.relname = allowed_select.relname
|
||||||
and relation.relkind in ('r', 'p', 'v', 'm', 'f')
|
|
||||||
), owner_insert_column(attname) as (
|
), owner_insert_column(attname) as (
|
||||||
values
|
values
|
||||||
{owner_insert_values}
|
{owner_insert_values}
|
||||||
|
), expected_proposal_column(ordinal, attname, typname, not_null, has_default, collated) as (
|
||||||
|
values
|
||||||
|
(1, 'id', 'uuid', true, true, false),
|
||||||
|
(2, 'proposal_type', 'text', true, false, true),
|
||||||
|
(3, 'status', 'text', true, true, true),
|
||||||
|
(4, 'proposed_by_handle', 'text', false, false, true),
|
||||||
|
(5, 'proposed_by_agent_id', 'uuid', false, false, false),
|
||||||
|
(6, 'channel', 'text', true, true, true),
|
||||||
|
(7, 'source_ref', 'text', false, false, true),
|
||||||
|
(8, 'rationale', 'text', true, false, true),
|
||||||
|
(9, 'payload', 'jsonb', true, false, false),
|
||||||
|
(10, 'reviewed_by_handle', 'text', false, false, true),
|
||||||
|
(11, 'reviewed_by_agent_id', 'uuid', false, false, false),
|
||||||
|
(12, 'reviewed_at', 'timestamptz', false, false, false),
|
||||||
|
(13, 'review_note', 'text', false, false, true),
|
||||||
|
(14, 'applied_by_handle', 'text', false, false, true),
|
||||||
|
(15, 'applied_by_agent_id', 'uuid', false, false, false),
|
||||||
|
(16, 'applied_at', 'timestamptz', false, false, false),
|
||||||
|
(17, 'created_at', 'timestamptz', true, true, false),
|
||||||
|
(18, 'updated_at', 'timestamptz', true, true, false)
|
||||||
|
), proposal_table as (
|
||||||
|
select relation.*
|
||||||
|
from pg_catalog.pg_class relation
|
||||||
|
join pg_catalog.pg_namespace namespace on namespace.oid = relation.relnamespace
|
||||||
|
where namespace.nspname = 'kb_stage'
|
||||||
|
and relation.relname = 'kb_proposals'
|
||||||
|
), expected_proposal_default(attname, expression) as (
|
||||||
|
values
|
||||||
|
('id', 'gen_random_uuid()'),
|
||||||
|
('status', '''pending_review''::text'),
|
||||||
|
('channel', '''cli''::text'),
|
||||||
|
('created_at', 'now()'),
|
||||||
|
('updated_at', 'now()')
|
||||||
|
), actual_proposal_default as (
|
||||||
|
select attribute.attname,
|
||||||
|
pg_catalog.pg_get_expr(default_row.adbin, default_row.adrelid) as expression
|
||||||
|
from proposal_table
|
||||||
|
join pg_catalog.pg_attribute attribute on attribute.attrelid = proposal_table.oid
|
||||||
|
join pg_catalog.pg_attrdef default_row
|
||||||
|
on default_row.adrelid = attribute.attrelid
|
||||||
|
and default_row.adnum = attribute.attnum
|
||||||
|
where attribute.attnum > 0
|
||||||
|
and not attribute.attisdropped
|
||||||
)
|
)
|
||||||
select pg_catalog.jsonb_build_object(
|
select pg_catalog.jsonb_build_object(
|
||||||
'canonical_connect', coalesce(
|
'canonical_connect', coalesce(
|
||||||
|
|
@ -720,6 +885,75 @@ select pg_catalog.jsonb_build_object(
|
||||||
where database_row.datname in ({_sql_literal(CANONICAL_DATABASE)}, {_sql_literal(LEGACY_DATABASE)})
|
where database_row.datname in ({_sql_literal(CANONICAL_DATABASE)}, {_sql_literal(LEGACY_DATABASE)})
|
||||||
and acl.grantee = runtime_role.oid
|
and acl.grantee = runtime_role.oid
|
||||||
), false),
|
), false),
|
||||||
|
'runtime_setting_contract', coalesce((
|
||||||
|
select pg_catalog.count(*) = 1
|
||||||
|
and pg_catalog.bool_and(
|
||||||
|
setting_row.setdatabase = (
|
||||||
|
select database_row.oid
|
||||||
|
from pg_catalog.pg_database database_row
|
||||||
|
where database_row.datname = {_sql_literal(CANONICAL_DATABASE)}
|
||||||
|
)
|
||||||
|
and pg_catalog.cardinality(setting_row.setconfig) = 3
|
||||||
|
and setting_row.setconfig @> array[
|
||||||
|
'search_path=pg_catalog, public, kb_stage',
|
||||||
|
'statement_timeout=15s',
|
||||||
|
'lock_timeout=2s'
|
||||||
|
]::text[]
|
||||||
|
)
|
||||||
|
from pg_catalog.pg_db_role_setting setting_row
|
||||||
|
join runtime_role on runtime_role.oid = setting_row.setrole
|
||||||
|
), false),
|
||||||
|
'unexpected_role_settings', (
|
||||||
|
select pg_catalog.count(*)::int
|
||||||
|
from pg_catalog.pg_db_role_setting setting_row
|
||||||
|
where setting_row.setrole in ((select oid from runtime_role), (select oid from owner_role))
|
||||||
|
and not (
|
||||||
|
setting_row.setrole = (select oid from runtime_role)
|
||||||
|
and setting_row.setdatabase = (
|
||||||
|
select database_row.oid
|
||||||
|
from pg_catalog.pg_database database_row
|
||||||
|
where database_row.datname = {_sql_literal(CANONICAL_DATABASE)}
|
||||||
|
)
|
||||||
|
and pg_catalog.cardinality(setting_row.setconfig) = 3
|
||||||
|
and setting_row.setconfig @> array[
|
||||||
|
'search_path=pg_catalog, public, kb_stage',
|
||||||
|
'statement_timeout=15s',
|
||||||
|
'lock_timeout=2s'
|
||||||
|
]::text[]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
'effective_setting_contract', (
|
||||||
|
pg_catalog.current_setting('search_path') = 'pg_catalog, public, kb_stage'
|
||||||
|
and pg_catalog.current_setting('statement_timeout') = '15s'
|
||||||
|
and pg_catalog.current_setting('lock_timeout') = '2s'
|
||||||
|
and pg_catalog.current_setting('session_preload_libraries') = ''
|
||||||
|
and pg_catalog.current_setting('local_preload_libraries') = ''
|
||||||
|
and pg_catalog.current_setting('role') = 'none'
|
||||||
|
),
|
||||||
|
'unexpected_connectable_databases', (
|
||||||
|
select pg_catalog.count(*)::int
|
||||||
|
from pg_catalog.pg_database database_row
|
||||||
|
where database_row.datname <> {_sql_literal(CANONICAL_DATABASE)}
|
||||||
|
and pg_catalog.has_database_privilege(current_user, database_row.oid, 'CONNECT')
|
||||||
|
),
|
||||||
|
'owner_database_connect_grants', (
|
||||||
|
select pg_catalog.count(*)::int
|
||||||
|
from pg_catalog.pg_database database_row
|
||||||
|
where pg_catalog.has_database_privilege(
|
||||||
|
{_sql_literal(STAGE_OWNER_DATABASE_ROLE)},
|
||||||
|
database_row.oid,
|
||||||
|
'CONNECT'
|
||||||
|
)
|
||||||
|
),
|
||||||
|
'public_database_connect_grants', (
|
||||||
|
select pg_catalog.count(*)::int
|
||||||
|
from pg_catalog.pg_database database_row
|
||||||
|
cross join lateral pg_catalog.aclexplode(
|
||||||
|
coalesce(database_row.datacl, pg_catalog.acldefault('d', database_row.datdba))
|
||||||
|
) acl
|
||||||
|
where acl.grantee = 0
|
||||||
|
and acl.privilege_type = 'CONNECT'
|
||||||
|
),
|
||||||
'unexpected_direct_database_acl_entries', (
|
'unexpected_direct_database_acl_entries', (
|
||||||
select pg_catalog.count(*)::int
|
select pg_catalog.count(*)::int
|
||||||
from pg_catalog.pg_database database_row
|
from pg_catalog.pg_database database_row
|
||||||
|
|
@ -727,8 +961,7 @@ select pg_catalog.jsonb_build_object(
|
||||||
cross join lateral pg_catalog.aclexplode(
|
cross join lateral pg_catalog.aclexplode(
|
||||||
coalesce(database_row.datacl, pg_catalog.acldefault('d', database_row.datdba))
|
coalesce(database_row.datacl, pg_catalog.acldefault('d', database_row.datdba))
|
||||||
) acl
|
) acl
|
||||||
where database_row.datname in ({_sql_literal(CANONICAL_DATABASE)}, {_sql_literal(LEGACY_DATABASE)})
|
where acl.grantee = runtime_role.oid
|
||||||
and acl.grantee = runtime_role.oid
|
|
||||||
and not (
|
and not (
|
||||||
database_row.datname = {_sql_literal(CANONICAL_DATABASE)}
|
database_row.datname = {_sql_literal(CANONICAL_DATABASE)}
|
||||||
and acl.privilege_type = 'CONNECT'
|
and acl.privilege_type = 'CONNECT'
|
||||||
|
|
@ -742,20 +975,19 @@ select pg_catalog.jsonb_build_object(
|
||||||
cross join lateral pg_catalog.aclexplode(
|
cross join lateral pg_catalog.aclexplode(
|
||||||
coalesce(database_row.datacl, pg_catalog.acldefault('d', database_row.datdba))
|
coalesce(database_row.datacl, pg_catalog.acldefault('d', database_row.datdba))
|
||||||
) acl
|
) acl
|
||||||
where database_row.datname in ({_sql_literal(CANONICAL_DATABASE)}, {_sql_literal(LEGACY_DATABASE)})
|
where acl.grantee = owner_role.oid
|
||||||
and acl.grantee = owner_role.oid
|
|
||||||
),
|
),
|
||||||
'database_create_grants', (
|
'database_create_grants', (
|
||||||
select pg_catalog.count(*)::int
|
select pg_catalog.count(*)::int
|
||||||
from (values ({_sql_literal(CANONICAL_DATABASE)}), ({_sql_literal(LEGACY_DATABASE)})) database_name(name)
|
from pg_catalog.pg_database database_row
|
||||||
where pg_catalog.has_database_privilege(current_user, database_name.name, 'CREATE')
|
where pg_catalog.has_database_privilege(current_user, database_row.oid, 'CREATE')
|
||||||
),
|
),
|
||||||
'owner_database_create_grants', (
|
'owner_database_create_grants', (
|
||||||
select pg_catalog.count(*)::int
|
select pg_catalog.count(*)::int
|
||||||
from (values ({_sql_literal(CANONICAL_DATABASE)}), ({_sql_literal(LEGACY_DATABASE)})) database_name(name)
|
from pg_catalog.pg_database database_row
|
||||||
where pg_catalog.has_database_privilege(
|
where pg_catalog.has_database_privilege(
|
||||||
{_sql_literal(STAGE_OWNER_DATABASE_ROLE)},
|
{_sql_literal(STAGE_OWNER_DATABASE_ROLE)},
|
||||||
database_name.name,
|
database_row.oid,
|
||||||
'CREATE'
|
'CREATE'
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
|
@ -837,6 +1069,241 @@ select pg_catalog.jsonb_build_object(
|
||||||
false
|
false
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
'unsafe_allowed_relation_kinds', (
|
||||||
|
select pg_catalog.count(*)::int
|
||||||
|
from allowed_relation
|
||||||
|
where allowed_relation.relkind is distinct from 'r'::"char"
|
||||||
|
),
|
||||||
|
'unsafe_allowed_relation_inheritance', (
|
||||||
|
select pg_catalog.count(*)::int
|
||||||
|
from pg_catalog.pg_inherits inheritance
|
||||||
|
where inheritance.inhparent in (select oid from allowed_relation where oid is not null)
|
||||||
|
or inheritance.inhrelid in (select oid from allowed_relation where oid is not null)
|
||||||
|
),
|
||||||
|
'unsafe_proposal_column_contract', (
|
||||||
|
select pg_catalog.count(*)::int
|
||||||
|
from expected_proposal_column expected
|
||||||
|
full join (
|
||||||
|
select attribute.attnum::int as ordinal,
|
||||||
|
attribute.attname,
|
||||||
|
type_row.typname,
|
||||||
|
attribute.attnotnull as not_null,
|
||||||
|
attribute.atthasdef as has_default,
|
||||||
|
attribute.attidentity,
|
||||||
|
attribute.attgenerated,
|
||||||
|
attribute.atttypmod,
|
||||||
|
type_namespace.nspname as type_namespace,
|
||||||
|
attribute.attcollation = 'pg_catalog.default'::pg_catalog.regcollation as collated
|
||||||
|
from proposal_table
|
||||||
|
join pg_catalog.pg_attribute attribute on attribute.attrelid = proposal_table.oid
|
||||||
|
join pg_catalog.pg_type type_row on type_row.oid = attribute.atttypid
|
||||||
|
join pg_catalog.pg_namespace type_namespace on type_namespace.oid = type_row.typnamespace
|
||||||
|
where attribute.attnum > 0
|
||||||
|
and not attribute.attisdropped
|
||||||
|
) actual using (ordinal, attname)
|
||||||
|
where expected.ordinal is null
|
||||||
|
or actual.ordinal is null
|
||||||
|
or expected.typname is distinct from actual.typname
|
||||||
|
or expected.not_null is distinct from actual.not_null
|
||||||
|
or expected.has_default is distinct from actual.has_default
|
||||||
|
or expected.collated is distinct from actual.collated
|
||||||
|
or actual.attidentity <> ''
|
||||||
|
or actual.attgenerated <> ''
|
||||||
|
or actual.atttypmod <> -1
|
||||||
|
or actual.type_namespace <> 'pg_catalog'
|
||||||
|
),
|
||||||
|
'proposal_table_contract', coalesce((
|
||||||
|
select pg_catalog.count(*) = 1
|
||||||
|
and pg_catalog.bool_and(
|
||||||
|
proposal_table.relkind = 'r'
|
||||||
|
and proposal_table.relpersistence = 'p'
|
||||||
|
and not proposal_table.relrowsecurity
|
||||||
|
and not proposal_table.relforcerowsecurity
|
||||||
|
)
|
||||||
|
from proposal_table
|
||||||
|
), false),
|
||||||
|
'unsafe_proposal_triggers', (
|
||||||
|
select pg_catalog.count(*)::int
|
||||||
|
from pg_catalog.pg_trigger trigger_row
|
||||||
|
join proposal_table on proposal_table.oid = trigger_row.tgrelid
|
||||||
|
where not trigger_row.tgisinternal
|
||||||
|
),
|
||||||
|
'unsafe_proposal_rewrite_rules', (
|
||||||
|
select pg_catalog.count(*)::int
|
||||||
|
from pg_catalog.pg_rewrite rewrite_row
|
||||||
|
join proposal_table on proposal_table.oid = rewrite_row.ev_class
|
||||||
|
),
|
||||||
|
'unsafe_proposal_policies', (
|
||||||
|
select pg_catalog.count(*)::int
|
||||||
|
from pg_catalog.pg_policy policy_row
|
||||||
|
join proposal_table on proposal_table.oid = policy_row.polrelid
|
||||||
|
),
|
||||||
|
'unsafe_proposal_generated_columns', (
|
||||||
|
select pg_catalog.count(*)::int
|
||||||
|
from pg_catalog.pg_attribute attribute
|
||||||
|
join proposal_table on proposal_table.oid = attribute.attrelid
|
||||||
|
where attribute.attnum > 0
|
||||||
|
and not attribute.attisdropped
|
||||||
|
and attribute.attgenerated <> ''
|
||||||
|
),
|
||||||
|
'unsafe_proposal_defaults', (
|
||||||
|
select pg_catalog.count(*)::int
|
||||||
|
from expected_proposal_default expected
|
||||||
|
full join actual_proposal_default actual using (attname)
|
||||||
|
where expected.expression is distinct from actual.expression
|
||||||
|
),
|
||||||
|
'unsafe_proposal_default_dependencies', (
|
||||||
|
select pg_catalog.count(*)::int
|
||||||
|
from (
|
||||||
|
select dependency.objid, dependency.refobjid
|
||||||
|
from proposal_table
|
||||||
|
join pg_catalog.pg_attrdef default_row on default_row.adrelid = proposal_table.oid
|
||||||
|
join pg_catalog.pg_depend dependency
|
||||||
|
on dependency.classid = 'pg_catalog.pg_attrdef'::pg_catalog.regclass
|
||||||
|
and dependency.objid = default_row.oid
|
||||||
|
and dependency.refclassid = 'pg_catalog.pg_proc'::pg_catalog.regclass
|
||||||
|
join pg_catalog.pg_proc function_row on function_row.oid = dependency.refobjid
|
||||||
|
join pg_catalog.pg_namespace namespace on namespace.oid = function_row.pronamespace
|
||||||
|
where namespace.nspname <> 'pg_catalog'
|
||||||
|
or function_row.oid not in (
|
||||||
|
'pg_catalog.gen_random_uuid()'::pg_catalog.regprocedure,
|
||||||
|
'pg_catalog.now()'::pg_catalog.regprocedure
|
||||||
|
)
|
||||||
|
union all
|
||||||
|
select dependency.objid, dependency.refobjid
|
||||||
|
from proposal_table
|
||||||
|
join pg_catalog.pg_attrdef default_row on default_row.adrelid = proposal_table.oid
|
||||||
|
join pg_catalog.pg_depend dependency
|
||||||
|
on dependency.classid = 'pg_catalog.pg_attrdef'::pg_catalog.regclass
|
||||||
|
and dependency.objid = default_row.oid
|
||||||
|
and dependency.refclassid = 'pg_catalog.pg_type'::pg_catalog.regclass
|
||||||
|
join pg_catalog.pg_type type_row on type_row.oid = dependency.refobjid
|
||||||
|
join pg_catalog.pg_namespace namespace on namespace.oid = type_row.typnamespace
|
||||||
|
where namespace.nspname <> 'pg_catalog'
|
||||||
|
union all
|
||||||
|
select dependency.objid, dependency.refobjid
|
||||||
|
from proposal_table
|
||||||
|
join pg_catalog.pg_attrdef default_row on default_row.adrelid = proposal_table.oid
|
||||||
|
join pg_catalog.pg_depend dependency
|
||||||
|
on dependency.classid = 'pg_catalog.pg_attrdef'::pg_catalog.regclass
|
||||||
|
and dependency.objid = default_row.oid
|
||||||
|
and dependency.refclassid = 'pg_catalog.pg_operator'::pg_catalog.regclass
|
||||||
|
join pg_catalog.pg_operator operator_row on operator_row.oid = dependency.refobjid
|
||||||
|
join pg_catalog.pg_namespace namespace on namespace.oid = operator_row.oprnamespace
|
||||||
|
where namespace.nspname <> 'pg_catalog'
|
||||||
|
union all
|
||||||
|
select dependency.objid, dependency.refobjid
|
||||||
|
from proposal_table
|
||||||
|
join pg_catalog.pg_attrdef default_row on default_row.adrelid = proposal_table.oid
|
||||||
|
join pg_catalog.pg_depend dependency
|
||||||
|
on dependency.classid = 'pg_catalog.pg_attrdef'::pg_catalog.regclass
|
||||||
|
and dependency.objid = default_row.oid
|
||||||
|
and dependency.refclassid = 'pg_catalog.pg_collation'::pg_catalog.regclass
|
||||||
|
join pg_catalog.pg_collation collation_row on collation_row.oid = dependency.refobjid
|
||||||
|
join pg_catalog.pg_namespace namespace on namespace.oid = collation_row.collnamespace
|
||||||
|
where namespace.nspname <> 'pg_catalog'
|
||||||
|
) unsafe_dependency
|
||||||
|
),
|
||||||
|
'owned_large_objects', (
|
||||||
|
select pg_catalog.count(*)::int
|
||||||
|
from pg_catalog.pg_largeobject_metadata metadata
|
||||||
|
where metadata.lomowner in ((select oid from runtime_role), (select oid from owner_role))
|
||||||
|
),
|
||||||
|
'large_object_acl_privileges', (
|
||||||
|
select pg_catalog.count(*)::int
|
||||||
|
from pg_catalog.pg_largeobject_metadata metadata
|
||||||
|
where exists (
|
||||||
|
select 1
|
||||||
|
from pg_catalog.aclexplode(
|
||||||
|
coalesce(metadata.lomacl, pg_catalog.acldefault('L', metadata.lomowner))
|
||||||
|
) acl
|
||||||
|
where acl.grantee in (0, (select oid from runtime_role), (select oid from owner_role))
|
||||||
|
and acl.privilege_type in ('SELECT', 'UPDATE')
|
||||||
|
)
|
||||||
|
),
|
||||||
|
'large_object_mutation_routine_execute', (
|
||||||
|
select pg_catalog.count(*)::int
|
||||||
|
from pg_catalog.pg_proc function_row
|
||||||
|
join pg_catalog.pg_namespace namespace on namespace.oid = function_row.pronamespace
|
||||||
|
cross join (values ({_sql_literal(RUNTIME_DATABASE_ROLE)}), ({_sql_literal(STAGE_OWNER_DATABASE_ROLE)})) scoped_role(name)
|
||||||
|
where namespace.nspname = 'pg_catalog'
|
||||||
|
and function_row.proname in (
|
||||||
|
'lo_creat', 'lo_create', 'lo_export', 'lo_from_bytea', 'lo_import',
|
||||||
|
'lo_open', 'lo_put', 'lo_truncate', 'lo_truncate64', 'lo_unlink', 'lowrite'
|
||||||
|
)
|
||||||
|
and pg_catalog.has_function_privilege(scoped_role.name, function_row.oid, 'EXECUTE')
|
||||||
|
),
|
||||||
|
'public_large_object_mutation_routine_execute', (
|
||||||
|
select pg_catalog.count(*)::int
|
||||||
|
from pg_catalog.pg_proc function_row
|
||||||
|
join pg_catalog.pg_namespace namespace on namespace.oid = function_row.pronamespace
|
||||||
|
cross join lateral pg_catalog.aclexplode(
|
||||||
|
coalesce(function_row.proacl, pg_catalog.acldefault('f', function_row.proowner))
|
||||||
|
) acl
|
||||||
|
where namespace.nspname = 'pg_catalog'
|
||||||
|
and function_row.proname in (
|
||||||
|
'lo_creat', 'lo_create', 'lo_export', 'lo_from_bytea', 'lo_import',
|
||||||
|
'lo_open', 'lo_put', 'lo_truncate', 'lo_truncate64', 'lo_unlink', 'lowrite'
|
||||||
|
)
|
||||||
|
and acl.grantee = 0
|
||||||
|
and acl.privilege_type = 'EXECUTE'
|
||||||
|
),
|
||||||
|
'parameter_privileges', (
|
||||||
|
select pg_catalog.count(*)::int
|
||||||
|
from pg_catalog.pg_parameter_acl parameter_acl
|
||||||
|
cross join (values ({_sql_literal(RUNTIME_DATABASE_ROLE)}), ({_sql_literal(STAGE_OWNER_DATABASE_ROLE)})) scoped_role(name)
|
||||||
|
where pg_catalog.has_parameter_privilege(scoped_role.name, parameter_acl.parname, 'SET')
|
||||||
|
or pg_catalog.has_parameter_privilege(scoped_role.name, parameter_acl.parname, 'ALTER SYSTEM')
|
||||||
|
),
|
||||||
|
'proposal_constraint_shape', coalesce((
|
||||||
|
select pg_catalog.count(*) = 6
|
||||||
|
and pg_catalog.count(*) filter (
|
||||||
|
where (constraint_row.conname, constraint_row.contype) in (
|
||||||
|
('kb_proposals_pkey', 'p'),
|
||||||
|
('kb_proposals_proposal_type_check', 'c'),
|
||||||
|
('kb_proposals_status_check', 'c'),
|
||||||
|
('kb_proposals_applied_by_agent_id_fkey', 'f'),
|
||||||
|
('kb_proposals_proposed_by_agent_id_fkey', 'f'),
|
||||||
|
('kb_proposals_reviewed_by_agent_id_fkey', 'f')
|
||||||
|
)
|
||||||
|
) = 6
|
||||||
|
from pg_catalog.pg_constraint constraint_row
|
||||||
|
join proposal_table on proposal_table.oid = constraint_row.conrelid
|
||||||
|
), false),
|
||||||
|
'unsafe_proposal_constraint_dependencies', (
|
||||||
|
select pg_catalog.count(*)::int
|
||||||
|
from pg_catalog.pg_constraint constraint_row
|
||||||
|
join proposal_table on proposal_table.oid = constraint_row.conrelid
|
||||||
|
join pg_catalog.pg_depend dependency
|
||||||
|
on dependency.classid = 'pg_catalog.pg_constraint'::pg_catalog.regclass
|
||||||
|
and dependency.objid = constraint_row.oid
|
||||||
|
and dependency.refclassid = 'pg_catalog.pg_proc'::pg_catalog.regclass
|
||||||
|
join pg_catalog.pg_proc function_row on function_row.oid = dependency.refobjid
|
||||||
|
join pg_catalog.pg_namespace namespace on namespace.oid = function_row.pronamespace
|
||||||
|
where namespace.nspname <> 'pg_catalog'
|
||||||
|
),
|
||||||
|
'unsafe_proposal_index_expressions', (
|
||||||
|
select pg_catalog.count(*)::int
|
||||||
|
from pg_catalog.pg_index index_row
|
||||||
|
join proposal_table on proposal_table.oid = index_row.indrelid
|
||||||
|
where index_row.indexprs is not null
|
||||||
|
or (
|
||||||
|
index_row.indpred is not null
|
||||||
|
and pg_catalog.pg_get_expr(index_row.indpred, index_row.indrelid)
|
||||||
|
<> '(proposed_by_handle IS NOT NULL)'
|
||||||
|
)
|
||||||
|
),
|
||||||
|
'other_scoped_backends', (
|
||||||
|
select pg_catalog.count(*)::int
|
||||||
|
from pg_catalog.pg_stat_activity activity
|
||||||
|
where activity.pid <> pg_catalog.pg_backend_pid()
|
||||||
|
and activity.usename in ({_sql_literal(RUNTIME_DATABASE_ROLE)}, {_sql_literal(STAGE_OWNER_DATABASE_ROLE)})
|
||||||
|
),
|
||||||
|
'scoped_prepared_xacts', (
|
||||||
|
select pg_catalog.count(*)::int
|
||||||
|
from pg_catalog.pg_prepared_xacts prepared
|
||||||
|
where prepared.owner in ({_sql_literal(RUNTIME_DATABASE_ROLE)}, {_sql_literal(STAGE_OWNER_DATABASE_ROLE)})
|
||||||
|
),
|
||||||
'table_dml_grants', (
|
'table_dml_grants', (
|
||||||
select pg_catalog.count(*)::int
|
select pg_catalog.count(*)::int
|
||||||
from pg_catalog.pg_class relation
|
from pg_catalog.pg_class relation
|
||||||
|
|
@ -1083,99 +1550,6 @@ select pg_catalog.jsonb_build_object(
|
||||||
""".strip()
|
""".strip()
|
||||||
|
|
||||||
|
|
||||||
def _legacy_catalog_privilege_posture_sql() -> str:
|
|
||||||
return f"""
|
|
||||||
with scoped_role(role_name, field_prefix) as (
|
|
||||||
values
|
|
||||||
({_sql_literal(RUNTIME_DATABASE_ROLE)}, 'runtime'),
|
|
||||||
({_sql_literal(STAGE_OWNER_DATABASE_ROLE)}, 'owner')
|
|
||||||
), restore_schema as (
|
|
||||||
select oid
|
|
||||||
from pg_catalog.pg_namespace
|
|
||||||
where nspname = 'teleo_restore'
|
|
||||||
), posture as (
|
|
||||||
select scoped_role.field_prefix,
|
|
||||||
(
|
|
||||||
select pg_catalog.count(*)::int
|
|
||||||
from restore_schema
|
|
||||||
cross join (values ('USAGE'), ('CREATE')) privilege(name)
|
|
||||||
where pg_catalog.has_schema_privilege(
|
|
||||||
scoped_role.role_name,
|
|
||||||
restore_schema.oid,
|
|
||||||
privilege.name
|
|
||||||
)
|
|
||||||
) as schema_privileges,
|
|
||||||
(
|
|
||||||
select pg_catalog.count(*)::int
|
|
||||||
from pg_catalog.pg_class relation
|
|
||||||
join restore_schema on restore_schema.oid = relation.relnamespace
|
|
||||||
cross join (
|
|
||||||
values ('SELECT'), ('INSERT'), ('UPDATE'), ('DELETE'),
|
|
||||||
('TRUNCATE'), ('REFERENCES'), ('TRIGGER')
|
|
||||||
) privilege(name)
|
|
||||||
where relation.relkind in ('r', 'p', 'v', 'm', 'f')
|
|
||||||
and pg_catalog.has_table_privilege(
|
|
||||||
scoped_role.role_name,
|
|
||||||
relation.oid,
|
|
||||||
privilege.name
|
|
||||||
)
|
|
||||||
) as table_privileges,
|
|
||||||
(
|
|
||||||
select pg_catalog.count(*)::int
|
|
||||||
from pg_catalog.pg_class relation
|
|
||||||
join restore_schema on restore_schema.oid = relation.relnamespace
|
|
||||||
join pg_catalog.pg_attribute attribute on attribute.attrelid = relation.oid
|
|
||||||
cross join (values ('SELECT'), ('INSERT'), ('UPDATE'), ('REFERENCES')) privilege(name)
|
|
||||||
where relation.relkind in ('r', 'p', 'v', 'm', 'f')
|
|
||||||
and attribute.attnum > 0
|
|
||||||
and not attribute.attisdropped
|
|
||||||
and pg_catalog.has_column_privilege(
|
|
||||||
scoped_role.role_name,
|
|
||||||
relation.oid,
|
|
||||||
attribute.attnum,
|
|
||||||
privilege.name
|
|
||||||
)
|
|
||||||
) as column_privileges,
|
|
||||||
(
|
|
||||||
select pg_catalog.count(*)::int
|
|
||||||
from pg_catalog.pg_class relation
|
|
||||||
join restore_schema on restore_schema.oid = relation.relnamespace
|
|
||||||
cross join (values ('USAGE'), ('SELECT'), ('UPDATE')) privilege(name)
|
|
||||||
where relation.relkind = 'S'
|
|
||||||
and pg_catalog.has_sequence_privilege(
|
|
||||||
scoped_role.role_name,
|
|
||||||
relation.oid,
|
|
||||||
privilege.name
|
|
||||||
)
|
|
||||||
) as sequence_privileges,
|
|
||||||
(
|
|
||||||
select pg_catalog.count(*)::int
|
|
||||||
from pg_catalog.pg_proc function_row
|
|
||||||
join restore_schema on restore_schema.oid = function_row.pronamespace
|
|
||||||
where pg_catalog.has_function_privilege(
|
|
||||||
scoped_role.role_name,
|
|
||||||
function_row.oid,
|
|
||||||
'EXECUTE'
|
|
||||||
)
|
|
||||||
) as routine_privileges
|
|
||||||
from scoped_role
|
|
||||||
)
|
|
||||||
select pg_catalog.jsonb_build_object(
|
|
||||||
'runtime_schema_privileges', max(schema_privileges) filter (where field_prefix = 'runtime'),
|
|
||||||
'runtime_table_privileges', max(table_privileges) filter (where field_prefix = 'runtime'),
|
|
||||||
'runtime_column_privileges', max(column_privileges) filter (where field_prefix = 'runtime'),
|
|
||||||
'runtime_sequence_privileges', max(sequence_privileges) filter (where field_prefix = 'runtime'),
|
|
||||||
'runtime_routine_privileges', max(routine_privileges) filter (where field_prefix = 'runtime'),
|
|
||||||
'owner_schema_privileges', max(schema_privileges) filter (where field_prefix = 'owner'),
|
|
||||||
'owner_table_privileges', max(table_privileges) filter (where field_prefix = 'owner'),
|
|
||||||
'owner_column_privileges', max(column_privileges) filter (where field_prefix = 'owner'),
|
|
||||||
'owner_sequence_privileges', max(sequence_privileges) filter (where field_prefix = 'owner'),
|
|
||||||
'owner_routine_privileges', max(routine_privileges) filter (where field_prefix = 'owner')
|
|
||||||
)::text
|
|
||||||
from posture;
|
|
||||||
""".strip()
|
|
||||||
|
|
||||||
|
|
||||||
def _canary_count_sql(source_ref: str) -> str:
|
def _canary_count_sql(source_ref: str) -> str:
|
||||||
return f"select pg_catalog.count(*)::text from kb_stage.kb_proposals where source_ref = {_sql_literal(source_ref)};"
|
return f"select pg_catalog.count(*)::text from kb_stage.kb_proposals where source_ref = {_sql_literal(source_ref)};"
|
||||||
|
|
||||||
|
|
@ -1202,6 +1576,7 @@ select pg_catalog.jsonb_build_object(
|
||||||
'proposal', proposal
|
'proposal', proposal
|
||||||
)::text
|
)::text
|
||||||
from staged;
|
from staged;
|
||||||
|
set constraints all immediate;
|
||||||
rollback;
|
rollback;
|
||||||
""".strip()
|
""".strip()
|
||||||
|
|
||||||
|
|
@ -1249,6 +1624,24 @@ def _negative_checks(run_id: str, source_ref: str) -> tuple[NegativeCheck, ...]:
|
||||||
_transactional("delete from public.claims where false"),
|
_transactional("delete from public.claims where false"),
|
||||||
"42501",
|
"42501",
|
||||||
),
|
),
|
||||||
|
NegativeCheck(
|
||||||
|
"large_object_creat",
|
||||||
|
CANONICAL_DATABASE,
|
||||||
|
_transactional("select pg_catalog.lo_creat(0)"),
|
||||||
|
"42501",
|
||||||
|
),
|
||||||
|
NegativeCheck(
|
||||||
|
"large_object_create",
|
||||||
|
CANONICAL_DATABASE,
|
||||||
|
_transactional("select pg_catalog.lo_create(0)"),
|
||||||
|
"42501",
|
||||||
|
),
|
||||||
|
NegativeCheck(
|
||||||
|
"large_object_from_bytea",
|
||||||
|
CANONICAL_DATABASE,
|
||||||
|
_transactional("select pg_catalog.lo_from_bytea(0, ''::bytea)"),
|
||||||
|
"42501",
|
||||||
|
),
|
||||||
NegativeCheck(
|
NegativeCheck(
|
||||||
"forged_proposer_overload",
|
"forged_proposer_overload",
|
||||||
CANONICAL_DATABASE,
|
CANONICAL_DATABASE,
|
||||||
|
|
@ -1313,10 +1706,18 @@ def _negative_checks(run_id: str, source_ref: str) -> tuple[NegativeCheck, ...]:
|
||||||
"42501",
|
"42501",
|
||||||
),
|
),
|
||||||
NegativeCheck(
|
NegativeCheck(
|
||||||
"legacy_teleo_restore_read",
|
"legacy_database_connect",
|
||||||
LEGACY_DATABASE,
|
LEGACY_DATABASE,
|
||||||
_transactional("select 1 from teleo_restore.response_audit limit 1"),
|
"select 1 /* legacy_database_connect */;",
|
||||||
"42501",
|
"42501",
|
||||||
|
expected_connection_denial=True,
|
||||||
|
),
|
||||||
|
NegativeCheck(
|
||||||
|
"template_database_connect",
|
||||||
|
TEMPLATE_DATABASE,
|
||||||
|
"select 1 /* template_database_connect */;",
|
||||||
|
"42501",
|
||||||
|
expected_connection_denial=True,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -1341,6 +1742,24 @@ def _expect_sqlstate(
|
||||||
observed_sqlstate="success",
|
observed_sqlstate="success",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if check.expected_connection_denial:
|
||||||
|
normalized = result.stderr.casefold()
|
||||||
|
expected_database = f'permission denied for database "{check.database}"'.casefold()
|
||||||
|
if expected_database not in normalized or "does not have connect privilege" not in normalized:
|
||||||
|
raise VerificationError(
|
||||||
|
"negative_connection_denial_mismatch",
|
||||||
|
check.name,
|
||||||
|
expected_sqlstate=check.expected_sqlstate,
|
||||||
|
observed_sqlstate="startup_error_unclassified",
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"check": check.name,
|
||||||
|
"expected_sqlstate": check.expected_sqlstate,
|
||||||
|
"observed_error_class": "connect_privilege_denied",
|
||||||
|
"observed_sqlstate": "not_exposed_by_libpq_startup",
|
||||||
|
"result": "denied",
|
||||||
|
}
|
||||||
|
|
||||||
observed = SQLSTATE_RE.findall(result.stderr)
|
observed = SQLSTATE_RE.findall(result.stderr)
|
||||||
unique_observed = sorted(set(observed))
|
unique_observed = sorted(set(observed))
|
||||||
if not observed:
|
if not observed:
|
||||||
|
|
@ -1373,6 +1792,7 @@ def _assert_identity(identity: dict[str, Any]) -> dict[str, Any]:
|
||||||
"server_addr": PRIVATE_CLOUDSQL_HOST,
|
"server_addr": PRIVATE_CLOUDSQL_HOST,
|
||||||
"server_port": PRIVATE_CLOUDSQL_PORT,
|
"server_port": PRIVATE_CLOUDSQL_PORT,
|
||||||
"ssl": True,
|
"ssl": True,
|
||||||
|
"system_identifier": EXPECTED_SYSTEM_IDENTIFIER,
|
||||||
}
|
}
|
||||||
if any(identity.get(key) != value for key, value in expected.items()):
|
if any(identity.get(key) != value for key, value in expected.items()):
|
||||||
raise VerificationError("private_ssl_identity_mismatch", "database_identity")
|
raise VerificationError("private_ssl_identity_mismatch", "database_identity")
|
||||||
|
|
@ -1510,16 +1930,6 @@ def _assert_catalog_privilege_posture(posture: dict[str, Any]) -> dict[str, int
|
||||||
return sanitized
|
return sanitized
|
||||||
|
|
||||||
|
|
||||||
def _assert_legacy_catalog_privilege_posture(posture: dict[str, Any]) -> dict[str, int]:
|
|
||||||
sanitized: dict[str, int] = {}
|
|
||||||
for field in LEGACY_CATALOG_ZERO_COUNT_FIELDS:
|
|
||||||
value = posture.get(field)
|
|
||||||
if isinstance(value, bool) or not isinstance(value, int) or value != 0:
|
|
||||||
raise VerificationError("legacy_catalog_privilege_posture_mismatch", "legacy_catalog_privileges")
|
|
||||||
sanitized[field] = 0
|
|
||||||
return sanitized
|
|
||||||
|
|
||||||
|
|
||||||
def _assert_allowed_reads(readback: dict[str, Any]) -> dict[str, int]:
|
def _assert_allowed_reads(readback: dict[str, Any]) -> dict[str, int]:
|
||||||
sanitized: dict[str, int] = {}
|
sanitized: dict[str, int] = {}
|
||||||
for key in ("claims_rows_sampled", "proposal_rows_sampled"):
|
for key in ("claims_rows_sampled", "proposal_rows_sampled"):
|
||||||
|
|
@ -1553,6 +1963,7 @@ def verify_runtime_permissions(
|
||||||
runner: Runner = subprocess_runner,
|
runner: Runner = subprocess_runner,
|
||||||
base_env: Mapping[str, str] | None = None,
|
base_env: Mapping[str, str] | None = None,
|
||||||
effective_user: str | None = None,
|
effective_user: str | None = None,
|
||||||
|
dependency_validator: DependencyValidator = validate_runtime_dependencies,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
try:
|
try:
|
||||||
run_id = validate_run_id(run_id)
|
run_id = validate_run_id(run_id)
|
||||||
|
|
@ -1566,14 +1977,13 @@ def verify_runtime_permissions(
|
||||||
raise VerificationError("unix_user_unknown", "runtime_user") from None
|
raise VerificationError("unix_user_unknown", "runtime_user") from None
|
||||||
if effective_user != RUNTIME_UNIX_USER:
|
if effective_user != RUNTIME_UNIX_USER:
|
||||||
raise VerificationError("unexpected_unix_user", "runtime_user")
|
raise VerificationError("unexpected_unix_user", "runtime_user")
|
||||||
|
dependency_validator()
|
||||||
|
|
||||||
source = os.environ if base_env is None else base_env
|
source = os.environ if base_env is None else base_env
|
||||||
source_ref = f"leo-runtime-permission:{run_id}"
|
source_ref = f"leo-runtime-permission:{run_id}"
|
||||||
generated_at = datetime.now(timezone.utc).isoformat(timespec="seconds").replace("+00:00", "Z")
|
generated_at = datetime.now(timezone.utc).isoformat(timespec="seconds").replace("+00:00", "Z")
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory(prefix="leoclean-runtime-permission-gcloud-") as config_name:
|
with nullcontext(selected_cloudsdk_config_path()) as config_dir:
|
||||||
config_dir = Path(config_name)
|
|
||||||
config_dir.chmod(0o700)
|
|
||||||
gcloud_env = _gcloud_environment(source, config_dir)
|
gcloud_env = _gcloud_environment(source, config_dir)
|
||||||
password = _read_scoped_password(env=gcloud_env, runner=runner)
|
password = _read_scoped_password(env=gcloud_env, runner=runner)
|
||||||
psql_env = _psql_environment(source, password)
|
psql_env = _psql_environment(source, password)
|
||||||
|
|
@ -1650,18 +2060,6 @@ def verify_runtime_permissions(
|
||||||
"catalog_privilege_posture",
|
"catalog_privilege_posture",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
legacy_catalog_privileges = _assert_legacy_catalog_privilege_posture(
|
|
||||||
_parse_json_object(
|
|
||||||
_run_psql_success(
|
|
||||||
check="legacy_catalog_privileges",
|
|
||||||
database=LEGACY_DATABASE,
|
|
||||||
sql=_legacy_catalog_privilege_posture_sql(),
|
|
||||||
env=psql_env,
|
|
||||||
runner=runner,
|
|
||||||
),
|
|
||||||
"legacy_catalog_privileges",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
allowed_reads = _assert_allowed_reads(
|
allowed_reads = _assert_allowed_reads(
|
||||||
_parse_json_object(
|
_parse_json_object(
|
||||||
_run_psql_success(
|
_run_psql_success(
|
||||||
|
|
@ -1721,7 +2119,6 @@ def verify_runtime_permissions(
|
||||||
"canary_rows_after": rows_after,
|
"canary_rows_after": rows_after,
|
||||||
"canary_rows_before": rows_before,
|
"canary_rows_before": rows_before,
|
||||||
"function_privileges": function_privileges,
|
"function_privileges": function_privileges,
|
||||||
"legacy_catalog_privileges": legacy_catalog_privileges,
|
|
||||||
"negative_permissions": negative_permissions,
|
"negative_permissions": negative_permissions,
|
||||||
"role_posture": role_posture,
|
"role_posture": role_posture,
|
||||||
"stage_function_definition": stage_function_definition,
|
"stage_function_definition": stage_function_definition,
|
||||||
|
|
@ -1768,7 +2165,7 @@ def verify_runtime_permissions(
|
||||||
"port": PRIVATE_CLOUDSQL_PORT,
|
"port": PRIVATE_CLOUDSQL_PORT,
|
||||||
"project": PROJECT_ID,
|
"project": PROJECT_ID,
|
||||||
"role": RUNTIME_DATABASE_ROLE,
|
"role": RUNTIME_DATABASE_ROLE,
|
||||||
"sslmode": "require",
|
"sslmode": "verify-ca",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/python3 -I
|
||||||
"""Verify the effective Leo service environment without exposing its values."""
|
"""Verify the effective Leo service environment without exposing its values."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
@ -18,6 +18,9 @@ RUNTIME_DATABASE_ROLE = "leoclean_kb_runtime"
|
||||||
SCOPED_PASSWORD_SECRET = "gcp-teleo-pgvector-standby-leoclean-kb-runtime-password"
|
SCOPED_PASSWORD_SECRET = "gcp-teleo-pgvector-standby-leoclean-kb-runtime-password"
|
||||||
ADMINISTRATOR_PASSWORD_SECRET = "gcp-teleo-pgvector-standby-postgres-password"
|
ADMINISTRATOR_PASSWORD_SECRET = "gcp-teleo-pgvector-standby-postgres-password"
|
||||||
RUNTIME_PATH = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
RUNTIME_PATH = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
||||||
|
RUNTIME_CLOUDSDK_CONFIG = "/usr/local/libexec/livingip/leoclean-kb/gcloud-config"
|
||||||
|
RUNTIME_SSL_MODE = "verify-ca"
|
||||||
|
RUNTIME_SSL_ROOT_CERT = "/usr/local/libexec/livingip/leoclean-kb/cloudsql-server-ca.pem"
|
||||||
|
|
||||||
FORBIDDEN_EXACT_FIELDS = frozenset(
|
FORBIDDEN_EXACT_FIELDS = frozenset(
|
||||||
{
|
{
|
||||||
|
|
@ -26,6 +29,7 @@ FORBIDDEN_EXACT_FIELDS = frozenset(
|
||||||
"BASHOPTS",
|
"BASHOPTS",
|
||||||
"CDPATH",
|
"CDPATH",
|
||||||
"CURL_CA_BUNDLE",
|
"CURL_CA_BUNDLE",
|
||||||
|
"CREDENTIALS_DIRECTORY",
|
||||||
"ENV",
|
"ENV",
|
||||||
"GLOBIGNORE",
|
"GLOBIGNORE",
|
||||||
"GOOGLE_APPLICATION_CREDENTIALS",
|
"GOOGLE_APPLICATION_CREDENTIALS",
|
||||||
|
|
@ -33,12 +37,22 @@ FORBIDDEN_EXACT_FIELDS = frozenset(
|
||||||
"HTTP_PROXY",
|
"HTTP_PROXY",
|
||||||
"IFS",
|
"IFS",
|
||||||
"NO_PROXY",
|
"NO_PROXY",
|
||||||
|
"OPENSSL_CONF",
|
||||||
|
"OPENSSL_ENGINES",
|
||||||
|
"OPENSSL_MODULES",
|
||||||
"PS4",
|
"PS4",
|
||||||
"REQUESTS_CA_BUNDLE",
|
"REQUESTS_CA_BUNDLE",
|
||||||
"SHELLOPTS",
|
"SHELLOPTS",
|
||||||
"SSL_CERT_DIR",
|
"SSL_CERT_DIR",
|
||||||
"SSL_CERT_FILE",
|
"SSL_CERT_FILE",
|
||||||
"SSLKEYLOGFILE",
|
"SSLKEYLOGFILE",
|
||||||
|
"GCONV_PATH",
|
||||||
|
"TELEO_CLOUDSQL_CREDENTIAL_MODE",
|
||||||
|
"TELEO_KB_CLAIM_BASE_URL",
|
||||||
|
"TELEO_KB_READ_ATTEMPTS",
|
||||||
|
"TELEO_MEMORY_INCLUDE_EXCERPTS",
|
||||||
|
"TELEO_MEMORY_REDACT",
|
||||||
|
"XDG_CONFIG_HOME",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -83,7 +97,12 @@ def expected_environment() -> dict[str, str]:
|
||||||
"""Build the exact reviewed environment contract."""
|
"""Build the exact reviewed environment contract."""
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
"CLOUDSDK_CONFIG": RUNTIME_CLOUDSDK_CONFIG,
|
||||||
"PATH": RUNTIME_PATH,
|
"PATH": RUNTIME_PATH,
|
||||||
|
"PGSSLMODE": RUNTIME_SSL_MODE,
|
||||||
|
"PGSSLROOTCERT": RUNTIME_SSL_ROOT_CERT,
|
||||||
|
"PYTHONNOUSERSITE": "1",
|
||||||
|
"PYTHONSAFEPATH": "1",
|
||||||
"TELEO_CANONICAL_CLOUDSQL_DB": CANONICAL_DATABASE,
|
"TELEO_CANONICAL_CLOUDSQL_DB": CANONICAL_DATABASE,
|
||||||
"TELEO_CLOUDSQL_DB": CANONICAL_DATABASE,
|
"TELEO_CLOUDSQL_DB": CANONICAL_DATABASE,
|
||||||
"TELEO_CLOUDSQL_ENABLE_AUDIT_FALLBACK": "0",
|
"TELEO_CLOUDSQL_ENABLE_AUDIT_FALLBACK": "0",
|
||||||
|
|
@ -127,22 +146,31 @@ def _forbidden_field_labels(environment: Mapping[str, str]) -> tuple[str, ...]:
|
||||||
labels: set[str] = set()
|
labels: set[str] = set()
|
||||||
for key in environment:
|
for key in environment:
|
||||||
normalized = key.upper()
|
normalized = key.upper()
|
||||||
if normalized.startswith("PG"):
|
if normalized.startswith("PG") and normalized not in {"PGSSLMODE", "PGSSLROOTCERT"}:
|
||||||
labels.add("PG*")
|
labels.add("PG*")
|
||||||
if normalized.startswith("CLOUDSDK_"):
|
if normalized.startswith("CLOUDSDK_") and normalized != "CLOUDSDK_CONFIG":
|
||||||
labels.add("CLOUDSDK_*")
|
labels.add("CLOUDSDK_*")
|
||||||
if normalized.startswith("LD_"):
|
if normalized.startswith("LD_"):
|
||||||
labels.add("LD_*")
|
labels.add("LD_*")
|
||||||
if normalized.startswith("BASH_FUNC_"):
|
if normalized.startswith("BASH_FUNC_"):
|
||||||
labels.add("BASH_FUNC_*")
|
labels.add("BASH_FUNC_*")
|
||||||
if normalized.startswith("PYTHON") and normalized != "PYTHONUNBUFFERED":
|
if normalized.startswith("PYTHON") and normalized not in {
|
||||||
|
"PYTHONNOUSERSITE",
|
||||||
|
"PYTHONSAFEPATH",
|
||||||
|
"PYTHONUNBUFFERED",
|
||||||
|
}:
|
||||||
labels.add("PYTHON*")
|
labels.add("PYTHON*")
|
||||||
if normalized in FORBIDDEN_EXACT_FIELDS:
|
if normalized in FORBIDDEN_EXACT_FIELDS:
|
||||||
labels.add(normalized)
|
labels.add(normalized)
|
||||||
return tuple(sorted(labels))
|
return tuple(sorted(labels))
|
||||||
|
|
||||||
|
|
||||||
def validate_environment(environment: Mapping[str, str]) -> tuple[str, ...]:
|
def validate_environment(
|
||||||
|
environment: Mapping[str, str],
|
||||||
|
*,
|
||||||
|
allow_missing_cloudsdk_config: bool = False,
|
||||||
|
allow_missing_runtime_security_fields: bool = False,
|
||||||
|
) -> tuple[str, ...]:
|
||||||
"""Validate the effective environment and return only reviewed field names."""
|
"""Validate the effective environment and return only reviewed field names."""
|
||||||
|
|
||||||
if not isinstance(environment, Mapping) or any(
|
if not isinstance(environment, Mapping) or any(
|
||||||
|
|
@ -151,6 +179,18 @@ def validate_environment(environment: Mapping[str, str]) -> tuple[str, ...]:
|
||||||
raise VerificationError("environment_malformed")
|
raise VerificationError("environment_malformed")
|
||||||
|
|
||||||
expected = expected_environment()
|
expected = expected_environment()
|
||||||
|
if allow_missing_runtime_security_fields:
|
||||||
|
for field in (
|
||||||
|
"CLOUDSDK_CONFIG",
|
||||||
|
"PGSSLMODE",
|
||||||
|
"PGSSLROOTCERT",
|
||||||
|
"PYTHONNOUSERSITE",
|
||||||
|
"PYTHONSAFEPATH",
|
||||||
|
):
|
||||||
|
if field not in environment:
|
||||||
|
expected.pop(field)
|
||||||
|
if allow_missing_cloudsdk_config and "CLOUDSDK_CONFIG" not in environment:
|
||||||
|
expected.pop("CLOUDSDK_CONFIG")
|
||||||
administrator_secret = ADMINISTRATOR_PASSWORD_SECRET.casefold()
|
administrator_secret = ADMINISTRATOR_PASSWORD_SECRET.casefold()
|
||||||
if any(administrator_secret in value.casefold() for value in environment.values()):
|
if any(administrator_secret in value.casefold() for value in environment.values()):
|
||||||
raise VerificationError("administrator_secret_reference")
|
raise VerificationError("administrator_secret_reference")
|
||||||
|
|
@ -172,12 +212,18 @@ def verify_process_environment(
|
||||||
pid: int,
|
pid: int,
|
||||||
*,
|
*,
|
||||||
proc_root: Path = Path("/proc"),
|
proc_root: Path = Path("/proc"),
|
||||||
|
allow_missing_cloudsdk_config: bool = False,
|
||||||
|
allow_missing_runtime_security_fields: bool = False,
|
||||||
) -> dict[str, object]:
|
) -> dict[str, object]:
|
||||||
"""Read and verify one process environment, returning a sanitized receipt."""
|
"""Read and verify one process environment, returning a sanitized receipt."""
|
||||||
|
|
||||||
validated_pid = validate_pid(pid)
|
validated_pid = validate_pid(pid)
|
||||||
raw = (proc_root / str(validated_pid) / "environ").read_bytes()
|
raw = (proc_root / str(validated_pid) / "environ").read_bytes()
|
||||||
validated_fields = validate_environment(parse_environment(raw))
|
validated_fields = validate_environment(
|
||||||
|
parse_environment(raw),
|
||||||
|
allow_missing_cloudsdk_config=allow_missing_cloudsdk_config,
|
||||||
|
allow_missing_runtime_security_fields=allow_missing_runtime_security_fields,
|
||||||
|
)
|
||||||
return {
|
return {
|
||||||
"runtime_environment": "scoped",
|
"runtime_environment": "scoped",
|
||||||
"status": "pass",
|
"status": "pass",
|
||||||
|
|
@ -196,13 +242,19 @@ def canonical_json(payload: Mapping[str, Any]) -> str:
|
||||||
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
||||||
parser = _SanitizedArgumentParser(description=__doc__, add_help=False)
|
parser = _SanitizedArgumentParser(description=__doc__, add_help=False)
|
||||||
parser.add_argument("--pid", required=True)
|
parser.add_argument("--pid", required=True)
|
||||||
|
parser.add_argument("--allow-missing-cloudsdk-config", action="store_true")
|
||||||
|
parser.add_argument("--allow-missing-runtime-security-fields", action="store_true")
|
||||||
return parser.parse_args(argv)
|
return parser.parse_args(argv)
|
||||||
|
|
||||||
|
|
||||||
def main(argv: list[str] | None = None) -> int:
|
def main(argv: list[str] | None = None) -> int:
|
||||||
try:
|
try:
|
||||||
args = parse_args(argv)
|
args = parse_args(argv)
|
||||||
receipt = verify_process_environment(validate_pid(args.pid))
|
receipt = verify_process_environment(
|
||||||
|
validate_pid(args.pid),
|
||||||
|
allow_missing_cloudsdk_config=args.allow_missing_cloudsdk_config,
|
||||||
|
allow_missing_runtime_security_fields=args.allow_missing_runtime_security_fields,
|
||||||
|
)
|
||||||
returncode = 0
|
returncode = 0
|
||||||
except VerificationError as exc:
|
except VerificationError as exc:
|
||||||
receipt = failure_receipt(exc)
|
receipt = failure_receipt(exc)
|
||||||
|
|
|
||||||
|
|
@ -46,8 +46,10 @@ DEFAULT_SERVICE = "leoclean-gcp-prod-parallel.service"
|
||||||
DEFAULT_HOST = "10.61.0.3"
|
DEFAULT_HOST = "10.61.0.3"
|
||||||
DEFAULT_PROJECT = "teleo-501523"
|
DEFAULT_PROJECT = "teleo-501523"
|
||||||
DEFAULT_SECRET = "gcp-teleo-pgvector-standby-postgres-password"
|
DEFAULT_SECRET = "gcp-teleo-pgvector-standby-postgres-password"
|
||||||
|
DEFAULT_SSL_ROOT_CERT = Path("/usr/local/libexec/livingip/leoclean-kb/cloudsql-server-ca.pem")
|
||||||
|
DEFAULT_SYSTEM_IDENTIFIER = "7659718422914359312"
|
||||||
DEFAULT_MANIFEST_SQL = HERE.parent / "ops" / "postgres_parity_manifest.sql"
|
DEFAULT_MANIFEST_SQL = HERE.parent / "ops" / "postgres_parity_manifest.sql"
|
||||||
REVIEWED_CLOUDSQL_TOOL_SHA256 = "66fa84b3bd6fe67e102fbce4509ea28c3537ba5bb1091e1a1a457b7425366064"
|
REVIEWED_CLOUDSQL_TOOL_SHA256 = "17d5213756ea6492e4232784a04b7f584afee5dce5184b33a2227e58847472c1"
|
||||||
REVIEWED_MANIFEST_SQL_SHA256 = "8b8cdc25d54fdd8de05eb38c6e4423d2836953eb6012d4545f5c9c71b5f0150a"
|
REVIEWED_MANIFEST_SQL_SHA256 = "8b8cdc25d54fdd8de05eb38c6e4423d2836953eb6012d4545f5c9c71b5f0150a"
|
||||||
COUNT_READBACK_RE = re.compile(
|
COUNT_READBACK_RE = re.compile(
|
||||||
r"DB readback:\s*claims:\s*`?(?P<claims>\d+)`?;\s*"
|
r"DB readback:\s*claims:\s*`?(?P<claims>\d+)`?;\s*"
|
||||||
|
|
@ -192,10 +194,13 @@ def run_target_psql(
|
||||||
**os.environ,
|
**os.environ,
|
||||||
"PGPASSWORD": password,
|
"PGPASSWORD": password,
|
||||||
"PGOPTIONS": "-c default_transaction_read_only=on",
|
"PGOPTIONS": "-c default_transaction_read_only=on",
|
||||||
|
"PGSSLMODE": "verify-ca",
|
||||||
|
"PGSSLROOTCERT": str(args.ssl_root_cert),
|
||||||
}
|
}
|
||||||
command = [
|
command = [
|
||||||
"psql",
|
"psql",
|
||||||
f"host={args.host} port=5432 dbname={args.target_db} user=postgres sslmode=require connect_timeout=8",
|
f"host={args.host} port=5432 dbname={args.target_db} user=postgres "
|
||||||
|
f"sslmode=verify-ca sslrootcert={args.ssl_root_cert} connect_timeout=8",
|
||||||
"-X",
|
"-X",
|
||||||
"-Atq",
|
"-Atq",
|
||||||
"-v",
|
"-v",
|
||||||
|
|
@ -232,7 +237,11 @@ rollback;
|
||||||
return json.loads(rows[-1])
|
return json.loads(rows[-1])
|
||||||
|
|
||||||
|
|
||||||
def validate_database_identity(identity: dict[str, Any], target_db: str) -> None:
|
def validate_database_identity(
|
||||||
|
identity: dict[str, Any],
|
||||||
|
target_db: str,
|
||||||
|
expected_system_identifier: str = DEFAULT_SYSTEM_IDENTIFIER,
|
||||||
|
) -> None:
|
||||||
if identity.get("current_database") != target_db:
|
if identity.get("current_database") != target_db:
|
||||||
raise bound.CheckpointError("Cloud SQL identity does not match the generated target database")
|
raise bound.CheckpointError("Cloud SQL identity does not match the generated target database")
|
||||||
if identity.get("ssl") is not True:
|
if identity.get("ssl") is not True:
|
||||||
|
|
@ -252,8 +261,8 @@ def validate_database_identity(identity: dict[str, Any], target_db: str) -> None
|
||||||
)
|
)
|
||||||
if not any(address in network for network in private_networks):
|
if not any(address in network for network in private_networks):
|
||||||
raise bound.CheckpointError("Cloud SQL target address is not RFC1918-private")
|
raise bound.CheckpointError("Cloud SQL target address is not RFC1918-private")
|
||||||
if not identity.get("system_identifier"):
|
if identity.get("system_identifier") != expected_system_identifier:
|
||||||
raise bound.CheckpointError("Cloud SQL target system identifier is missing")
|
raise bound.CheckpointError("Cloud SQL target system identifier is not the reviewed instance")
|
||||||
|
|
||||||
|
|
||||||
def canonical_status(args: argparse.Namespace) -> dict[str, Any]:
|
def canonical_status(args: argparse.Namespace) -> dict[str, Any]:
|
||||||
|
|
@ -347,6 +356,7 @@ def build_cloudsql_wrapper(
|
||||||
host: str,
|
host: str,
|
||||||
project: str,
|
project: str,
|
||||||
password_secret: str,
|
password_secret: str,
|
||||||
|
ssl_root_cert: Path,
|
||||||
run_nonce: str,
|
run_nonce: str,
|
||||||
system_exec_path: str = bound.SYSTEM_EXEC_PATH,
|
system_exec_path: str = bound.SYSTEM_EXEC_PATH,
|
||||||
) -> str:
|
) -> str:
|
||||||
|
|
@ -361,18 +371,21 @@ TARGET_HOST={shlex.quote(host)}
|
||||||
SOURCE_COMPUTE={shlex.quote(SOURCE_COMPUTE)}
|
SOURCE_COMPUTE={shlex.quote(SOURCE_COMPUTE)}
|
||||||
PROJECT={shlex.quote(project)}
|
PROJECT={shlex.quote(project)}
|
||||||
PASSWORD_SECRET={shlex.quote(password_secret)}
|
PASSWORD_SECRET={shlex.quote(password_secret)}
|
||||||
|
SSL_ROOT_CERT={shlex.quote(str(ssl_root_cert))}
|
||||||
RUN_NONCE={shlex.quote(run_nonce)}
|
RUN_NONCE={shlex.quote(run_nonce)}
|
||||||
PYTHON={shlex.quote(python)}
|
PYTHON={shlex.quote(python)}
|
||||||
export PATH={shlex.quote(system_exec_path)}
|
export PATH={shlex.quote(system_exec_path)}
|
||||||
export CLOUDSDK_CONFIG=/home/teleo/.config/gcloud
|
export CLOUDSDK_CONFIG=/home/teleo/.config/gcloud
|
||||||
export PGOPTIONS="-c default_transaction_read_only=on"
|
export PGOPTIONS="-c default_transaction_read_only=on"
|
||||||
|
export PGSSLMODE=verify-ca
|
||||||
|
export PGSSLROOTCERT="$SSL_ROOT_CERT"
|
||||||
INVOCATION_ID="$($PYTHON -c 'import uuid; print(uuid.uuid4())')"
|
INVOCATION_ID="$($PYTHON -c 'import uuid; print(uuid.uuid4())')"
|
||||||
if ! PGPASSWORD="$(gcloud secrets versions access latest --secret="$PASSWORD_SECRET" --project="$PROJECT")"; then
|
if ! PGPASSWORD="$(gcloud secrets versions access latest --secret="$PASSWORD_SECRET" --project="$PROJECT")"; then
|
||||||
exit 124
|
exit 124
|
||||||
fi
|
fi
|
||||||
export PGPASSWORD
|
export PGPASSWORD
|
||||||
trap 'unset PGPASSWORD PGOPTIONS' EXIT
|
trap 'unset PGPASSWORD PGOPTIONS' EXIT
|
||||||
DB_RECEIPT="$(psql "host=$TARGET_HOST port=5432 dbname=$TARGET_DATABASE user=postgres sslmode=require connect_timeout=8" -X -Atq -v ON_ERROR_STOP=1 <<'SQL'
|
DB_RECEIPT="$(psql "host=$TARGET_HOST port=5432 dbname=$TARGET_DATABASE user=postgres sslmode=verify-ca sslrootcert=$SSL_ROOT_CERT connect_timeout=8" -X -Atq -v ON_ERROR_STOP=1 <<'SQL'
|
||||||
begin transaction read only;
|
begin transaction read only;
|
||||||
select jsonb_build_object(
|
select jsonb_build_object(
|
||||||
'current_database', current_database(),
|
'current_database', current_database(),
|
||||||
|
|
@ -468,6 +481,7 @@ def patch_temp_bridge(args: argparse.Namespace, temp_profile: Path) -> dict[str,
|
||||||
project=args.project,
|
project=args.project,
|
||||||
password_secret=args.password_secret,
|
password_secret=args.password_secret,
|
||||||
run_nonce=run_nonce,
|
run_nonce=run_nonce,
|
||||||
|
ssl_root_cert=args.ssl_root_cert,
|
||||||
)
|
)
|
||||||
binding = f"""
|
binding = f"""
|
||||||
## Generated GCP Database Checkpoint
|
## Generated GCP Database Checkpoint
|
||||||
|
|
@ -788,6 +802,7 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
||||||
parser.add_argument("--host", default=DEFAULT_HOST)
|
parser.add_argument("--host", default=DEFAULT_HOST)
|
||||||
parser.add_argument("--project", default=DEFAULT_PROJECT)
|
parser.add_argument("--project", default=DEFAULT_PROJECT)
|
||||||
parser.add_argument("--password-secret", default=DEFAULT_SECRET)
|
parser.add_argument("--password-secret", default=DEFAULT_SECRET)
|
||||||
|
parser.add_argument("--ssl-root-cert", default=DEFAULT_SSL_ROOT_CERT, type=Path)
|
||||||
parser.add_argument("--service", default=DEFAULT_SERVICE)
|
parser.add_argument("--service", default=DEFAULT_SERVICE)
|
||||||
parser.add_argument("--live-profile", default=bound.LIVE_PROFILE, type=Path)
|
parser.add_argument("--live-profile", default=bound.LIVE_PROFILE, type=Path)
|
||||||
parser.add_argument("--chat-id", default=bound.DEFAULT_CHAT_ID)
|
parser.add_argument("--chat-id", default=bound.DEFAULT_CHAT_ID)
|
||||||
|
|
@ -804,6 +819,8 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
||||||
parser.error("--target-db must start with teleo_clone_ and cannot name a live database")
|
parser.error("--target-db must start with teleo_clone_ and cannot name a live database")
|
||||||
if not args.cloudsql_tool.is_file():
|
if not args.cloudsql_tool.is_file():
|
||||||
parser.error("--cloudsql-tool must exist")
|
parser.error("--cloudsql-tool must exist")
|
||||||
|
if not args.ssl_root_cert.is_file():
|
||||||
|
parser.error("--ssl-root-cert must exist")
|
||||||
if not args.manifest_sql.is_file():
|
if not args.manifest_sql.is_file():
|
||||||
parser.error("--manifest-sql must exist")
|
parser.error("--manifest-sql must exist")
|
||||||
if not args.parity_receipt.is_file():
|
if not args.parity_receipt.is_file():
|
||||||
|
|
|
||||||
|
|
@ -452,7 +452,8 @@ select current_database() = :'expected_database' as exact_database_bound \\gset
|
||||||
"psql",
|
"psql",
|
||||||
(
|
(
|
||||||
f"host={self.args.host} port=5432 dbname={self.args.target_db} "
|
f"host={self.args.host} port=5432 dbname={self.args.target_db} "
|
||||||
f"user={role} sslmode=require connect_timeout=8"
|
f"user={role} sslmode=verify-ca "
|
||||||
|
f"sslrootcert={self.args.ssl_root_cert} connect_timeout=8"
|
||||||
),
|
),
|
||||||
"-X",
|
"-X",
|
||||||
"-Atq",
|
"-Atq",
|
||||||
|
|
@ -461,7 +462,13 @@ select current_database() = :'expected_database' as exact_database_bound \\gset
|
||||||
"-v",
|
"-v",
|
||||||
f"expected_database={self.args.target_db}",
|
f"expected_database={self.args.target_db}",
|
||||||
]
|
]
|
||||||
env = {**os.environ, "PGPASSWORD": password, "PGOPTIONS": pgoptions}
|
env = {
|
||||||
|
**os.environ,
|
||||||
|
"PGPASSWORD": password,
|
||||||
|
"PGOPTIONS": pgoptions,
|
||||||
|
"PGSSLMODE": "verify-ca",
|
||||||
|
"PGSSLROOTCERT": str(self.args.ssl_root_cert),
|
||||||
|
}
|
||||||
try:
|
try:
|
||||||
try:
|
try:
|
||||||
output = gcp.run(
|
output = gcp.run(
|
||||||
|
|
@ -474,6 +481,8 @@ select current_database() = :'expected_database' as exact_database_bound \\gset
|
||||||
finally:
|
finally:
|
||||||
env.pop("PGPASSWORD", None)
|
env.pop("PGPASSWORD", None)
|
||||||
env.pop("PGOPTIONS", None)
|
env.pop("PGOPTIONS", None)
|
||||||
|
env.pop("PGSSLMODE", None)
|
||||||
|
env.pop("PGSSLROOTCERT", None)
|
||||||
password = ""
|
password = ""
|
||||||
lines = output.splitlines()
|
lines = output.splitlines()
|
||||||
receipt_lines = [line for line in lines if line.startswith(ROLE_RECEIPT_PREFIX)]
|
receipt_lines = [line for line in lines if line.startswith(ROLE_RECEIPT_PREFIX)]
|
||||||
|
|
@ -692,6 +701,7 @@ def _build_gcp_read_wrapper(
|
||||||
host=args.host,
|
host=args.host,
|
||||||
project=args.project,
|
project=args.project,
|
||||||
password_secret=args.password_secret,
|
password_secret=args.password_secret,
|
||||||
|
ssl_root_cert=getattr(args, "ssl_root_cert", gcp.DEFAULT_SSL_ROOT_CERT),
|
||||||
run_nonce=run_nonce,
|
run_nonce=run_nonce,
|
||||||
)
|
)
|
||||||
wrapper = wrapper.replace(
|
wrapper = wrapper.replace(
|
||||||
|
|
@ -699,7 +709,11 @@ def _build_gcp_read_wrapper(
|
||||||
f"PASSWORD_SECRET={shlex.quote(args.password_secret)}\nREAD_ROLE={shlex.quote(args.read_role)}\n",
|
f"PASSWORD_SECRET={shlex.quote(args.password_secret)}\nREAD_ROLE={shlex.quote(args.read_role)}\n",
|
||||||
1,
|
1,
|
||||||
)
|
)
|
||||||
wrapper = wrapper.replace("user=postgres sslmode=require", "user=$READ_ROLE sslmode=require", 1)
|
wrapper = wrapper.replace(
|
||||||
|
"user=postgres sslmode=verify-ca",
|
||||||
|
"user=$READ_ROLE sslmode=verify-ca",
|
||||||
|
1,
|
||||||
|
)
|
||||||
wrapper = wrapper.replace(
|
wrapper = wrapper.replace(
|
||||||
" 'default_transaction_read_only', current_setting('default_transaction_read_only')\n",
|
" 'default_transaction_read_only', current_setting('default_transaction_read_only')\n",
|
||||||
" 'default_transaction_read_only', current_setting('default_transaction_read_only'),\n"
|
" 'default_transaction_read_only', current_setting('default_transaction_read_only'),\n"
|
||||||
|
|
@ -755,6 +769,8 @@ def _cloudsql_lifecycle_wrapper(
|
||||||
args.instance,
|
args.instance,
|
||||||
"--password-secret",
|
"--password-secret",
|
||||||
args.password_secret,
|
args.password_secret,
|
||||||
|
"--ssl-root-cert",
|
||||||
|
str(args.ssl_root_cert),
|
||||||
"--read-role",
|
"--read-role",
|
||||||
args.read_role,
|
args.read_role,
|
||||||
"--stage-password-secret",
|
"--stage-password-secret",
|
||||||
|
|
@ -1600,6 +1616,7 @@ def parse_internal_stage_args(argv: list[str]) -> argparse.Namespace:
|
||||||
parser.add_argument("--project", required=True)
|
parser.add_argument("--project", required=True)
|
||||||
parser.add_argument("--instance", required=True)
|
parser.add_argument("--instance", required=True)
|
||||||
parser.add_argument("--password-secret", required=True)
|
parser.add_argument("--password-secret", required=True)
|
||||||
|
parser.add_argument("--ssl-root-cert", required=True, type=Path)
|
||||||
parser.add_argument("--read-role", required=True)
|
parser.add_argument("--read-role", required=True)
|
||||||
parser.add_argument("--stage-password-secret", required=True)
|
parser.add_argument("--stage-password-secret", required=True)
|
||||||
parser.add_argument("--stage-role", required=True)
|
parser.add_argument("--stage-role", required=True)
|
||||||
|
|
@ -1609,6 +1626,8 @@ def parse_internal_stage_args(argv: list[str]) -> argparse.Namespace:
|
||||||
parser.add_argument("--target-identity-receipt", required=True, type=Path)
|
parser.add_argument("--target-identity-receipt", required=True, type=Path)
|
||||||
parser.add_argument("--target-identity-sha256", required=True)
|
parser.add_argument("--target-identity-sha256", required=True)
|
||||||
args = parser.parse_args(argv)
|
args = parser.parse_args(argv)
|
||||||
|
if not args.ssl_root_cert.is_file():
|
||||||
|
parser.error("--ssl-root-cert must exist")
|
||||||
args.review_role = "kb_review"
|
args.review_role = "kb_review"
|
||||||
args.apply_role = "kb_apply"
|
args.apply_role = "kb_apply"
|
||||||
args.allow_stage = True
|
args.allow_stage = True
|
||||||
|
|
@ -1680,6 +1699,7 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
||||||
parser.add_argument("--project", default=gcp.DEFAULT_PROJECT)
|
parser.add_argument("--project", default=gcp.DEFAULT_PROJECT)
|
||||||
parser.add_argument("--instance")
|
parser.add_argument("--instance")
|
||||||
parser.add_argument("--password-secret", default=gcp.DEFAULT_SECRET)
|
parser.add_argument("--password-secret", default=gcp.DEFAULT_SECRET)
|
||||||
|
parser.add_argument("--ssl-root-cert", default=gcp.DEFAULT_SSL_ROOT_CERT, type=Path)
|
||||||
parser.add_argument("--read-role", default="kb_read")
|
parser.add_argument("--read-role", default="kb_read")
|
||||||
parser.add_argument("--stage-role", default=DEFAULT_STAGE_ROLE)
|
parser.add_argument("--stage-role", default=DEFAULT_STAGE_ROLE)
|
||||||
parser.add_argument("--review-role", default=DEFAULT_REVIEW_ROLE)
|
parser.add_argument("--review-role", default=DEFAULT_REVIEW_ROLE)
|
||||||
|
|
@ -1713,6 +1733,8 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
||||||
if args.turn_timeout <= 0:
|
if args.turn_timeout <= 0:
|
||||||
parser.error("--turn-timeout must be positive")
|
parser.error("--turn-timeout must be positive")
|
||||||
if args.execute:
|
if args.execute:
|
||||||
|
if not args.ssl_root_cert.is_file():
|
||||||
|
parser.error("--ssl-root-cert must exist for execution")
|
||||||
receipt_inputs = (
|
receipt_inputs = (
|
||||||
args.target_identity_receipt,
|
args.target_identity_receipt,
|
||||||
args.target_identity_sha256,
|
args.target_identity_sha256,
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,36 @@
|
||||||
[Service]
|
[Service]
|
||||||
UnsetEnvironment=PGPASSWORD
|
UnsetEnvironment=PGPASSWORD
|
||||||
UnsetEnvironment=PGPASSFILE PGSERVICE PGHOST PGPORT PGDATABASE PGUSER PGOPTIONS
|
UnsetEnvironment=PGPASSFILE PGSERVICE PGHOST PGPORT PGDATABASE PGUSER PGOPTIONS
|
||||||
UnsetEnvironment=GOOGLE_APPLICATION_CREDENTIALS CLOUDSDK_CONFIG CLOUDSDK_CORE_PROJECT CLOUDSDK_AUTH_ACCESS_TOKEN
|
UnsetEnvironment=GOOGLE_APPLICATION_CREDENTIALS CLOUDSDK_CORE_PROJECT CLOUDSDK_AUTH_ACCESS_TOKEN
|
||||||
UnsetEnvironment=BASH_ENV ENV BASHOPTS SHELLOPTS CDPATH GLOBIGNORE IFS PS4
|
UnsetEnvironment=BASH_ENV ENV BASHOPTS SHELLOPTS CDPATH GLOBIGNORE IFS PS4
|
||||||
UnsetEnvironment=LD_PRELOAD LD_LIBRARY_PATH LD_AUDIT LD_DEBUG LD_DEBUG_OUTPUT LD_DYNAMIC_WEAK LD_HWCAP_MASK LD_ORIGIN_PATH LD_PROFILE LD_SHOW_AUXV LD_TRACE_LOADED_OBJECTS LD_USE_LOAD_BIAS LD_VERBOSE LD_WARN
|
UnsetEnvironment=LD_PRELOAD LD_LIBRARY_PATH LD_AUDIT LD_DEBUG LD_DEBUG_OUTPUT LD_DYNAMIC_WEAK LD_HWCAP_MASK LD_ORIGIN_PATH LD_PROFILE LD_SHOW_AUXV LD_TRACE_LOADED_OBJECTS LD_USE_LOAD_BIAS LD_VERBOSE LD_WARN
|
||||||
UnsetEnvironment=PYTHONHOME PYTHONPATH PYTHONSTARTUP PYTHONINSPECT PYTHONUSERBASE PYTHONHTTPSVERIFY
|
UnsetEnvironment=PYTHONHOME PYTHONPATH PYTHONSTARTUP PYTHONINSPECT PYTHONUSERBASE PYTHONHTTPSVERIFY
|
||||||
UnsetEnvironment=HTTP_PROXY HTTPS_PROXY ALL_PROXY NO_PROXY http_proxy https_proxy all_proxy no_proxy
|
UnsetEnvironment=HTTP_PROXY HTTPS_PROXY ALL_PROXY NO_PROXY http_proxy https_proxy all_proxy no_proxy
|
||||||
UnsetEnvironment=SSL_CERT_FILE SSL_CERT_DIR REQUESTS_CA_BUNDLE CURL_CA_BUNDLE SSLKEYLOGFILE
|
UnsetEnvironment=SSL_CERT_FILE SSL_CERT_DIR REQUESTS_CA_BUNDLE CURL_CA_BUNDLE SSLKEYLOGFILE
|
||||||
ReadOnlyPaths=/home/teleo/.hermes
|
UnsetEnvironment=OPENSSL_CONF OPENSSL_MODULES OPENSSL_ENGINES GCONV_PATH CREDENTIALS_DIRECTORY
|
||||||
|
UnsetEnvironment=XDG_CONFIG_HOME
|
||||||
|
UnsetEnvironment=TELEO_CLOUDSQL_CREDENTIAL_MODE TELEO_KB_CLAIM_BASE_URL TELEO_KB_READ_ATTEMPTS TELEO_MEMORY_INCLUDE_EXCERPTS TELEO_MEMORY_REDACT
|
||||||
|
ReadOnlyPaths=
|
||||||
|
ReadWritePaths=
|
||||||
|
InaccessiblePaths=
|
||||||
|
BindPaths=
|
||||||
|
BindReadOnlyPaths=
|
||||||
|
SupplementaryGroups=
|
||||||
|
ReadOnlyPaths=/home/teleo
|
||||||
ReadWritePaths=/home/teleo/.hermes/profiles/leoclean/state /home/teleo/.hermes/profiles/leoclean/workspace
|
ReadWritePaths=/home/teleo/.hermes/profiles/leoclean/state /home/teleo/.hermes/profiles/leoclean/workspace
|
||||||
|
InaccessiblePaths=-/home/teleo/.config/gcloud -/home/teleo/.pgpass -/home/teleo/.pg_service.conf -/home/teleo/.postgresql
|
||||||
BindReadOnlyPaths=/usr/local/libexec/livingip/leoclean-kb:/home/teleo/.hermes/profiles/leoclean/bin
|
BindReadOnlyPaths=/usr/local/libexec/livingip/leoclean-kb:/home/teleo/.hermes/profiles/leoclean/bin
|
||||||
CapabilityBoundingSet=~CAP_SYS_ADMIN
|
CapabilityBoundingSet=
|
||||||
|
AmbientCapabilities=
|
||||||
|
NoNewPrivileges=yes
|
||||||
|
Group=teleo
|
||||||
|
SupplementaryGroups=teleo
|
||||||
Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||||
|
Environment=CLOUDSDK_CONFIG=/usr/local/libexec/livingip/leoclean-kb/gcloud-config
|
||||||
|
Environment=PGSSLMODE=verify-ca
|
||||||
|
Environment=PGSSLROOTCERT=/usr/local/libexec/livingip/leoclean-kb/cloudsql-server-ca.pem
|
||||||
|
Environment=PYTHONNOUSERSITE=1
|
||||||
|
Environment=PYTHONSAFEPATH=1
|
||||||
Environment=TELEO_KB_MODE=cloudsql
|
Environment=TELEO_KB_MODE=cloudsql
|
||||||
Environment=TELEO_GCP_METADATA_ONLY=1
|
Environment=TELEO_GCP_METADATA_ONLY=1
|
||||||
Environment=TELEO_GCP_PROJECT=teleo-501523
|
Environment=TELEO_GCP_PROJECT=teleo-501523
|
||||||
|
|
|
||||||
|
|
@ -109,6 +109,7 @@ def test_wrapper_binds_private_read_only_database_and_records_calls(tmp_path: Pa
|
||||||
host="10.61.0.3",
|
host="10.61.0.3",
|
||||||
project="teleo-501523",
|
project="teleo-501523",
|
||||||
password_secret="test-secret-name",
|
password_secret="test-secret-name",
|
||||||
|
ssl_root_cert=tmp_path / "cloudsql-server-ca.pem",
|
||||||
run_nonce="nonce",
|
run_nonce="nonce",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -117,7 +118,8 @@ def test_wrapper_binds_private_read_only_database_and_records_calls(tmp_path: Pa
|
||||||
assert '--canonical-db "$TARGET_DATABASE"' in wrapper
|
assert '--canonical-db "$TARGET_DATABASE"' in wrapper
|
||||||
assert "--credential-mode clone-readonly" in wrapper
|
assert "--credential-mode clone-readonly" in wrapper
|
||||||
assert "--user postgres" in wrapper
|
assert "--user postgres" in wrapper
|
||||||
assert "sslmode=require" in wrapper
|
assert "sslmode=verify-ca" in wrapper
|
||||||
|
assert "sslrootcert=$SSL_ROOT_CERT" in wrapper
|
||||||
assert "begin transaction read only" in wrapper
|
assert "begin transaction read only" in wrapper
|
||||||
assert 'export PGOPTIONS="-c default_transaction_read_only=on"' in wrapper
|
assert 'export PGOPTIONS="-c default_transaction_read_only=on"' in wrapper
|
||||||
assert "default_transaction_read_only" in wrapper
|
assert "default_transaction_read_only" in wrapper
|
||||||
|
|
@ -140,6 +142,7 @@ def test_database_identity_requires_private_tls_and_read_only_target() -> None:
|
||||||
"default_transaction_read_only": "on",
|
"default_transaction_read_only": "on",
|
||||||
},
|
},
|
||||||
"teleo_clone_test",
|
"teleo_clone_test",
|
||||||
|
"1234",
|
||||||
)
|
)
|
||||||
|
|
||||||
with pytest.raises(RuntimeError, match="not RFC1918-private"):
|
with pytest.raises(RuntimeError, match="not RFC1918-private"):
|
||||||
|
|
@ -153,6 +156,7 @@ def test_database_identity_requires_private_tls_and_read_only_target() -> None:
|
||||||
"default_transaction_read_only": "on",
|
"default_transaction_read_only": "on",
|
||||||
},
|
},
|
||||||
"teleo_clone_test",
|
"teleo_clone_test",
|
||||||
|
"1234",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -184,6 +188,7 @@ def test_database_fingerprint_uses_full_normalized_manifest(monkeypatch: pytest.
|
||||||
"host": "10.61.0.3",
|
"host": "10.61.0.3",
|
||||||
"target_db": "teleo_clone_test",
|
"target_db": "teleo_clone_test",
|
||||||
"manifest_sql": manifest_sql,
|
"manifest_sql": manifest_sql,
|
||||||
|
"ssl_root_cert": tmp_path / "cloudsql-server-ca.pem",
|
||||||
},
|
},
|
||||||
)()
|
)()
|
||||||
output = "\n".join(
|
output = "\n".join(
|
||||||
|
|
@ -282,6 +287,7 @@ def test_generated_wrapper_executes_only_clone_bound_default_read_only_tool(tmp_
|
||||||
host="10.61.0.3",
|
host="10.61.0.3",
|
||||||
project="teleo-501523",
|
project="teleo-501523",
|
||||||
password_secret="test-secret",
|
password_secret="test-secret",
|
||||||
|
ssl_root_cert=tmp_path / "cloudsql-server-ca.pem",
|
||||||
run_nonce="test-nonce",
|
run_nonce="test-nonce",
|
||||||
system_exec_path=f"{fake_bin}:/usr/bin:/bin",
|
system_exec_path=f"{fake_bin}:/usr/bin:/bin",
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -131,6 +131,7 @@ def executor_args(**overrides: object) -> argparse.Namespace:
|
||||||
"project": "teleo-501523",
|
"project": "teleo-501523",
|
||||||
"instance": "teleo-pgvector-standby",
|
"instance": "teleo-pgvector-standby",
|
||||||
"password_secret": "read-secret",
|
"password_secret": "read-secret",
|
||||||
|
"ssl_root_cert": Path("ops/gcp-teleo-pgvector-standby-server-ca.pem").resolve(),
|
||||||
"read_role": "kb_read",
|
"read_role": "kb_read",
|
||||||
"stage_role": "kb_stage_writer",
|
"stage_role": "kb_stage_writer",
|
||||||
"review_role": "kb_review",
|
"review_role": "kb_review",
|
||||||
|
|
@ -229,6 +230,10 @@ def test_executor_binds_every_call_and_defaults_read_only_except_controller_phas
|
||||||
|
|
||||||
assert len(calls) == 4
|
assert len(calls) == 4
|
||||||
assert all("dbname=teleo_clone_working_leo" in call["command"][1] for call in calls)
|
assert all("dbname=teleo_clone_working_leo" in call["command"][1] for call in calls)
|
||||||
|
assert all("sslmode=verify-ca" in call["command"][1] for call in calls)
|
||||||
|
assert all("sslrootcert=" in call["command"][1] for call in calls)
|
||||||
|
assert all(call["env"]["PGSSLMODE"] == "verify-ca" for call in calls)
|
||||||
|
assert all(call["env"]["PGSSLROOTCERT"].endswith("gcp-teleo-pgvector-standby-server-ca.pem") for call in calls)
|
||||||
assert all("expected_database=teleo_clone_working_leo" in call["command"] for call in calls)
|
assert all("expected_database=teleo_clone_working_leo" in call["command"] for call in calls)
|
||||||
assert calls[0]["env"]["PGOPTIONS"] == "-c default_transaction_read_only=on"
|
assert calls[0]["env"]["PGOPTIONS"] == "-c default_transaction_read_only=on"
|
||||||
assert [call["env"]["PGOPTIONS"] for call in calls[1:]] == [
|
assert [call["env"]["PGOPTIONS"] for call in calls[1:]] == [
|
||||||
|
|
@ -535,6 +540,9 @@ def test_lifecycle_wrapper_exposes_only_read_or_exact_stage_and_never_delivery(t
|
||||||
)
|
)
|
||||||
assert catalog_wrapper.count('"prompt_id": "DC-01"') == 2
|
assert catalog_wrapper.count('"prompt_id": "DC-01"') == 2
|
||||||
assert '--user "$READ_ROLE"' in catalog_wrapper
|
assert '--user "$READ_ROLE"' in catalog_wrapper
|
||||||
|
assert "sslmode=verify-ca" in catalog_wrapper
|
||||||
|
assert "sslrootcert=$SSL_ROOT_CERT" in catalog_wrapper
|
||||||
|
assert "sslmode=require" not in catalog_wrapper
|
||||||
|
|
||||||
|
|
||||||
def test_capability_receipt_rejects_superuser_or_wrong_phase_acl() -> None:
|
def test_capability_receipt_rejects_superuser_or_wrong_phase_acl() -> None:
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,6 @@ class FakeRunner:
|
||||||
function_posture_override: Mapping[str, Mapping[str, object]] | None = None,
|
function_posture_override: Mapping[str, Mapping[str, object]] | None = None,
|
||||||
stage_definition_override: Mapping[str, object] | None = None,
|
stage_definition_override: Mapping[str, object] | None = None,
|
||||||
catalog_posture_override: Mapping[str, object] | None = None,
|
catalog_posture_override: Mapping[str, object] | None = None,
|
||||||
legacy_catalog_posture_override: Mapping[str, object] | None = None,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
self.sqlstate_override = sqlstate_override
|
self.sqlstate_override = sqlstate_override
|
||||||
self.sqlstate_missing_for = sqlstate_missing_for
|
self.sqlstate_missing_for = sqlstate_missing_for
|
||||||
|
|
@ -46,7 +45,6 @@ class FakeRunner:
|
||||||
}
|
}
|
||||||
self.stage_definition_override = dict(stage_definition_override or {})
|
self.stage_definition_override = dict(stage_definition_override or {})
|
||||||
self.catalog_posture_override = dict(catalog_posture_override or {})
|
self.catalog_posture_override = dict(catalog_posture_override or {})
|
||||||
self.legacy_catalog_posture_override = dict(legacy_catalog_posture_override or {})
|
|
||||||
self.calls: list[tuple[list[str], dict[str, str], int, bool]] = []
|
self.calls: list[tuple[list[str], dict[str, str], int, bool]] = []
|
||||||
self.negative = {check.sql: check for check in verifier._negative_checks(RUN_ID, SOURCE_REF)}
|
self.negative = {check.sql: check for check in verifier._negative_checks(RUN_ID, SOURCE_REF)}
|
||||||
|
|
||||||
|
|
@ -94,6 +92,7 @@ class FakeRunner:
|
||||||
"session_user": verifier.RUNTIME_DATABASE_ROLE,
|
"session_user": verifier.RUNTIME_DATABASE_ROLE,
|
||||||
"ssl": True,
|
"ssl": True,
|
||||||
"ssl_version": "TLSv1.3",
|
"ssl_version": "TLSv1.3",
|
||||||
|
"system_identifier": verifier.EXPECTED_SYSTEM_IDENTIFIER,
|
||||||
}
|
}
|
||||||
identity.update(self.identity_override)
|
identity.update(self.identity_override)
|
||||||
return verifier.CommandResult(0, json.dumps(identity), f"ignored {SECRET_VALUE}")
|
return verifier.CommandResult(0, json.dumps(identity), f"ignored {SECRET_VALUE}")
|
||||||
|
|
@ -179,13 +178,6 @@ class FakeRunner:
|
||||||
)
|
)
|
||||||
catalog_posture.update(self.catalog_posture_override)
|
catalog_posture.update(self.catalog_posture_override)
|
||||||
return verifier.CommandResult(0, json.dumps(catalog_posture), f"ignored {SECRET_VALUE}")
|
return verifier.CommandResult(0, json.dumps(catalog_posture), f"ignored {SECRET_VALUE}")
|
||||||
if sql == verifier._legacy_catalog_privilege_posture_sql():
|
|
||||||
legacy_posture: dict[str, object] = {
|
|
||||||
field: 0 for field in verifier.LEGACY_CATALOG_ZERO_COUNT_FIELDS
|
|
||||||
}
|
|
||||||
legacy_posture["leak"] = SECRET_VALUE
|
|
||||||
legacy_posture.update(self.legacy_catalog_posture_override)
|
|
||||||
return verifier.CommandResult(0, json.dumps(legacy_posture), f"ignored {SECRET_VALUE}")
|
|
||||||
if sql == verifier._canary_count_sql(SOURCE_REF):
|
if sql == verifier._canary_count_sql(SOURCE_REF):
|
||||||
return verifier.CommandResult(0, "0\n", f"ignored {SECRET_VALUE}")
|
return verifier.CommandResult(0, "0\n", f"ignored {SECRET_VALUE}")
|
||||||
if sql == verifier._stage_sql(RUN_ID, SOURCE_REF):
|
if sql == verifier._stage_sql(RUN_ID, SOURCE_REF):
|
||||||
|
|
@ -211,6 +203,15 @@ class FakeRunner:
|
||||||
return verifier.CommandResult(0, SECRET_VALUE, f"unexpected success {SECRET_VALUE}")
|
return verifier.CommandResult(0, SECRET_VALUE, f"unexpected success {SECRET_VALUE}")
|
||||||
if check.name == self.sqlstate_missing_for:
|
if check.name == self.sqlstate_missing_for:
|
||||||
return verifier.CommandResult(1, SECRET_VALUE, f"denied without state {SECRET_VALUE}")
|
return verifier.CommandResult(1, SECRET_VALUE, f"denied without state {SECRET_VALUE}")
|
||||||
|
if check.expected_connection_denial:
|
||||||
|
return verifier.CommandResult(
|
||||||
|
2,
|
||||||
|
SECRET_VALUE,
|
||||||
|
(
|
||||||
|
f'connection failed: FATAL: permission denied for database "{check.database}"\n'
|
||||||
|
f"DETAIL: User does not have CONNECT privilege. {SECRET_VALUE}"
|
||||||
|
),
|
||||||
|
)
|
||||||
observed = check.expected_sqlstate
|
observed = check.expected_sqlstate
|
||||||
if self.sqlstate_override is not None and check.name == self.sqlstate_override[0]:
|
if self.sqlstate_override is not None and check.name == self.sqlstate_override[0]:
|
||||||
observed = self.sqlstate_override[1]
|
observed = self.sqlstate_override[1]
|
||||||
|
|
@ -243,6 +244,7 @@ def run_success(runner: FakeRunner) -> dict[str, object]:
|
||||||
runner=runner,
|
runner=runner,
|
||||||
base_env=poisoned_environment(),
|
base_env=poisoned_environment(),
|
||||||
effective_user="teleo",
|
effective_user="teleo",
|
||||||
|
dependency_validator=lambda: None,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -253,8 +255,10 @@ def assert_child_boundaries(runner: FakeRunner) -> None:
|
||||||
assert timeout == verifier.COMMAND_TIMEOUT_SECONDS
|
assert timeout == verifier.COMMAND_TIMEOUT_SECONDS
|
||||||
pg_keys = sorted(key for key in env if key.upper().startswith("PG"))
|
pg_keys = sorted(key for key in env if key.upper().startswith("PG"))
|
||||||
if command[0] == verifier.PSQL_BIN:
|
if command[0] == verifier.PSQL_BIN:
|
||||||
assert pg_keys == ["PGPASSWORD"]
|
assert pg_keys == ["PGPASSWORD", "PGSSLMODE", "PGSSLROOTCERT"]
|
||||||
assert env["PGPASSWORD"] == SECRET_VALUE
|
assert env["PGPASSWORD"] == SECRET_VALUE
|
||||||
|
assert env["PGSSLMODE"] == "verify-ca"
|
||||||
|
assert env["PGSSLROOTCERT"] == str(verifier.SERVER_CA_PATH)
|
||||||
assert discard_stdout is False
|
assert discard_stdout is False
|
||||||
else:
|
else:
|
||||||
assert pg_keys == []
|
assert pg_keys == []
|
||||||
|
|
@ -284,6 +288,7 @@ def test_live_receipt_contract_is_sanitized_and_uses_exact_child_environments()
|
||||||
"session_user": verifier.RUNTIME_DATABASE_ROLE,
|
"session_user": verifier.RUNTIME_DATABASE_ROLE,
|
||||||
"ssl": True,
|
"ssl": True,
|
||||||
"ssl_version": "TLSv1.3",
|
"ssl_version": "TLSv1.3",
|
||||||
|
"system_identifier": verifier.EXPECTED_SYSTEM_IDENTIFIER,
|
||||||
}
|
}
|
||||||
assert receipt["checks"]["rolled_back_proposal"] == {
|
assert receipt["checks"]["rolled_back_proposal"] == {
|
||||||
"agent_id_matches": True,
|
"agent_id_matches": True,
|
||||||
|
|
@ -331,9 +336,6 @@ def test_live_receipt_contract_is_sanitized_and_uses_exact_child_environments()
|
||||||
**{field: 0 for field in verifier.CATALOG_ZERO_COUNT_FIELDS},
|
**{field: 0 for field in verifier.CATALOG_ZERO_COUNT_FIELDS},
|
||||||
**{field: True for field in verifier.CATALOG_TRUE_FIELDS},
|
**{field: True for field in verifier.CATALOG_TRUE_FIELDS},
|
||||||
}
|
}
|
||||||
assert receipt["checks"]["legacy_catalog_privileges"] == {
|
|
||||||
field: 0 for field in verifier.LEGACY_CATALOG_ZERO_COUNT_FIELDS
|
|
||||||
}
|
|
||||||
assert receipt["checks"]["stage_function_definition"] == {
|
assert receipt["checks"]["stage_function_definition"] == {
|
||||||
"acl_exact": True,
|
"acl_exact": True,
|
||||||
"argument_modes": None,
|
"argument_modes": None,
|
||||||
|
|
@ -570,7 +572,6 @@ def test_catalog_queries_use_exact_regprocedure_signatures_and_both_membership_d
|
||||||
owner_role_sql = verifier._stage_owner_role_posture_sql()
|
owner_role_sql = verifier._stage_owner_role_posture_sql()
|
||||||
stage_definition_sql = verifier._stage_function_definition_sql()
|
stage_definition_sql = verifier._stage_function_definition_sql()
|
||||||
catalog_sql = verifier._catalog_privilege_posture_sql()
|
catalog_sql = verifier._catalog_privilege_posture_sql()
|
||||||
legacy_sql = verifier._legacy_catalog_privilege_posture_sql()
|
|
||||||
|
|
||||||
assert "pg_catalog.to_regprocedure(signature)" in function_sql
|
assert "pg_catalog.to_regprocedure(signature)" in function_sql
|
||||||
for _name, signature, _exists, _execute in verifier.FUNCTION_PRIVILEGE_EXPECTATIONS:
|
for _name, signature, _exists, _execute in verifier.FUNCTION_PRIVILEGE_EXPECTATIONS:
|
||||||
|
|
@ -597,12 +598,10 @@ def test_catalog_queries_use_exact_regprocedure_signatures_and_both_membership_d
|
||||||
assert "nspname !~ '^pg_'" in catalog_sql
|
assert "nspname !~ '^pg_'" in catalog_sql
|
||||||
assert "nspname <> 'information_schema'" in catalog_sql
|
assert "nspname <> 'information_schema'" in catalog_sql
|
||||||
assert verifier.STAGE_OWNER_DATABASE_ROLE in catalog_sql
|
assert verifier.STAGE_OWNER_DATABASE_ROLE in catalog_sql
|
||||||
assert "teleo_restore" in legacy_sql
|
assert "public_database_connect_grants" in catalog_sql
|
||||||
assert "pg_catalog.has_schema_privilege" in legacy_sql
|
assert "public_large_object_mutation_routine_execute" in catalog_sql
|
||||||
assert "pg_catalog.has_table_privilege" in legacy_sql
|
assert "'lo_creat'" in catalog_sql
|
||||||
assert "pg_catalog.has_column_privilege" in legacy_sql
|
assert "'lo_open'" in catalog_sql
|
||||||
assert "pg_catalog.has_sequence_privilege" in legacy_sql
|
|
||||||
assert "pg_catalog.has_function_privilege" in legacy_sql
|
|
||||||
for schema, relation in verifier.READ_TABLE_ALLOWLIST:
|
for schema, relation in verifier.READ_TABLE_ALLOWLIST:
|
||||||
assert schema in catalog_sql
|
assert schema in catalog_sql
|
||||||
assert relation in catalog_sql
|
assert relation in catalog_sql
|
||||||
|
|
@ -630,38 +629,27 @@ def test_any_missing_required_catalog_privilege_fails_closed(field: str) -> None
|
||||||
assert caught.value.code == "catalog_privilege_posture_mismatch"
|
assert caught.value.code == "catalog_privilege_posture_mismatch"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("field", verifier.LEGACY_CATALOG_ZERO_COUNT_FIELDS)
|
def test_legacy_and_template_database_connect_are_both_behaviorally_denied() -> None:
|
||||||
def test_any_teleo_restore_catalog_privilege_fails_closed(field: str) -> None:
|
|
||||||
runner = FakeRunner(legacy_catalog_posture_override={field: 1})
|
|
||||||
|
|
||||||
with pytest.raises(verifier.VerificationError) as caught:
|
|
||||||
run_success(runner)
|
|
||||||
|
|
||||||
assert caught.value.code == "legacy_catalog_privilege_posture_mismatch"
|
|
||||||
assert caught.value.check == "legacy_catalog_privileges"
|
|
||||||
assert SECRET_VALUE not in str(caught.value)
|
|
||||||
|
|
||||||
|
|
||||||
def test_teleo_restore_catalog_and_behavioral_denial_are_both_exercised_on_legacy_database() -> None:
|
|
||||||
runner = FakeRunner()
|
runner = FakeRunner()
|
||||||
|
|
||||||
receipt = run_success(runner)
|
receipt = run_success(runner)
|
||||||
|
|
||||||
legacy_catalog_call = next(
|
denials = {check["check"]: check for check in receipt["checks"]["negative_permissions"]}
|
||||||
command
|
for name in ("legacy_database_connect", "template_database_connect"):
|
||||||
|
assert denials[name] == {
|
||||||
|
"check": name,
|
||||||
|
"expected_sqlstate": "42501",
|
||||||
|
"observed_error_class": "connect_privilege_denied",
|
||||||
|
"observed_sqlstate": "not_exposed_by_libpq_startup",
|
||||||
|
"result": "denied",
|
||||||
|
}
|
||||||
|
database_calls = [
|
||||||
|
" ".join(command)
|
||||||
for command, _env, _timeout, _discard in runner.calls
|
for command, _env, _timeout, _discard in runner.calls
|
||||||
if any(part == f"--command={verifier._legacy_catalog_privilege_posture_sql()}" for part in command)
|
if command[0] == verifier.PSQL_BIN
|
||||||
)
|
]
|
||||||
assert any(f"dbname={verifier.LEGACY_DATABASE}" in part for part in legacy_catalog_call)
|
assert any(f"dbname={verifier.LEGACY_DATABASE}" in command for command in database_calls)
|
||||||
legacy_denial = next(
|
assert any(f"dbname={verifier.TEMPLATE_DATABASE}" in command for command in database_calls)
|
||||||
check for check in receipt["checks"]["negative_permissions"] if check["check"] == "legacy_teleo_restore_read"
|
|
||||||
)
|
|
||||||
assert legacy_denial == {
|
|
||||||
"check": "legacy_teleo_restore_read",
|
|
||||||
"expected_sqlstate": "42501",
|
|
||||||
"observed_sqlstate": "42501",
|
|
||||||
"result": "denied",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def test_required_sqlstate_mismatch_fails_closed_without_secret_in_error() -> None:
|
def test_required_sqlstate_mismatch_fails_closed_without_secret_in_error() -> None:
|
||||||
|
|
@ -693,6 +681,19 @@ def test_missing_verbose_sqlstate_fails_closed_without_raw_stderr() -> None:
|
||||||
assert SECRET_VALUE not in json.dumps(error.as_dict())
|
assert SECRET_VALUE not in json.dumps(error.as_dict())
|
||||||
|
|
||||||
|
|
||||||
|
def test_unclassified_database_startup_failure_cannot_masquerade_as_connect_denial() -> None:
|
||||||
|
runner = FakeRunner(sqlstate_missing_for="legacy_database_connect")
|
||||||
|
|
||||||
|
with pytest.raises(verifier.VerificationError) as caught:
|
||||||
|
run_success(runner)
|
||||||
|
|
||||||
|
error = caught.value
|
||||||
|
assert error.code == "negative_connection_denial_mismatch"
|
||||||
|
assert error.check == "legacy_database_connect"
|
||||||
|
assert error.observed_sqlstate == "startup_error_unclassified"
|
||||||
|
assert SECRET_VALUE not in json.dumps(error.as_dict())
|
||||||
|
|
||||||
|
|
||||||
def test_unexpected_permission_success_fails_closed_and_transaction_sql_has_rollback() -> None:
|
def test_unexpected_permission_success_fails_closed_and_transaction_sql_has_rollback() -> None:
|
||||||
runner = FakeRunner(unexpected_success_for="proposal_update")
|
runner = FakeRunner(unexpected_success_for="proposal_update")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,6 @@ def test_later_dropin_effective_override_fails_without_outputting_value(
|
||||||
[
|
[
|
||||||
("PGPASSWORD", "PG*"),
|
("PGPASSWORD", "PG*"),
|
||||||
("pgservice", "PG*"),
|
("pgservice", "PG*"),
|
||||||
("CLOUDSDK_CONFIG", "CLOUDSDK_*"),
|
|
||||||
("CLOUDSDK_CORE_PROJECT", "CLOUDSDK_*"),
|
("CLOUDSDK_CORE_PROJECT", "CLOUDSDK_*"),
|
||||||
("CLOUDSDK_AUTH_ACCESS_TOKEN", "CLOUDSDK_*"),
|
("CLOUDSDK_AUTH_ACCESS_TOKEN", "CLOUDSDK_*"),
|
||||||
("cloudsdk_auth_refresh_token", "CLOUDSDK_*"),
|
("cloudsdk_auth_refresh_token", "CLOUDSDK_*"),
|
||||||
|
|
@ -102,6 +101,11 @@ def test_later_dropin_effective_override_fails_without_outputting_value(
|
||||||
("REQUESTS_CA_BUNDLE", "REQUESTS_CA_BUNDLE"),
|
("REQUESTS_CA_BUNDLE", "REQUESTS_CA_BUNDLE"),
|
||||||
("curl_ca_bundle", "CURL_CA_BUNDLE"),
|
("curl_ca_bundle", "CURL_CA_BUNDLE"),
|
||||||
("SSLKEYLOGFILE", "SSLKEYLOGFILE"),
|
("SSLKEYLOGFILE", "SSLKEYLOGFILE"),
|
||||||
|
("TELEO_CLOUDSQL_CREDENTIAL_MODE", "TELEO_CLOUDSQL_CREDENTIAL_MODE"),
|
||||||
|
("TELEO_KB_CLAIM_BASE_URL", "TELEO_KB_CLAIM_BASE_URL"),
|
||||||
|
("TELEO_KB_READ_ATTEMPTS", "TELEO_KB_READ_ATTEMPTS"),
|
||||||
|
("TELEO_MEMORY_INCLUDE_EXCERPTS", "TELEO_MEMORY_INCLUDE_EXCERPTS"),
|
||||||
|
("TELEO_MEMORY_REDACT", "TELEO_MEMORY_REDACT"),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_environment_file_broader_credentials_and_forbidden_fields_fail_closed(
|
def test_environment_file_broader_credentials_and_forbidden_fields_fail_closed(
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,8 @@ ROOT = Path(__file__).resolve().parents[1]
|
||||||
BRIDGE_DIR = ROOT / "hermes-agent" / "leoclean-bin"
|
BRIDGE_DIR = ROOT / "hermes-agent" / "leoclean-bin"
|
||||||
DB_CONTEXT_PLUGIN = ROOT / "hermes-agent" / "leoclean-plugins" / "vps" / "leo-db-context" / "__init__.py"
|
DB_CONTEXT_PLUGIN = ROOT / "hermes-agent" / "leoclean-plugins" / "vps" / "leo-db-context" / "__init__.py"
|
||||||
GCP_RUNTIME_ROLE_SQL = ROOT / "ops" / "gcp_leoclean_runtime_role.sql"
|
GCP_RUNTIME_ROLE_SQL = ROOT / "ops" / "gcp_leoclean_runtime_role.sql"
|
||||||
|
GCP_RUNTIME_WRAPPER = BRIDGE_DIR / "teleo-kb-gcp"
|
||||||
|
GCP_RUNTIME_PROVISIONER = ROOT / "ops" / "provision_gcp_leoclean_runtime_role.sh"
|
||||||
|
|
||||||
|
|
||||||
def test_leoclean_bridge_files_are_present_and_parseable() -> None:
|
def test_leoclean_bridge_files_are_present_and_parseable() -> None:
|
||||||
|
|
@ -42,6 +44,71 @@ def test_leoclean_bridge_files_are_present_and_parseable() -> None:
|
||||||
subprocess.run(["bash", "-n", str(wrapper)], check=True)
|
subprocess.run(["bash", "-n", str(wrapper)], check=True)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"argument",
|
||||||
|
[
|
||||||
|
"--host",
|
||||||
|
"--host=127.0.0.1",
|
||||||
|
"--port",
|
||||||
|
"--port=6543",
|
||||||
|
"--db",
|
||||||
|
"--db=postgres",
|
||||||
|
"--canonical-db",
|
||||||
|
"--canonical-db=postgres",
|
||||||
|
"--user",
|
||||||
|
"--user=postgres",
|
||||||
|
"--password-secret",
|
||||||
|
"--password-secret=administrator-secret",
|
||||||
|
"--project",
|
||||||
|
"--project=other-project",
|
||||||
|
"--credential-mode",
|
||||||
|
"--credential-mode=clone-readonly",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_deployed_gcp_wrapper_executable_rejects_every_identity_override(argument: str) -> None:
|
||||||
|
injected = "sensitive-override-value"
|
||||||
|
completed = subprocess.run(
|
||||||
|
[str(GCP_RUNTIME_WRAPPER), argument, injected],
|
||||||
|
capture_output=True,
|
||||||
|
check=False,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert completed.returncode == 64
|
||||||
|
assert "identity and endpoint overrides are forbidden" in completed.stderr
|
||||||
|
assert injected not in completed.stderr
|
||||||
|
assert completed.stdout == ""
|
||||||
|
|
||||||
|
|
||||||
|
def test_gcp_wrapper_and_password_provisioner_are_executable_and_syntax_valid() -> None:
|
||||||
|
assert os.access(GCP_RUNTIME_WRAPPER, os.X_OK)
|
||||||
|
assert os.access(GCP_RUNTIME_PROVISIONER, os.X_OK)
|
||||||
|
subprocess.run(["bash", "-n", str(GCP_RUNTIME_WRAPPER)], check=True)
|
||||||
|
subprocess.run(["bash", "-n", str(GCP_RUNTIME_PROVISIONER)], check=True)
|
||||||
|
|
||||||
|
wrapper = GCP_RUNTIME_WRAPPER.read_text(encoding="utf-8")
|
||||||
|
assert "exec /usr/bin/env -i" in wrapper
|
||||||
|
assert '/usr/bin/python3 -I "$CLOUDSQL_TOOL" "$@"' in wrapper
|
||||||
|
assert "TELEO_CLOUDSQL_USER=leoclean_kb_runtime" in wrapper
|
||||||
|
assert "PGSSLMODE=verify-ca" in wrapper
|
||||||
|
assert "TELEO_CLOUDSQL_ENABLE_AUDIT_FALLBACK=0" in wrapper
|
||||||
|
|
||||||
|
provisioner = GCP_RUNTIME_PROVISIONER.read_text(encoding="utf-8")
|
||||||
|
capture_runtime = "runtime_password=${TELEO_LEOCLEAN_DB_PASSWORD-}"
|
||||||
|
capture_admin = "administrator_password=${PGPASSWORD-}"
|
||||||
|
clear_ambient = "unset TELEO_LEOCLEAN_DB_PASSWORD PGPASSWORD"
|
||||||
|
first_external_child = 'SCRIPT_DIR="$(cd -- "$script_directory" && pwd -P)"'
|
||||||
|
assert provisioner.index(capture_runtime) < provisioner.index(first_external_child)
|
||||||
|
assert provisioner.index(capture_admin) < provisioner.index(first_external_child)
|
||||||
|
assert provisioner.index(clear_ambient) < provisioner.index(first_external_child)
|
||||||
|
assert '"$SETSID_BIN" -- "$PSQL_BIN"' in provisioner
|
||||||
|
assert 'kill -TERM -- "-$provision_pid"' in provisioner
|
||||||
|
assert 'wait "$provision_pid"' in provisioner
|
||||||
|
assert "SERVER_CA_SHA256=80701e" in provisioner
|
||||||
|
assert "--no-password" in provisioner
|
||||||
|
assert "PGSSLMODE=verify-ca" in provisioner
|
||||||
|
|
||||||
|
|
||||||
def test_cloudsql_wrapper_supports_expected_review_gated_commands() -> None:
|
def test_cloudsql_wrapper_supports_expected_review_gated_commands() -> None:
|
||||||
wrapper_text = (BRIDGE_DIR / "teleo-kb").read_text()
|
wrapper_text = (BRIDGE_DIR / "teleo-kb").read_text()
|
||||||
cloudsql_text = (BRIDGE_DIR / "cloudsql_memory_tool.py").read_text()
|
cloudsql_text = (BRIDGE_DIR / "cloudsql_memory_tool.py").read_text()
|
||||||
|
|
@ -502,7 +569,7 @@ def test_cloudsql_psql_uses_only_explicit_pg_environment_fields(monkeypatch: pyt
|
||||||
monkeypatch.setattr(module.subprocess, "run", fake_run)
|
monkeypatch.setattr(module.subprocess, "run", fake_run)
|
||||||
|
|
||||||
assert module.run_psql(args, "select 1;") == "ok\n"
|
assert module.run_psql(args, "select 1;") == "ok\n"
|
||||||
assert captured["command"] == ["psql", "-X", "-At", "-q", "-v", "ON_ERROR_STOP=1"]
|
assert captured["command"] == ["/usr/bin/psql", "-X", "-At", "-q", "-v", "ON_ERROR_STOP=1"]
|
||||||
kwargs = captured["kwargs"]
|
kwargs = captured["kwargs"]
|
||||||
assert isinstance(kwargs, dict)
|
assert isinstance(kwargs, dict)
|
||||||
env = kwargs["env"]
|
env = kwargs["env"]
|
||||||
|
|
@ -512,7 +579,8 @@ def test_cloudsql_psql_uses_only_explicit_pg_environment_fields(monkeypatch: pyt
|
||||||
"PGPORT": "5432",
|
"PGPORT": "5432",
|
||||||
"PGDATABASE": "teleo_canonical",
|
"PGDATABASE": "teleo_canonical",
|
||||||
"PGUSER": module.RUNTIME_DB_USER,
|
"PGUSER": module.RUNTIME_DB_USER,
|
||||||
"PGSSLMODE": "require",
|
"PGSSLMODE": "verify-ca",
|
||||||
|
"PGSSLROOTCERT": module.RUNTIME_SSL_ROOT_CERT,
|
||||||
"PGCONNECT_TIMEOUT": "8",
|
"PGCONNECT_TIMEOUT": "8",
|
||||||
"PGPASSWORD": "scoped-runtime-password",
|
"PGPASSWORD": "scoped-runtime-password",
|
||||||
}
|
}
|
||||||
|
|
@ -728,7 +796,7 @@ def test_cloudsql_runtime_read_marker_binds_database_user_private_server_and_ssl
|
||||||
"server_port": int(module.RUNTIME_CLOUDSQL_PORT),
|
"server_port": int(module.RUNTIME_CLOUDSQL_PORT),
|
||||||
"ssl": True,
|
"ssl": True,
|
||||||
"ssl_version": "TLSv1.3",
|
"ssl_version": "TLSv1.3",
|
||||||
"system_identifier": "system-id",
|
"system_identifier": module.RUNTIME_SYSTEM_IDENTIFIER,
|
||||||
"wal_lsn": "0/1",
|
"wal_lsn": "0/1",
|
||||||
}
|
}
|
||||||
captured: dict[str, object] = {}
|
captured: dict[str, object] = {}
|
||||||
|
|
@ -771,7 +839,7 @@ def test_cloudsql_runtime_read_marker_rejects_wrong_live_identity(
|
||||||
"server_port": int(module.RUNTIME_CLOUDSQL_PORT),
|
"server_port": int(module.RUNTIME_CLOUDSQL_PORT),
|
||||||
"ssl": True,
|
"ssl": True,
|
||||||
"ssl_version": "TLSv1.3",
|
"ssl_version": "TLSv1.3",
|
||||||
"system_identifier": "system-id",
|
"system_identifier": module.RUNTIME_SYSTEM_IDENTIFIER,
|
||||||
"wal_lsn": "0/1",
|
"wal_lsn": "0/1",
|
||||||
}
|
}
|
||||||
marker[field] = value
|
marker[field] = value
|
||||||
|
|
@ -832,14 +900,16 @@ def test_gcp_runtime_role_uses_one_narrow_staging_function() -> None:
|
||||||
assert "stage_leoclean_proposal" in sql
|
assert "stage_leoclean_proposal" in sql
|
||||||
assert "'pending_review'" in sql
|
assert "'pending_review'" in sql
|
||||||
assert "revoke all privileges on all tables in schema public, kb_stage" in sql
|
assert "revoke all privileges on all tables in schema public, kb_stage" in sql
|
||||||
assert "can_insert_proposals_directly" in sql
|
assert "grant insert" in sql.lower()
|
||||||
assert "can_create_database_objects" in sql
|
|
||||||
assert "can_create_temp_objects" in sql
|
|
||||||
assert "can_connect_legacy_database" in sql
|
|
||||||
assert "unexpectedly has database CREATE" in sql
|
assert "unexpectedly has database CREATE" in sql
|
||||||
|
assert "unexpectedly has schema CREATE" in sql
|
||||||
|
assert "runtime CONNECT reaches a noncanonical database" in sql
|
||||||
|
assert "PUBLIC retains database CONNECT" in sql
|
||||||
|
assert "PUBLIC retains large-object mutator EXECUTE" in sql
|
||||||
assert "REVOKE TEMP FROM PUBLIC would be a" in sql
|
assert "REVOKE TEMP FROM PUBLIC would be a" in sql
|
||||||
assert "can execute unexpected SECURITY DEFINER functions" in sql
|
assert "can execute unexpected SECURITY DEFINER functions" in sql
|
||||||
assert "revoke all privileges (%s) on table %I.%I from %I" in sql
|
assert "revoke connect on database %I from public" in sql
|
||||||
|
assert "revoke execute on function pg_catalog.%I(%s) from public" in sql
|
||||||
assert re.search(
|
assert re.search(
|
||||||
r"grant\s+insert\s*\([^)]*proposal_type[^)]*payload[^)]*\)\s+"
|
r"grant\s+insert\s*\([^)]*proposal_type[^)]*payload[^)]*\)\s+"
|
||||||
r"on\s+kb_stage\.kb_proposals\s+to\s+leoclean_kb_stage_owner",
|
r"on\s+kb_stage\.kb_proposals\s+to\s+leoclean_kb_stage_owner",
|
||||||
|
|
@ -855,12 +925,13 @@ def test_gcp_runtime_role_uses_one_narrow_staging_function() -> None:
|
||||||
assert "grant delete" not in sql.lower()
|
assert "grant delete" not in sql.lower()
|
||||||
assert "create role leoclean_kb_runtime nologin" in sql.lower()
|
assert "create role leoclean_kb_runtime nologin" in sql.lower()
|
||||||
disable_statement = "alter role leoclean_kb_runtime\n with nologin nosuperuser"
|
disable_statement = "alter role leoclean_kb_runtime\n with nologin nosuperuser"
|
||||||
password_statement = "alter role leoclean_kb_runtime password"
|
password_statement = "\\password leoclean_kb_runtime"
|
||||||
assert sql.lower().index(disable_statement) < sql.lower().index(password_statement)
|
assert sql.lower().index(disable_statement) < sql.index(password_statement)
|
||||||
assert sql.lower().index(disable_statement) < sql.index("\\getenv runtime_password")
|
assert "\\getenv runtime_password" not in sql
|
||||||
assert sql.index("\\getenv runtime_password") < sql.lower().index(password_statement)
|
assert "password :'runtime_password'" not in sql.lower()
|
||||||
assert sql.lower().index("with nologin nosuperuser") < sql.index("\\connect teleo_kb")
|
assert sql.lower().index("with nologin nosuperuser") < sql.index("\\connect teleo_canonical")
|
||||||
assert sql.index("\\connect teleo_kb") < sql.lower().rindex("with login nosuperuser")
|
assert sql.index("\\connect teleo_canonical") < sql.lower().rindex("with login nosuperuser")
|
||||||
|
assert sql.lower().rindex("with login nosuperuser") < sql.rindex("commit;")
|
||||||
assert "leoclean_kb_runtime final login attributes are unsafe" in sql
|
assert "leoclean_kb_runtime final login attributes are unsafe" in sql
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -869,6 +940,9 @@ def _load_module(path: Path):
|
||||||
assert spec and spec.loader
|
assert spec and spec.loader
|
||||||
module = importlib.util.module_from_spec(spec)
|
module = importlib.util.module_from_spec(spec)
|
||||||
spec.loader.exec_module(module)
|
spec.loader.exec_module(module)
|
||||||
|
if path.name == "cloudsql_memory_tool.py":
|
||||||
|
module.runtime_ssl_root_cert = lambda: module.RUNTIME_SSL_ROOT_CERT
|
||||||
|
module.runtime_cloudsdk_config = lambda: module.RUNTIME_CLOUDSDK_CONFIG
|
||||||
return module
|
return module
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,33 @@
|
||||||
|
import os
|
||||||
|
import resource
|
||||||
|
import signal
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
|
||||||
|
|
||||||
|
def embedded_service_context_runner_source() -> str:
|
||||||
|
script = (REPO_ROOT / "deploy" / "sync-gcp-leoclean-runtime.sh").read_text()
|
||||||
|
marker = "remote_helpers=$(cat <<'REMOTE'\n"
|
||||||
|
start = script.index(marker) + len(marker)
|
||||||
|
end = script.index("\nREMOTE\n)", start)
|
||||||
|
helpers = script[start:end]
|
||||||
|
blocks = [part.split("\nPY", maxsplit=1)[0] for part in helpers.split("<<'PY'\n")[1:]]
|
||||||
|
return next(block for block in blocks if "def run_stopped_child(" in block)
|
||||||
|
|
||||||
|
|
||||||
|
def load_service_context_runner() -> dict[str, object]:
|
||||||
|
namespace: dict[str, object] = {"__name__": "embedded_service_context_runner_test"}
|
||||||
|
source = embedded_service_context_runner_source()
|
||||||
|
exec(compile(source, "<embedded-service-context-runner>", "exec"), namespace)
|
||||||
|
return namespace
|
||||||
|
|
||||||
|
|
||||||
def test_teleo_agent_template_supports_optional_per_agent_env_file():
|
def test_teleo_agent_template_supports_optional_per_agent_env_file():
|
||||||
unit = (REPO_ROOT / "systemd" / "teleo-agent@.service").read_text()
|
unit = (REPO_ROOT / "systemd" / "teleo-agent@.service").read_text()
|
||||||
|
|
||||||
|
|
@ -189,7 +213,7 @@ def test_gcp_runtime_sync_is_source_bound_and_scoped():
|
||||||
assert "sha256sum" in script
|
assert "sha256sum" in script
|
||||||
assert "--preflight-only" in script
|
assert "--preflight-only" in script
|
||||||
assert "--restart" in script
|
assert "--restart" in script
|
||||||
assert '/usr/bin/python3 "$tool_path" status --format json --redacted' in script
|
assert '/usr/bin/python3 -I "$tool_path" status --format json --redacted' in script
|
||||||
assert "sa-teleo-prod-vm@teleo-501523.iam.gserviceaccount.com" in script
|
assert "sa-teleo-prod-vm@teleo-501523.iam.gserviceaccount.com" in script
|
||||||
assert "169.254.169.254/computeMetadata/v1/instance/service-accounts/default/email" in script
|
assert "169.254.169.254/computeMetadata/v1/instance/service-accounts/default/email" in script
|
||||||
assert "ProxyHandler({})" in script
|
assert "ProxyHandler({})" in script
|
||||||
|
|
@ -204,7 +228,8 @@ def test_gcp_runtime_sync_is_source_bound_and_scoped():
|
||||||
wrapper_probe = script.split("run_service_wrapper_status() {", maxsplit=1)[1].split(
|
wrapper_probe = script.split("run_service_wrapper_status() {", maxsplit=1)[1].split(
|
||||||
"\n}\n\nservice_fingerprint", maxsplit=1
|
"\n}\n\nservice_fingerprint", maxsplit=1
|
||||||
)[0]
|
)[0]
|
||||||
assert 'nsenter -t "$main_pid" -m --env' in wrapper_probe
|
assert 'run_in_service_cgroup "$main_pid"' in wrapper_probe
|
||||||
|
assert 'nsenter -t "$main_pid" -m -n --env' in wrapper_probe
|
||||||
assert "/usr/bin/setpriv" in wrapper_probe
|
assert "/usr/bin/setpriv" in wrapper_probe
|
||||||
assert "--reuid=teleo" in wrapper_probe
|
assert "--reuid=teleo" in wrapper_probe
|
||||||
assert "TELEO_CLOUDSQL_" not in wrapper_probe
|
assert "TELEO_CLOUDSQL_" not in wrapper_probe
|
||||||
|
|
@ -218,15 +243,54 @@ def test_gcp_runtime_sync_is_source_bound_and_scoped():
|
||||||
assert "--property=EnvironmentFiles" in script
|
assert "--property=EnvironmentFiles" in script
|
||||||
assert "Service has an effective EnvironmentFile" in script
|
assert "Service has an effective EnvironmentFile" in script
|
||||||
assert "Service has a later drop-in" in script
|
assert "Service has a later drop-in" in script
|
||||||
|
assert "--property=PrivateNetwork" in script
|
||||||
|
assert "--property=PrivateUsers" in script
|
||||||
|
assert "NetworkNamespacePath" in script
|
||||||
|
assert "IPAddressDeny" in script
|
||||||
|
assert "IPAddressAllow" in script
|
||||||
|
assert "RestrictAddressFamilies" in script
|
||||||
|
assert "RestrictNetworkInterfaces" in script
|
||||||
|
assert "SystemCallFilter" in script
|
||||||
|
assert "AppArmorProfile" in script
|
||||||
|
assert "SELinuxContext" in script
|
||||||
|
assert '--property="$network_property"' in script
|
||||||
assert "assert_administrator_secret_denied" in script
|
assert "assert_administrator_secret_denied" in script
|
||||||
assert '"stdout_discarded": True' in script
|
assert '"stdout_discarded": True' in script
|
||||||
assert '"credential_context": "systemd_main_pid_environment"' in script
|
assert '"credential_context": "systemd_main_pid_environment"' in script
|
||||||
administrator_probe = script.split("assert_administrator_secret_denied() {", maxsplit=1)[1].split(
|
administrator_probe = script.split("assert_administrator_secret_denied() {", maxsplit=1)[1].split(
|
||||||
"\n}\n\nrun_scoped_status", maxsplit=1
|
"\n}\n\nrun_scoped_status", maxsplit=1
|
||||||
)[0]
|
)[0]
|
||||||
assert 'nsenter -t "$main_pid" -m --env' in administrator_probe
|
assert 'run_in_service_cgroup "$main_pid"' in administrator_probe
|
||||||
|
assert 'nsenter -t "$main_pid" -m -n --env' in administrator_probe
|
||||||
assert "/usr/bin/setpriv" in administrator_probe
|
assert "/usr/bin/setpriv" in administrator_probe
|
||||||
assert "env -i" not in administrator_probe
|
assert "env -i" not in administrator_probe
|
||||||
|
permission_probe = script.split("run_permission_verifier() {", maxsplit=1)[1].split(
|
||||||
|
"\n}\n\nassert_trusted_executable", maxsplit=1
|
||||||
|
)[0]
|
||||||
|
assert 'if [ "$phase" = pre ]; then' in permission_probe
|
||||||
|
assert 'run_in_service_cgroup "$main_pid"' in permission_probe
|
||||||
|
assert 'nsenter -t "$main_pid" -m -n' in permission_probe
|
||||||
|
assert "/usr/bin/env -i" in permission_probe
|
||||||
|
assert 'verification_tmp="$(sudo mktemp -d /run/' in script
|
||||||
|
assert "run_in_service_cgroup() {" in script
|
||||||
|
assert "--property=ControlGroup" in script
|
||||||
|
assert 'Path(f"/proc/{pid}/cgroup")' in script
|
||||||
|
assert 'Path("/sys/fs/cgroup").resolve(strict=True)' in script
|
||||||
|
assert 'cgroup_dir / "cgroup.procs"' in script
|
||||||
|
assert "os.WUNTRACED" in script
|
||||||
|
assert "signal.SIGSTOP" in script
|
||||||
|
assert "signal.SIGCONT" in script
|
||||||
|
assert "os.waitpid(child, 0)" in script
|
||||||
|
assert "PR_SET_PDEATHSIG" in script
|
||||||
|
assert "signal.pthread_sigmask" in script
|
||||||
|
assert "ParentInterrupted" in script
|
||||||
|
assert "kill_and_reap(child)" in script
|
||||||
|
assert "Seccomp_filters" in script
|
||||||
|
assert '"/proc/$main_pid/attr/current"' in script
|
||||||
|
assert "/proc/1/ns/user" in script
|
||||||
|
assert "resource.prlimit" in script
|
||||||
|
assert "copy_process_limits" in script
|
||||||
|
assert "configure_stopped_child" in script
|
||||||
assert 'preflight_service_before="$(service_fingerprint)"' in script
|
assert 'preflight_service_before="$(service_fingerprint)"' in script
|
||||||
assert 'assert_service_fingerprint "$preflight_service_before"' in script
|
assert 'assert_service_fingerprint "$preflight_service_before"' in script
|
||||||
assert "backup_path" in script
|
assert "backup_path" in script
|
||||||
|
|
@ -253,7 +317,7 @@ def test_gcp_runtime_sync_is_source_bound_and_scoped():
|
||||||
assert 'REMOTE_RUNTIME_BIN="${REMOTE_RUNTIME_BIN:-/usr/local/libexec/livingip/leoclean-kb}"' in script
|
assert 'REMOTE_RUNTIME_BIN="${REMOTE_RUNTIME_BIN:-/usr/local/libexec/livingip/leoclean-kb}"' in script
|
||||||
assert 'sudo install -d -o root -g root -m 0755 "$runtime_parent"' in script
|
assert 'sudo install -d -o root -g root -m 0755 "$runtime_parent"' in script
|
||||||
assert 'sudo install -d -o root -g root -m 0755 "$runtime_bin"' in script
|
assert 'sudo install -d -o root -g root -m 0755 "$runtime_bin"' in script
|
||||||
assert 'remote_tmp="$(sudo mktemp -d /var/tmp/teleo-gcp-leoclean-runtime.XXXXXX)"' in script
|
assert 'remote_tmp="$(sudo mktemp -d /run/teleo-gcp-leoclean-runtime.XXXXXX)"' in script
|
||||||
assert 'sudo install -d -o root -g root -m 0755 "$payload_dir"' in script
|
assert 'sudo install -d -o root -g root -m 0755 "$payload_dir"' in script
|
||||||
assert 'sudo install -d -o root -g root -m 0700 "$backup_dir"' in script
|
assert 'sudo install -d -o root -g root -m 0700 "$backup_dir"' in script
|
||||||
assert 'sudo install -d -o teleo -g teleo -m 0700 "$work_dir"' in script
|
assert 'sudo install -d -o teleo -g teleo -m 0700 "$work_dir"' in script
|
||||||
|
|
@ -288,7 +352,10 @@ def test_gcp_runtime_sync_is_source_bound_and_scoped():
|
||||||
assert 'for executable_dir in \\\n /usr/local/sbin \\' in script
|
assert 'for executable_dir in \\\n /usr/local/sbin \\' in script
|
||||||
assert 'sudo stat -Lc \'%U:%G\' "$executable_dir"' in script
|
assert 'sudo stat -Lc \'%U:%G\' "$executable_dir"' in script
|
||||||
assert 'sudo -n -u teleo test ! -w "$executable_dir"' in script
|
assert 'sudo -n -u teleo test ! -w "$executable_dir"' in script
|
||||||
assert 'for protected_file in /bin/bash /usr/bin/curl /usr/bin/python3' in script
|
assert (
|
||||||
|
'for protected_file in /bin/bash /usr/bin/curl /usr/bin/env /usr/bin/gcloud '
|
||||||
|
'/usr/bin/psql /usr/bin/python3 /usr/bin/setpriv' in script
|
||||||
|
)
|
||||||
assert '/usr/bin/curl -q --noproxy' in script
|
assert '/usr/bin/curl -q --noproxy' in script
|
||||||
assert 'test "$(sudo cat "$revision")" = "$SOURCE_REVISION"' in script
|
assert 'test "$(sudo cat "$revision")" = "$SOURCE_REVISION"' in script
|
||||||
assert 'assert_existing_root_directory "$existing_directory"' in script
|
assert 'assert_existing_root_directory "$existing_directory"' in script
|
||||||
|
|
@ -314,30 +381,39 @@ def test_gcp_runtime_sync_is_source_bound_and_scoped():
|
||||||
assert 'sudo install -o root -g root -m 0755 "$work_dir/' not in script
|
assert 'sudo install -o root -g root -m 0755 "$work_dir/' not in script
|
||||||
assert 'nsenter -t "$main_pid" -m -- sha256sum "$SERVICE_PROFILE_BIN/teleo-kb"' in script
|
assert 'nsenter -t "$main_pid" -m -- sha256sum "$SERVICE_PROFILE_BIN/teleo-kb"' in script
|
||||||
assert 'findmnt -T "$SERVICE_PROFILE_BIN"' in script
|
assert 'findmnt -T "$SERVICE_PROFILE_BIN"' in script
|
||||||
assert 'findmnt -M "$SERVICE_HERMES_ROOT"' in script
|
assert "findmnt -M /home/teleo" in script
|
||||||
assert 'findmnt -M "$SERVICE_PROFILE_ROOT/state"' in script
|
assert 'findmnt -M "$SERVICE_PROFILE_ROOT/state"' in script
|
||||||
assert 'findmnt -M "$SERVICE_PROFILE_ROOT/workspace"' in script
|
assert 'findmnt -M "$SERVICE_PROFILE_ROOT/workspace"' in script
|
||||||
assert "cap_sys_admin" in script
|
assert "for capability_field in CapInh CapPrm CapEff CapBnd CapAmb" in script
|
||||||
|
assert "0000000000000000" in script
|
||||||
assert '"current_tier": "T3_live_readonly"' in script
|
assert '"current_tier": "T3_live_readonly"' in script
|
||||||
assert '"status": "pass"' in script
|
assert '"status": "pass"' in script
|
||||||
assert "UnsetEnvironment=PGPASSWORD" in dropin
|
assert "UnsetEnvironment=PGPASSWORD" in dropin
|
||||||
assert "UnsetEnvironment=GOOGLE_APPLICATION_CREDENTIALS CLOUDSDK_CONFIG" in dropin
|
assert (
|
||||||
|
"UnsetEnvironment=GOOGLE_APPLICATION_CREDENTIALS CLOUDSDK_CORE_PROJECT "
|
||||||
|
"CLOUDSDK_AUTH_ACCESS_TOKEN" in dropin
|
||||||
|
)
|
||||||
assert "UnsetEnvironment=BASH_ENV ENV BASHOPTS SHELLOPTS" in dropin
|
assert "UnsetEnvironment=BASH_ENV ENV BASHOPTS SHELLOPTS" in dropin
|
||||||
assert "UnsetEnvironment=LD_PRELOAD LD_LIBRARY_PATH LD_AUDIT" in dropin
|
assert "UnsetEnvironment=LD_PRELOAD LD_LIBRARY_PATH LD_AUDIT" in dropin
|
||||||
assert "UnsetEnvironment=PYTHONHOME PYTHONPATH PYTHONSTARTUP PYTHONINSPECT" in dropin
|
assert "UnsetEnvironment=PYTHONHOME PYTHONPATH PYTHONSTARTUP PYTHONINSPECT" in dropin
|
||||||
assert "UnsetEnvironment=HTTP_PROXY HTTPS_PROXY ALL_PROXY NO_PROXY" in dropin
|
assert "UnsetEnvironment=HTTP_PROXY HTTPS_PROXY ALL_PROXY NO_PROXY" in dropin
|
||||||
assert "UnsetEnvironment=SSL_CERT_FILE SSL_CERT_DIR REQUESTS_CA_BUNDLE CURL_CA_BUNDLE SSLKEYLOGFILE" in dropin
|
assert "UnsetEnvironment=SSL_CERT_FILE SSL_CERT_DIR REQUESTS_CA_BUNDLE CURL_CA_BUNDLE SSLKEYLOGFILE" in dropin
|
||||||
|
assert (
|
||||||
|
"UnsetEnvironment=TELEO_CLOUDSQL_CREDENTIAL_MODE TELEO_KB_CLAIM_BASE_URL "
|
||||||
|
"TELEO_KB_READ_ATTEMPTS TELEO_MEMORY_INCLUDE_EXCERPTS TELEO_MEMORY_REDACT" in dropin
|
||||||
|
)
|
||||||
assert (
|
assert (
|
||||||
"BindReadOnlyPaths=/usr/local/libexec/livingip/leoclean-kb:/home/teleo/.hermes/profiles/leoclean/bin" in dropin
|
"BindReadOnlyPaths=/usr/local/libexec/livingip/leoclean-kb:/home/teleo/.hermes/profiles/leoclean/bin" in dropin
|
||||||
)
|
)
|
||||||
assert "ReadOnlyPaths=/home/teleo/.hermes" in dropin
|
assert "ReadOnlyPaths=/home/teleo" in dropin
|
||||||
assert (
|
assert (
|
||||||
"ReadWritePaths=/home/teleo/.hermes/profiles/leoclean/state "
|
"ReadWritePaths=/home/teleo/.hermes/profiles/leoclean/state "
|
||||||
"/home/teleo/.hermes/profiles/leoclean/workspace" in dropin
|
"/home/teleo/.hermes/profiles/leoclean/workspace" in dropin
|
||||||
)
|
)
|
||||||
assert "CapabilityBoundingSet=~CAP_SYS_ADMIN" in dropin
|
assert "CapabilityBoundingSet=" in dropin
|
||||||
|
assert "AmbientCapabilities=" in dropin
|
||||||
assert "RuntimeDirectory=" not in dropin
|
assert "RuntimeDirectory=" not in dropin
|
||||||
assert "Environment=CLOUDSDK_" not in dropin
|
assert "Environment=CLOUDSDK_CONFIG=/usr/local/libexec/livingip/leoclean-kb/gcloud-config" in dropin
|
||||||
assert "Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" in dropin
|
assert "Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" in dropin
|
||||||
assert "TELEO_KB_MODE=cloudsql" in dropin
|
assert "TELEO_KB_MODE=cloudsql" in dropin
|
||||||
assert "TELEO_GCP_METADATA_ONLY=1" in dropin
|
assert "TELEO_GCP_METADATA_ONLY=1" in dropin
|
||||||
|
|
@ -374,7 +450,7 @@ def test_gcp_runtime_embedded_remote_programs_are_bash_syntax_valid() -> None:
|
||||||
assert completed.returncode == 0, f"{name}: {completed.stderr}"
|
assert completed.returncode == 0, f"{name}: {completed.stderr}"
|
||||||
|
|
||||||
embedded_python = [part.split("\nPY", maxsplit=1)[0] for part in blocks["remote_helpers"].split("<<'PY'\n")[1:]]
|
embedded_python = [part.split("\nPY", maxsplit=1)[0] for part in blocks["remote_helpers"].split("<<'PY'\n")[1:]]
|
||||||
assert len(embedded_python) == 2
|
assert len(embedded_python) == 4
|
||||||
for index, body in enumerate(embedded_python):
|
for index, body in enumerate(embedded_python):
|
||||||
completed = subprocess.run(
|
completed = subprocess.run(
|
||||||
["python3", "-c", "import sys; compile(sys.stdin.read(), '<embedded>', 'exec')"],
|
["python3", "-c", "import sys; compile(sys.stdin.read(), '<embedded>', 'exec')"],
|
||||||
|
|
@ -386,6 +462,162 @@ def test_gcp_runtime_embedded_remote_programs_are_bash_syntax_valid() -> None:
|
||||||
assert completed.returncode == 0, f"embedded_python_{index}: {completed.stderr}"
|
assert completed.returncode == 0, f"embedded_python_{index}: {completed.stderr}"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("command", "expected_returncode"),
|
||||||
|
[
|
||||||
|
(["/bin/sh", "-c", "exit 0"], 0),
|
||||||
|
(["/bin/sh", "-c", "exit 7"], 7),
|
||||||
|
(["/bin/sh", "-c", "kill -TERM $$"], 128 + signal.SIGTERM),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_service_context_runner_propagates_result_and_reaps_child(
|
||||||
|
command: list[str],
|
||||||
|
expected_returncode: int,
|
||||||
|
) -> None:
|
||||||
|
run_stopped_child = load_service_context_runner()["run_stopped_child"]
|
||||||
|
moved_children: list[int] = []
|
||||||
|
|
||||||
|
result = run_stopped_child( # type: ignore[operator]
|
||||||
|
command,
|
||||||
|
"/test.service",
|
||||||
|
move_child=moved_children.append,
|
||||||
|
read_child_cgroup=lambda _pid: "/test.service",
|
||||||
|
prepare_child=lambda _parent: None,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result == expected_returncode
|
||||||
|
assert len(moved_children) == 1
|
||||||
|
with pytest.raises(ProcessLookupError):
|
||||||
|
os.kill(moved_children[0], 0)
|
||||||
|
|
||||||
|
|
||||||
|
def test_service_context_runner_reaps_stopped_child_when_move_fails() -> None:
|
||||||
|
run_stopped_child = load_service_context_runner()["run_stopped_child"]
|
||||||
|
stopped_children: list[int] = []
|
||||||
|
|
||||||
|
def fail_move(child: int) -> None:
|
||||||
|
stopped_children.append(child)
|
||||||
|
raise RuntimeError("synthetic move failure")
|
||||||
|
|
||||||
|
with pytest.raises(RuntimeError, match="synthetic move failure"):
|
||||||
|
run_stopped_child( # type: ignore[operator]
|
||||||
|
["/bin/sh", "-c", "exit 0"],
|
||||||
|
"/test.service",
|
||||||
|
move_child=fail_move,
|
||||||
|
read_child_cgroup=lambda _pid: "/test.service",
|
||||||
|
prepare_child=lambda _parent: None,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert len(stopped_children) == 1
|
||||||
|
with pytest.raises(ProcessLookupError):
|
||||||
|
os.kill(stopped_children[0], 0)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(not hasattr(resource, "prlimit"), reason="requires Linux prlimit")
|
||||||
|
def test_service_context_runner_copies_mainpid_resource_limits(tmp_path: Path) -> None:
|
||||||
|
namespace = load_service_context_runner()
|
||||||
|
run_stopped_child = namespace["run_stopped_child"]
|
||||||
|
copy_process_limits = namespace["copy_process_limits"]
|
||||||
|
limit_readback = tmp_path / "limits.txt"
|
||||||
|
child_program = (
|
||||||
|
"import resource,sys; from pathlib import Path; "
|
||||||
|
"limits=resource.getrlimit(resource.RLIMIT_NOFILE); "
|
||||||
|
"Path(sys.argv[1]).write_text(f'{limits[0]}:{limits[1]}', encoding='ascii')"
|
||||||
|
)
|
||||||
|
|
||||||
|
def lower_nofile_limit() -> None:
|
||||||
|
resource.setrlimit(resource.RLIMIT_NOFILE, (64, 64))
|
||||||
|
|
||||||
|
source = subprocess.Popen(
|
||||||
|
[sys.executable, "-c", "import time; time.sleep(30)"],
|
||||||
|
preexec_fn=lower_nofile_limit,
|
||||||
|
stdin=subprocess.DEVNULL,
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.DEVNULL,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
result = run_stopped_child( # type: ignore[operator]
|
||||||
|
[sys.executable, "-c", child_program, str(limit_readback)],
|
||||||
|
"/test.service",
|
||||||
|
move_child=lambda _child: None,
|
||||||
|
read_child_cgroup=lambda _pid: "/test.service",
|
||||||
|
prepare_child=lambda _parent: None,
|
||||||
|
configure_stopped_child=lambda child: copy_process_limits(source.pid, child), # type: ignore[operator]
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
source.terminate()
|
||||||
|
source.wait(timeout=5)
|
||||||
|
|
||||||
|
assert result == 0
|
||||||
|
assert limit_readback.read_text(encoding="ascii") == "64:64"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("signum", [signal.SIGTERM, signal.SIGHUP])
|
||||||
|
def test_service_context_runner_interruption_kills_and_reaps_probe(
|
||||||
|
tmp_path: Path,
|
||||||
|
signum: signal.Signals,
|
||||||
|
) -> None:
|
||||||
|
run_stopped_child = load_service_context_runner()["run_stopped_child"]
|
||||||
|
child_pid_path = tmp_path / f"probe-{signum.name}.pid"
|
||||||
|
child_program = (
|
||||||
|
"import os,sys,time; from pathlib import Path; "
|
||||||
|
"Path(sys.argv[1]).write_text(str(os.getpid()), encoding='ascii'); "
|
||||||
|
"time.sleep(30)"
|
||||||
|
)
|
||||||
|
runner_pid = os.fork()
|
||||||
|
if runner_pid == 0:
|
||||||
|
result = run_stopped_child( # type: ignore[operator]
|
||||||
|
[sys.executable, "-c", child_program, str(child_pid_path)],
|
||||||
|
"/test.service",
|
||||||
|
move_child=lambda _child: None,
|
||||||
|
read_child_cgroup=lambda _pid: "/test.service",
|
||||||
|
prepare_child=lambda _parent: None,
|
||||||
|
)
|
||||||
|
os._exit(result)
|
||||||
|
|
||||||
|
runner_reaped = False
|
||||||
|
probe_pid: int | None = None
|
||||||
|
try:
|
||||||
|
deadline = time.monotonic() + 5
|
||||||
|
probe_pid_text = ""
|
||||||
|
while time.monotonic() < deadline:
|
||||||
|
if child_pid_path.is_file():
|
||||||
|
probe_pid_text = child_pid_path.read_text(encoding="ascii")
|
||||||
|
if probe_pid_text.isdecimal():
|
||||||
|
break
|
||||||
|
waited, _status = os.waitpid(runner_pid, os.WNOHANG)
|
||||||
|
if waited == runner_pid:
|
||||||
|
runner_reaped = True
|
||||||
|
pytest.fail("service-context runner exited before probe start")
|
||||||
|
time.sleep(0.01)
|
||||||
|
assert probe_pid_text.isdecimal()
|
||||||
|
probe_pid = int(probe_pid_text)
|
||||||
|
|
||||||
|
os.kill(runner_pid, signum)
|
||||||
|
waited, status = os.waitpid(runner_pid, 0)
|
||||||
|
runner_reaped = True
|
||||||
|
assert waited == runner_pid
|
||||||
|
assert os.WIFSIGNALED(status)
|
||||||
|
assert os.WTERMSIG(status) == signum
|
||||||
|
with pytest.raises(ProcessLookupError):
|
||||||
|
os.kill(probe_pid, 0)
|
||||||
|
finally:
|
||||||
|
if not runner_reaped:
|
||||||
|
try:
|
||||||
|
os.kill(runner_pid, signal.SIGKILL)
|
||||||
|
except ProcessLookupError:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
os.waitpid(runner_pid, 0)
|
||||||
|
except ChildProcessError:
|
||||||
|
pass
|
||||||
|
if probe_pid is not None:
|
||||||
|
try:
|
||||||
|
os.kill(probe_pid, signal.SIGKILL)
|
||||||
|
except ProcessLookupError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def test_gcp_runtime_rejects_preexisting_symlink_before_mutation(tmp_path: Path) -> None:
|
def test_gcp_runtime_rejects_preexisting_symlink_before_mutation(tmp_path: Path) -> None:
|
||||||
script = (REPO_ROOT / "deploy" / "sync-gcp-leoclean-runtime.sh").read_text()
|
script = (REPO_ROOT / "deploy" / "sync-gcp-leoclean-runtime.sh").read_text()
|
||||||
function_start = script.index("assert_existing_root_directory() {")
|
function_start = script.index("assert_existing_root_directory() {")
|
||||||
|
|
@ -428,10 +660,43 @@ set -euo pipefail
|
||||||
SERVICE=leoclean.service
|
SERVICE=leoclean.service
|
||||||
REMOTE_DROPIN_DIR=/etc/systemd/system/leoclean.service.d
|
REMOTE_DROPIN_DIR=/etc/systemd/system/leoclean.service.d
|
||||||
DROPIN_NAME=10-cloudsql-memory.conf
|
DROPIN_NAME=10-cloudsql-memory.conf
|
||||||
|
REMOTE_RUNTIME_BIN=/usr/local/libexec/livingip/leoclean-kb
|
||||||
|
SERVICE_PROFILE_ROOT=/home/teleo/.hermes/profiles/leoclean
|
||||||
|
SERVICE_PROFILE_BIN=$SERVICE_PROFILE_ROOT/bin
|
||||||
|
assert_exact_word_set() {{
|
||||||
|
local actual="$1" expected="$2" label="$3" actual_sorted expected_sorted word
|
||||||
|
actual_sorted="$(for word in $actual; do printf '%s\\n' "${{word#-}}"; done | sort)"
|
||||||
|
expected_sorted="$(for word in $expected; do printf '%s\\n' "${{word#-}}"; done | sort)"
|
||||||
|
[ "$actual_sorted" = "$expected_sorted" ] || {{
|
||||||
|
echo "Service effective $label differs from the reviewed exact allowlist." >&2
|
||||||
|
return 1
|
||||||
|
}}
|
||||||
|
}}
|
||||||
systemctl() {{
|
systemctl() {{
|
||||||
case "$*" in
|
local argument property=
|
||||||
*--property=DropInPaths*) printf '%s\\n' "${{FAKE_DROPINS:-}}" ;;
|
for argument in "$@"; do
|
||||||
*--property=EnvironmentFiles*) printf '%s\\n' "${{FAKE_ENVIRONMENT_FILES:-}}" ;;
|
case "$argument" in --property=*) property=${{argument#--property=}} ;; esac
|
||||||
|
done
|
||||||
|
case "$property" in
|
||||||
|
DropInPaths) printf '%s\\n' "${{FAKE_DROPINS:-}}" ;;
|
||||||
|
EnvironmentFiles) printf '%s\\n' "${{FAKE_ENVIRONMENT_FILES:-}}" ;;
|
||||||
|
LoadCredential|LoadCredentialEncrypted|SetCredential|SetCredentialEncrypted|ImportCredential|ExecCondition|ExecStartPre|ExecStartPost|ExecReload|ExecStop|ExecStopPost)
|
||||||
|
local variable="FAKE_$property"
|
||||||
|
printf '%s\\n' "${{!variable:-}}"
|
||||||
|
;;
|
||||||
|
CapabilityBoundingSet|AmbientCapabilities|BindPaths|TemporaryFileSystem) printf '\\n' ;;
|
||||||
|
NoNewPrivileges) printf 'yes\\n' ;;
|
||||||
|
PrivateNetwork) printf '%s\\n' "${{FAKE_PrivateNetwork:-no}}" ;;
|
||||||
|
PrivateUsers) printf '%s\\n' "${{FAKE_PrivateUsers:-no}}" ;;
|
||||||
|
NetworkNamespacePath|IPAddressDeny|IPAddressAllow|RestrictAddressFamilies|RestrictNetworkInterfaces|SocketBindAllow|SocketBindDeny|SystemCallFilter|AppArmorProfile|SELinuxContext)
|
||||||
|
local variable="FAKE_$property"
|
||||||
|
printf '%s\\n' "${{!variable:-}}"
|
||||||
|
;;
|
||||||
|
Group|SupplementaryGroups) printf 'teleo\\n' ;;
|
||||||
|
ReadOnlyPaths) printf '%s\\n' "${{FAKE_ReadOnlyPaths:-/home/teleo}}" ;;
|
||||||
|
ReadWritePaths) printf '%s\\n' "$SERVICE_PROFILE_ROOT/state $SERVICE_PROFILE_ROOT/workspace" ;;
|
||||||
|
BindReadOnlyPaths) printf '%s\\n' "$REMOTE_RUNTIME_BIN:$SERVICE_PROFILE_BIN" ;;
|
||||||
|
InaccessiblePaths) printf '%s\\n' '/home/teleo/.config/gcloud /home/teleo/.pgpass /home/teleo/.pg_service.conf /home/teleo/.postgresql' ;;
|
||||||
*) return 90 ;;
|
*) return 90 ;;
|
||||||
esac
|
esac
|
||||||
}}
|
}}
|
||||||
|
|
@ -445,6 +710,19 @@ assert_unit_configuration
|
||||||
"FAKE_ENVIRONMENT_FILES": "",
|
"FAKE_ENVIRONMENT_FILES": "",
|
||||||
},
|
},
|
||||||
{"FAKE_DROPINS": reviewed, "FAKE_ENVIRONMENT_FILES": "/etc/leoclean-runtime.env"},
|
{"FAKE_DROPINS": reviewed, "FAKE_ENVIRONMENT_FILES": "/etc/leoclean-runtime.env"},
|
||||||
|
{"FAKE_DROPINS": reviewed, "FAKE_ExecStartPost": "/usr/local/bin/conflicting-hook"},
|
||||||
|
{"FAKE_DROPINS": reviewed, "FAKE_ReadOnlyPaths": "/home/teleo /tmp"},
|
||||||
|
{"FAKE_DROPINS": reviewed, "FAKE_PrivateNetwork": "yes"},
|
||||||
|
{"FAKE_DROPINS": reviewed, "FAKE_PrivateUsers": "yes"},
|
||||||
|
{"FAKE_DROPINS": reviewed, "FAKE_NetworkNamespacePath": "/run/netns/restricted"},
|
||||||
|
{"FAKE_DROPINS": reviewed, "FAKE_IPAddressDeny": "any"},
|
||||||
|
{"FAKE_DROPINS": reviewed, "FAKE_IPAddressAllow": "10.61.0.4/32"},
|
||||||
|
{"FAKE_DROPINS": reviewed, "FAKE_RestrictAddressFamilies": "AF_UNIX"},
|
||||||
|
{"FAKE_DROPINS": reviewed, "FAKE_RestrictNetworkInterfaces": "~ens4"},
|
||||||
|
{"FAKE_DROPINS": reviewed, "FAKE_SocketBindDeny": "any"},
|
||||||
|
{"FAKE_DROPINS": reviewed, "FAKE_SystemCallFilter": "@system-service ~@network-io"},
|
||||||
|
{"FAKE_DROPINS": reviewed, "FAKE_AppArmorProfile": "restricted-profile"},
|
||||||
|
{"FAKE_DROPINS": reviewed, "FAKE_SELinuxContext": "system_u:system_r:restricted_t:s0"},
|
||||||
)
|
)
|
||||||
for environment in cases:
|
for environment in cases:
|
||||||
completed = subprocess.run(
|
completed = subprocess.run(
|
||||||
|
|
@ -462,12 +740,53 @@ assert_unit_configuration
|
||||||
input=harness,
|
input=harness,
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
check=False,
|
check=False,
|
||||||
env={"FAKE_DROPINS": reviewed, "FAKE_ENVIRONMENT_FILES": ""},
|
env={"FAKE_DROPINS": reviewed},
|
||||||
text=True,
|
text=True,
|
||||||
)
|
)
|
||||||
assert allowed.returncode == 0, allowed.stderr
|
assert allowed.returncode == 0, allowed.stderr
|
||||||
|
|
||||||
|
|
||||||
|
def test_gcp_runtime_effective_execstart_override_is_rejected_before_process_probe() -> None:
|
||||||
|
script = (REPO_ROOT / "deploy" / "sync-gcp-leoclean-runtime.sh").read_text()
|
||||||
|
function_start = script.index("assert_service_running() {")
|
||||||
|
function_end = script.index("\n\nassert_unit_configuration() {", function_start)
|
||||||
|
service_function = script[function_start:function_end]
|
||||||
|
harness = f"""
|
||||||
|
set -euo pipefail
|
||||||
|
SERVICE=leoclean.service
|
||||||
|
EXPECTED_HERMES_EXECUTABLE=/home/teleo/.hermes/hermes-agent/hermes.py
|
||||||
|
EXPECTED_HERMES_PYTHON=/home/teleo/.hermes/hermes-agent/venv/bin/python
|
||||||
|
systemctl() {{
|
||||||
|
local argument property=
|
||||||
|
for argument in "$@"; do
|
||||||
|
case "$argument" in --property=*) property=${{argument#--property=}} ;; esac
|
||||||
|
done
|
||||||
|
case "$property" in
|
||||||
|
ActiveState) printf 'active\\n' ;;
|
||||||
|
SubState) printf 'running\\n' ;;
|
||||||
|
MainPID) printf '1234\\n' ;;
|
||||||
|
User) printf 'teleo\\n' ;;
|
||||||
|
ExecStart) printf '{{ path=/usr/bin/python3 ; argv[]=/usr/bin/python3 /tmp/conflicting.py ; }}\\n' ;;
|
||||||
|
*) return 90 ;;
|
||||||
|
esac
|
||||||
|
}}
|
||||||
|
ps() {{ printf 'teleo\\n'; }}
|
||||||
|
{service_function}
|
||||||
|
assert_service_running false
|
||||||
|
"""
|
||||||
|
|
||||||
|
completed = subprocess.run(
|
||||||
|
["bash", "-s"],
|
||||||
|
input=harness,
|
||||||
|
capture_output=True,
|
||||||
|
check=False,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert completed.returncode != 0
|
||||||
|
assert "effective ExecStart is not the reviewed" in completed.stderr
|
||||||
|
|
||||||
|
|
||||||
def test_gcp_runtime_absence_rollback_removes_every_created_directory(tmp_path: Path) -> None:
|
def test_gcp_runtime_absence_rollback_removes_every_created_directory(tmp_path: Path) -> None:
|
||||||
script = (REPO_ROOT / "deploy" / "sync-gcp-leoclean-runtime.sh").read_text()
|
script = (REPO_ROOT / "deploy" / "sync-gcp-leoclean-runtime.sh").read_text()
|
||||||
function_start = script.index("backup_directory_state() {")
|
function_start = script.index("backup_directory_state() {")
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue