Merge current main into PR 219
This commit is contained in:
commit
699edb935f
6 changed files with 118 additions and 13 deletions
10
.github/workflows/gcp-artifact.yml
vendored
10
.github/workflows/gcp-artifact.yml
vendored
|
|
@ -1,9 +1,6 @@
|
||||||
name: gcp-artifact
|
name: gcp-artifact
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
|
|
@ -28,6 +25,13 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
steps:
|
steps:
|
||||||
|
- name: Require manual main dispatch
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
test "${GITHUB_EVENT_NAME}" = "workflow_dispatch"
|
||||||
|
test "${GITHUB_REF}" = "refs/heads/main"
|
||||||
|
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- id: auth
|
- id: auth
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ the current readiness run passes.
|
||||||
- `roles/storage.objectViewer`
|
- `roles/storage.objectViewer`
|
||||||
- GitHub Actions can publish Artifact Registry images through Workload Identity Federation:
|
- GitHub Actions can publish Artifact Registry images through Workload Identity Federation:
|
||||||
- workflow: `.github/workflows/gcp-artifact.yml`
|
- workflow: `.github/workflows/gcp-artifact.yml`
|
||||||
|
- trigger: explicit `workflow_dispatch` only; ordinary branch pushes never publish
|
||||||
- provider: `projects/785938879453/locations/global/workloadIdentityPools/github-actions/providers/living-ip-github`
|
- provider: `projects/785938879453/locations/global/workloadIdentityPools/github-actions/providers/living-ip-github`
|
||||||
- service account: `sa-artifact-builder@teleo-501523.iam.gserviceaccount.com`
|
- service account: `sa-artifact-builder@teleo-501523.iam.gserviceaccount.com`
|
||||||
- repository scope: `living-ip/teleo-infrastructure`
|
- repository scope: `living-ip/teleo-infrastructure`
|
||||||
|
|
@ -53,13 +54,31 @@ the current readiness run passes.
|
||||||
|
|
||||||
## How To Build
|
## How To Build
|
||||||
|
|
||||||
Automatic Artifact Registry publishing runs on pushes to `main` through GitHub Actions. To run the same lane manually from GitHub:
|
The legacy `teleo-pipeline-gcp-staging` image is never published by an ordinary
|
||||||
|
push to `main`. Publishing it is a live Artifact Registry mutation and requires
|
||||||
|
an explicit manual dispatch from the reviewed `main` revision:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
gh workflow run gcp-artifact.yml --repo living-ip/teleo-infrastructure --ref main
|
gh workflow run gcp-artifact.yml --repo living-ip/teleo-infrastructure --ref main
|
||||||
```
|
```
|
||||||
|
|
||||||
The workflow authenticates to GCP with Workload Identity Federation, builds `Dockerfile.gcp-staging`, runs the image smoke test, pushes the image, and uploads `gcp-artifact-image.txt` as a run artifact.
|
The workflow authenticates to GCP with Workload Identity Federation, builds
|
||||||
|
`Dockerfile.gcp-staging`, runs the image smoke test, pushes the image, and
|
||||||
|
uploads `gcp-artifact-image.txt` as a run artifact. This is the legacy Teleo
|
||||||
|
pipeline image path; it does not publish the separately reviewed
|
||||||
|
`leoclean-nosend-staging` artifact.
|
||||||
|
|
||||||
|
The first job step fails before checkout or cloud authentication unless the
|
||||||
|
event is `workflow_dispatch` and the selected ref is `refs/heads/main`. This is
|
||||||
|
a repository-owned execution guard. The current Workload Identity Federation
|
||||||
|
condition remains repository-scoped rather than IAM ref-scoped; binding the
|
||||||
|
principal to `main` in IAM is a separate reviewed live-GCP hardening decision.
|
||||||
|
|
||||||
|
The manual-only governance change does not attest or repair that legacy image.
|
||||||
|
The last run that reached a runner (`29810180369`) failed before publication
|
||||||
|
because `Dockerfile.gcp-staging` did not copy the packaged
|
||||||
|
`observatory_read_adapter` directory. Do not dispatch the legacy publisher
|
||||||
|
until a separate reviewed repair or retirement decision closes that defect.
|
||||||
|
|
||||||
For a read-only GCP posture probe through the same Workload Identity path:
|
For a read-only GCP posture probe through the same Workload Identity path:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,10 +19,15 @@ select 'create role leoclean_kb_stage_owner nologin nosuperuser nocreatedb nocre
|
||||||
where not exists (select 1 from pg_catalog.pg_roles where rolname = 'leoclean_kb_stage_owner')
|
where not exists (select 1 from pg_catalog.pg_roles where rolname = 'leoclean_kb_stage_owner')
|
||||||
\gexec
|
\gexec
|
||||||
|
|
||||||
|
-- Cloud SQL's postgres administrator has CREATEROLE but is deliberately not a
|
||||||
|
-- true PostgreSQL superuser. It may not spell SUPERUSER, REPLICATION, or
|
||||||
|
-- BYPASSRLS attributes in ALTER ROLE, even when setting their safe false form.
|
||||||
|
-- CREATE ROLE above establishes those attributes as false; the verification
|
||||||
|
-- blocks below fail closed if an existing role ever carries any of them.
|
||||||
alter role leoclean_kb_runtime
|
alter role leoclean_kb_runtime
|
||||||
with nologin nosuperuser nocreatedb nocreaterole noinherit noreplication nobypassrls connection limit 8;
|
with nologin nocreatedb nocreaterole noinherit connection limit 8;
|
||||||
alter role leoclean_kb_stage_owner
|
alter role leoclean_kb_stage_owner
|
||||||
with nologin nosuperuser nocreatedb nocreaterole noinherit noreplication nobypassrls connection limit -1
|
with nologin nocreatedb nocreaterole noinherit connection limit -1
|
||||||
password null;
|
password null;
|
||||||
|
|
||||||
-- Snapshot every non-superuser principal connected to either scoped role before
|
-- Snapshot every non-superuser principal connected to either scoped role before
|
||||||
|
|
@ -1432,7 +1437,7 @@ $verification$;
|
||||||
-- canonical privilege, large-object, and topology assertion. Any failure rolls
|
-- canonical privilege, large-object, and topology assertion. Any failure rolls
|
||||||
-- back the complete privilege migration and leaves the role NOLOGIN.
|
-- back the complete privilege migration and leaves the role NOLOGIN.
|
||||||
alter role leoclean_kb_runtime
|
alter role leoclean_kb_runtime
|
||||||
with login nosuperuser nocreatedb nocreaterole noinherit noreplication nobypassrls connection limit 8;
|
with login nocreatedb nocreaterole noinherit connection limit 8;
|
||||||
|
|
||||||
do $login_verification$
|
do $login_verification$
|
||||||
begin
|
begin
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,31 @@
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
WORKFLOW = Path(".github/workflows/gcp-artifact.yml")
|
||||||
|
|
||||||
|
|
||||||
|
def test_gcp_artifact_workflow_is_manual_only() -> None:
|
||||||
|
workflow = yaml.load(WORKFLOW.read_text(), Loader=yaml.BaseLoader)
|
||||||
|
|
||||||
|
assert set(workflow["on"]) == {"workflow_dispatch"}
|
||||||
|
assert "push" not in workflow["on"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_gcp_artifact_workflow_requires_main_before_authentication() -> None:
|
||||||
|
workflow = WORKFLOW.read_text()
|
||||||
|
|
||||||
|
guard = workflow.index("Require manual main dispatch")
|
||||||
|
checkout = workflow.index("actions/checkout@v4")
|
||||||
|
auth = workflow.index("google-github-actions/auth@v2")
|
||||||
|
|
||||||
|
assert 'test "${GITHUB_EVENT_NAME}" = "workflow_dispatch"' in workflow
|
||||||
|
assert 'test "${GITHUB_REF}" = "refs/heads/main"' in workflow
|
||||||
|
assert guard < checkout < auth
|
||||||
|
|
||||||
|
|
||||||
def test_gcp_artifact_workflow_retains_immutable_image_digest() -> None:
|
def test_gcp_artifact_workflow_retains_immutable_image_digest() -> None:
|
||||||
workflow = Path(".github/workflows/gcp-artifact.yml").read_text()
|
workflow = WORKFLOW.read_text()
|
||||||
|
|
||||||
assert "gcloud artifacts docker images describe" in workflow
|
assert "gcloud artifacts docker images describe" in workflow
|
||||||
assert "Image tag already exists; reusing immutable Artifact Registry image." in workflow
|
assert "Image tag already exists; reusing immutable Artifact Registry image." in workflow
|
||||||
|
|
|
||||||
|
|
@ -230,6 +230,47 @@ def _provision(container: str, remote_sql_path: str) -> subprocess.CompletedProc
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _assert_cloudsql_admin_attribute_compatibility(container: str) -> None:
|
||||||
|
probe_role = "cloudsql_admin_probe"
|
||||||
|
target_role = "cloudsql_attribute_target"
|
||||||
|
_require_success(
|
||||||
|
_psql(
|
||||||
|
container,
|
||||||
|
f"""
|
||||||
|
create role {probe_role}
|
||||||
|
login createrole createdb nosuperuser noreplication nobypassrls;
|
||||||
|
create role {target_role}
|
||||||
|
nologin nosuperuser nocreatedb nocreaterole noinherit noreplication nobypassrls;
|
||||||
|
grant {target_role} to {probe_role} with admin option;
|
||||||
|
""",
|
||||||
|
),
|
||||||
|
"Cloud SQL administrator attribute fixture",
|
||||||
|
)
|
||||||
|
|
||||||
|
_require_success(
|
||||||
|
_psql(
|
||||||
|
container,
|
||||||
|
f"""
|
||||||
|
alter role {target_role}
|
||||||
|
with nologin nocreatedb nocreaterole noinherit connection limit 8;
|
||||||
|
alter role {target_role}
|
||||||
|
with login nocreatedb nocreaterole noinherit connection limit 8;
|
||||||
|
""",
|
||||||
|
role=probe_role,
|
||||||
|
),
|
||||||
|
"Cloud SQL-compatible scoped attribute changes",
|
||||||
|
)
|
||||||
|
|
||||||
|
for forbidden_attribute in ("nosuperuser", "noreplication", "nobypassrls"):
|
||||||
|
denied = _psql(
|
||||||
|
container,
|
||||||
|
f"alter role {target_role} with {forbidden_attribute};",
|
||||||
|
role=probe_role,
|
||||||
|
)
|
||||||
|
assert denied.returncode != 0
|
||||||
|
assert re.search(r"(?:ERROR|FATAL):\s+42501:", denied.stderr), denied.stderr
|
||||||
|
|
||||||
|
|
||||||
def _read_table_rows_state(container: str) -> dict[str, dict[str, object]]:
|
def _read_table_rows_state(container: str) -> dict[str, dict[str, object]]:
|
||||||
table_output = _require_success(
|
table_output = _require_success(
|
||||||
_psql(
|
_psql(
|
||||||
|
|
@ -720,6 +761,7 @@ def test_digest_pinned_network_disabled_postgres_16_14_permission_contract(tmp_p
|
||||||
|
|
||||||
_require_success(_psql(container, "create database teleo_kb;"), "legacy database fixture")
|
_require_success(_psql(container, "create database teleo_kb;"), "legacy database fixture")
|
||||||
_require_success(_psql(container, _bootstrap_sql()), "permission fixture bootstrap")
|
_require_success(_psql(container, _bootstrap_sql()), "permission fixture bootstrap")
|
||||||
|
_assert_cloudsql_admin_attribute_compatibility(container)
|
||||||
_require_success(
|
_require_success(
|
||||||
_run(["docker", "cp", str(ROLE_SQL), f"{container}:/tmp/gcp_leoclean_runtime_role.sql"]),
|
_run(["docker", "cp", str(ROLE_SQL), f"{container}:/tmp/gcp_leoclean_runtime_role.sql"]),
|
||||||
"role SQL copy",
|
"role SQL copy",
|
||||||
|
|
|
||||||
|
|
@ -1000,14 +1000,26 @@ def test_gcp_runtime_role_uses_one_narrow_staging_function() -> None:
|
||||||
assert "grant update" not in sql.lower()
|
assert "grant update" not in sql.lower()
|
||||||
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 nocreatedb"
|
||||||
|
enable_statement = "alter role leoclean_kb_runtime\n with login nocreatedb"
|
||||||
password_statement = "\\password leoclean_kb_runtime"
|
password_statement = "\\password leoclean_kb_runtime"
|
||||||
assert sql.lower().index(disable_statement) < sql.index(password_statement)
|
assert sql.lower().index(disable_statement) < sql.index(password_statement)
|
||||||
assert "\\getenv runtime_password" not in sql
|
assert "\\getenv runtime_password" not in sql
|
||||||
assert "password :'runtime_password'" not in sql.lower()
|
assert "password :'runtime_password'" not in sql.lower()
|
||||||
assert sql.lower().index("with nologin nosuperuser") < sql.index("\\connect teleo_canonical")
|
assert sql.lower().index(disable_statement) < sql.index("\\connect teleo_canonical")
|
||||||
assert sql.index("\\connect teleo_canonical") < sql.lower().rindex("with login nosuperuser")
|
assert sql.index("\\connect teleo_canonical") < sql.lower().rindex(enable_statement)
|
||||||
assert sql.lower().rindex("with login nosuperuser") < sql.rindex("commit;")
|
assert sql.lower().rindex(enable_statement) < sql.rindex("commit;")
|
||||||
|
alter_statements = re.findall(r"alter\s+role\s+[^;]+;", sql, re.IGNORECASE | re.DOTALL)
|
||||||
|
scoped_attribute_statements = [
|
||||||
|
statement
|
||||||
|
for statement in alter_statements
|
||||||
|
if "leoclean_kb_runtime" in statement or "leoclean_kb_stage_owner" in statement
|
||||||
|
]
|
||||||
|
assert scoped_attribute_statements
|
||||||
|
for statement in scoped_attribute_statements:
|
||||||
|
assert "nosuperuser" not in statement.lower()
|
||||||
|
assert "noreplication" not in statement.lower()
|
||||||
|
assert "nobypassrls" not in statement.lower()
|
||||||
assert "leoclean_kb_runtime final login attributes are unsafe" in sql
|
assert "leoclean_kb_runtime final login attributes are unsafe" in sql
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue