From 37d2158428675afebb3d644400256fee9c6f8964 Mon Sep 17 00:00:00 2001 From: Edwin Jonathan Date: Sat, 7 Nov 2026 09:47:13 -0500 Subject: [PATCH 01/26] Add fail-closed check for public SSH and Kubernetes API ingress --- scripts/check_infrastructure_safety.py | 33 ++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 scripts/check_infrastructure_safety.py diff --git a/scripts/check_infrastructure_safety.py b/scripts/check_infrastructure_safety.py new file mode 100644 index 0000000..13c9239 --- /dev/null +++ b/scripts/check_infrastructure_safety.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +"""Fail-closed checks for known dangerous ARCHNET infrastructure defaults.""" +from pathlib import Path +import re +import sys + +ROOT = Path(__file__).resolve().parents[1] +TERRAFORM = ROOT / "terraform" / "main.tf" + +def check(): + failures = [] + if not TERRAFORM.is_file(): + return ["Missing terraform/main.tf"] + source = TERRAFORM.read_text(encoding="utf-8") + # Inspect ingress blocks, not egress rules. This is a conservative guard, + # not a complete Terraform parser or a replacement for plan/policy checks. + ingress_blocks = re.findall(r"\bingress\s*\{([^{}]*)\}", source, re.S) + if not ingress_blocks: + failures.append("No ingress blocks found; review Terraform manually") + for block in ingress_blocks: + if re.search(r'cidr_blocks\s*=\s*\[\s*"0\.0\.0\.0/0"\s*\]', block): + ports = re.findall(r'(?:from_port|to_port)\s*=\s*(\d+)', block) + if any(p in {"22", "6443"} for p in ports): + failures.append("Public SSH or Kubernetes API ingress detected") + return failures + +if __name__ == "__main__": + errors = check() + for error in errors: + print("ERROR:", error, file=sys.stderr) + if errors: + sys.exit(1) + print("Known public control-plane ingress checks passed") From 9396b564d9d93ff73edb85438111f871513030f0 Mon Sep 17 00:00:00 2001 From: Edwin Jonathan Date: Sat, 7 Nov 2026 09:47:17 -0500 Subject: [PATCH 02/26] Remove unrestricted SSH and Kubernetes API ingress --- terraform/main.tf | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/terraform/main.tf b/terraform/main.tf index 974a29b..2f6e407 100644 --- a/terraform/main.tf +++ b/terraform/main.tf @@ -63,13 +63,6 @@ resource "aws_security_group" "archnet" { name = "archnet-sg" description = "Archnet security group" - ingress { - from_port = 22 - to_port = 22 - protocol = "tcp" - cidr_blocks = ["0.0.0.0/0"] - } - ingress { from_port = 80 to_port = 80 @@ -84,13 +77,6 @@ resource "aws_security_group" "archnet" { cidr_blocks = ["0.0.0.0/0"] } - ingress { - from_port = 6443 - to_port = 6443 - protocol = "tcp" - cidr_blocks = ["0.0.0.0/0"] - } - egress { from_port = 0 to_port = 0 From 2a2e6a2cea6e349f35ac666b590faf91a7740a96 Mon Sep 17 00:00:00 2001 From: Edwin Jonathan Date: Sat, 7 Nov 2026 09:47:24 -0500 Subject: [PATCH 03/26] Make YAML, directory and critical security checks fail closed --- .github/workflows/ci.yml | 55 ++++++++++++++++++++++++++++------------ 1 file changed, 39 insertions(+), 16 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c4e1b3d..92adeba 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,4 @@ -name: Archnet CI/CD +name: Archnet validation on: push: @@ -6,38 +6,61 @@ on: pull_request: branches: [main] +permissions: + contents: read + jobs: validate: runs-on: ubuntu-latest + timeout-minutes: 10 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - - name: Validate YAML files + - name: Validate YAML syntax + shell: bash run: | - find . -name "*.yml" -o -name "*.yaml" | head -20 - echo "YAML structure validated" + set -euo pipefail + python -m pip install --disable-pip-version-check PyYAML==6.0.2 + python - <<'PY' + from pathlib import Path + import yaml + paths = sorted([*Path('.').rglob('*.yaml'), *Path('.').rglob('*.yml')]) + if not paths: + raise SystemExit('No YAML files found') + for path in paths: + with path.open(encoding='utf-8') as stream: + list(yaml.safe_load_all(stream)) + print('Parsed:', path) + PY - - name: Validate Shell Scripts + - name: Validate shell syntax + shell: bash run: | - find . -name "*.sh" -exec bash -n {} \; - echo "Shell scripts OK" + set -euo pipefail + while IFS= read -r -d '' file; do + bash -n "$file" + done < <(find . -type f -name '*.sh' -print0) - - name: Check Project Structure + - name: Validate required directories + shell: bash run: | - ls -la - echo "terraform/ argocd/ observability/ apps/ docs/ k3s/" | tr ' ' '\n' | while read dir; do - [ -d "$dir" ] && echo "OK: $dir exists" || echo "MISSING: $dir" + set -euo pipefail + for dir in terraform argocd observability apps docs k3s; do + test -d "$dir" || { echo "Missing: $dir" >&2; exit 1; } done + - name: Deny public SSH and Kubernetes API ingress + run: python scripts/check_infrastructure_safety.py + security: runs-on: ubuntu-latest + timeout-minutes: 10 steps: - - uses: actions/checkout@v3 - - - name: Run Trivy + - uses: actions/checkout@v4 + - name: Scan filesystem with Trivy uses: aquasecurity/trivy-action@master with: scan-type: fs scan-ref: . severity: CRITICAL - exit-code: 0 + exit-code: '1' From 5aadb01642abfedb89db7b545c21f7805458c3ac Mon Sep 17 00:00:00 2001 From: Edwin Jonathan Date: Sat, 7 Nov 2026 09:47:33 -0500 Subject: [PATCH 04/26] Document deployment stop conditions and low-cost architecture --- docs/DEPLOYMENT_SAFETY.md | 42 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 docs/DEPLOYMENT_SAFETY.md diff --git a/docs/DEPLOYMENT_SAFETY.md b/docs/DEPLOYMENT_SAFETY.md new file mode 100644 index 0000000..b714063 --- /dev/null +++ b/docs/DEPLOYMENT_SAFETY.md @@ -0,0 +1,42 @@ +# Deployment safety and cost boundary + +Status: **not validated for production use**. Do not apply the current Terraform to customer infrastructure. + +## Immediate security controls + +The initial AWS configuration exposed SSH (22/TCP) and the Kubernetes API +(6443/TCP) to the public internet. This branch removes both ingress rules. +The cluster must not be assumed remotely manageable after this change. +A separate, authenticated administrative access design and recovery procedure +must be reviewed before deployment. + +The existing public HTTP and HTTPS ingress rules also require an application +threat model, TLS configuration and verification before deployment. + +The static guard in `scripts/check_infrastructure_safety.py` detects only +simple literal Terraform ingress blocks. It is not a complete security +policy engine and does not analyze modules, dynamic expressions or actual +cloud state. Review `terraform plan` and cloud permissions independently. + +## Budget: maximum $10/month + +Default: no continuously running cloud infrastructure. Use local disposable +environments and short-lived CI tests. Never provision AWS resources without +an independently checked estimate including compute, EBS, IPv4, data transfer, +monitoring, snapshots and tax. Use account budgets and alerts; alerts are not +hard spending limits. + +No chaos experiments against customer infrastructure. Tests must be isolated, +authorized, bounded, and followed by recovery verification. + +## Required before any production-readiness claim + +- Verified installation from a clean environment by a second operator +- Locked and reviewed dependency versions and supply-chain scanning +- Working deployment, rollback and disaster recovery runbooks +- Access control and secret management review +- Measured backup restoration, SLOs and failure behavior +- Reviewed Terraform plan and complete cost estimate +- Passing CI on the exact commit being released + +A successful static check or CI run does not establish production readiness. From a6c0e230d4e76d6b5be2551a6ae1b3d58b85b303 Mon Sep 17 00:00:00 2001 From: Edwin Jonathan Date: Sat, 7 Nov 2026 09:49:33 -0500 Subject: [PATCH 05/26] Default cloud instance to disabled and remove broken bootstrap reference --- terraform/main.tf | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/terraform/main.tf b/terraform/main.tf index 2f6e407..86b4563 100644 --- a/terraform/main.tf +++ b/terraform/main.tf @@ -88,6 +88,7 @@ resource "aws_security_group" "archnet" { } resource "aws_instance" "k3s" { + count = var.enable_cloud_deployment ? 1 : 0 ami = data.aws_ami.ubuntu.id instance_type = var.instance_type subnet_id = aws_subnet.public.id @@ -100,6 +101,8 @@ resource "aws_instance" "k3s" { delete_on_termination = true } - user_data = base64encode(file("${path.module}/../k3s/install.sh")) + # No bootstrap is configured: the previous referenced script was absent. + # Cloud provisioning is disabled until an audited bootstrap and access path exist. + user_data = null tags = { Name = "archnet-k3s-master" } } From 58fa0be8da89f81cd72c007077209967a2634559 Mon Sep 17 00:00:00 2001 From: Edwin Jonathan Date: Sat, 7 Nov 2026 09:49:38 -0500 Subject: [PATCH 06/26] Add explicit fail-closed cloud deployment lock --- terraform/variables.tf | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/terraform/variables.tf b/terraform/variables.tf index 1591354..8321398 100644 --- a/terraform/variables.tf +++ b/terraform/variables.tf @@ -10,3 +10,13 @@ variable "key_name" { description = "EC2 key pair name (must exist in AWS)" type = string } + +variable "enable_cloud_deployment" { + description = "Safety lock: cloud deployment is disabled until bootstrap, access and cost gates are implemented." + type = bool + default = false + validation { + condition = var.enable_cloud_deployment == false + error_message = "Cloud deployment is intentionally disabled pending audited bootstrap and cost review." + } +} From bca85bcb44336826c476f58d0ec6bc74703ada26 Mon Sep 17 00:00:00 2001 From: Edwin Jonathan Date: Sat, 7 Nov 2026 09:49:41 -0500 Subject: [PATCH 07/26] Remove misleading SSH instructions and handle disabled instance --- terraform/outputs.tf | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/terraform/outputs.tf b/terraform/outputs.tf index fadbf7c..8e73a97 100644 --- a/terraform/outputs.tf +++ b/terraform/outputs.tf @@ -1,9 +1,4 @@ output "k3s_public_ip" { - value = aws_instance.k3s.public_ip - description = "Public IP of k3s master" -} - -output "ssh_command" { - value = "ssh -i ~/.ssh/YOUR_KEY.pem ubuntu@${aws_instance.k3s.public_ip}" - description = "SSH command" + value = try(aws_instance.k3s[0].public_ip, null) + description = "Null while cloud deployment is disabled" } From feb33c14a6282739dc515f2b03970316e5625749 Mon Sep 17 00:00:00 2001 From: Edwin Jonathan Date: Sat, 7 Nov 2026 09:49:49 -0500 Subject: [PATCH 08/26] Disable automated GitOps synchronization and pruning --- argocd/application.yaml | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/argocd/application.yaml b/argocd/application.yaml index 90b5b3f..a16cf61 100644 --- a/argocd/application.yaml +++ b/argocd/application.yaml @@ -1,3 +1,5 @@ +# Reference only: not applied by default. +# Replace the repository and revision with a reviewed, immutable release before use. apiVersion: argoproj.io/v1alpha1 kind: Application metadata: @@ -6,15 +8,12 @@ metadata: spec: project: default source: - repoURL: https://github.com/YOUR_USERNAME/archnet.git - targetRevision: HEAD + repoURL: https://github.com/EdwinJdevops/ARCHNET.git + targetRevision: main path: apps destination: server: https://kubernetes.default.svc namespace: default syncPolicy: - automated: - prune: true - selfHeal: true syncOptions: - - CreateNamespace=true + - CreateNamespace=false From a5609c2c8f2a8934ee761fcbe0260d9de13d53ce Mon Sep 17 00:00:00 2001 From: Edwin Jonathan Date: Sat, 7 Nov 2026 09:49:56 -0500 Subject: [PATCH 09/26] Replace unsupported production claims with verified experimental status --- README.md | 93 ++++++++++++------------------------------------------- 1 file changed, 19 insertions(+), 74 deletions(-) diff --git a/README.md b/README.md index 4762d34..8e87cf1 100644 --- a/README.md +++ b/README.md @@ -1,87 +1,32 @@ -# Archnet: Zero-Trust Internal Developer Platform +# ARCHNET — Internal Developer Platform research -![CI/CD](https://github.com/EdwinJdevops/ARCHNET/actions/workflows/ci.yml/badge.svg) -![License](https://img.shields.io/badge/license-MIT-blue) -![Stack](https://img.shields.io/badge/stack-k3s%20%7C%20ArgoCD%20%7C%20Prometheus-brightgreen) +**Status: experimental / not production-ready.** This repository is undergoing a source, deployment and operational safety audit. No customer production use is authorized or validated. -> A production-grade Internal Developer Platform built by a self-taught DevOps Engineer from Nigeria. +## Current scope -## Problem It Solves -Most teams deploy to Kubernetes manually — no automation, no observability, no drift detection. Archnet automates the entire platform lifecycle. +The repository contains Terraform for an AWS VPC and EC2 instance, an Argo CD Application reference, and CI checks. The original README described additional security, observability and automation features that have **not been independently verified** in this audit. Do not interpret the architecture as an operational deployment. -## Architecture -Developer → Git Push -→ GitHub Actions (CI) -→ Docker Registry -→ ArgoCD (GitOps) -→ k3s Cluster -→ Prometheus + Grafana -→ AlertManager → Slack +## Deployment safety -## Tech Stack -| Tool | Purpose | -|------|---------| -| k3s | Lightweight production Kubernetes | -| ArgoCD | GitOps deployments + drift detection | -| Sealed-Secrets | Zero-trust secret management | -| Prometheus | Metrics collection | -| Grafana | Dashboards + alerting | -| Loki | Log aggregation | -| GitHub Actions | CI/CD pipeline | -| Terraform | AWS infrastructure as code | +**Cloud instance creation is disabled by default and explicitly locked.** The earlier bootstrap script referenced by Terraform was missing; it has not been replaced with an unreviewed remote installer. The default instance size was `t3.medium` and must not be provisioned without a complete cost assessment. -## Key Features -- Self-healing deployments (ArgoCD drift detection) -- Secrets encrypted at rest in Git -- Full observability stack (metrics + logs + alerts) -- RBAC audit logging -- Zero-trust network policies -- Single-command infrastructure provisioning +Do not run `terraform apply` expecting a functioning k3s cluster. The current infrastructure does not implement a secure administrative access path or a validated cluster bootstrap. -## Quick Start -```bash -# 1. Clone -git clone https://github.com/EdwinJdevops/ARCHNET -cd ARCHNET +See [deployment safety](docs/DEPLOYMENT_SAFETY.md). -# 2. Provision AWS infrastructure -cd terraform -terraform init -terraform plan -terraform apply +## Continuous integration -# 3. Access ArgoCD -kubectl port-forward svc/argocd-server -n argocd 8080:443 +The CI workflow parses YAML, checks shell syntax and required directories, rejects known public SSH/Kubernetes API ingress rules, and runs a critical-severity filesystem scan. These are limited static checks, not evidence of a working deployment. -# 4. Access Grafana -kubectl port-forward svc/grafana -n monitoring 3000:3000 -``` +## Planned operator acceptance criteria -## Project Structure -archnet/ -├── .github/workflows/ # CI/CD pipeline -├── terraform/ # AWS infrastructure -├── k3s/ # Cluster setup -├── argocd/ # GitOps manifests -├── observability/ # Prometheus, Grafana, Loki -├── apps/ # Application deployments -└── docs/ # Architecture + decisions +1. A second engineer can install and recover a disposable environment from written instructions. +2. Access controls, secrets and dependency provenance are reviewed. +3. Backups are restored successfully under test. +4. Release and rollback paths are reproducible. +5. Reliability objectives and chaos-test results are measured and published. +6. No cloud costs are incurred without a documented estimate and authorization. -## Security Model -- Default deny network policies -- RBAC least-privilege access -- Sealed-Secrets (decrypt only inside cluster) -- Trivy image scanning pre-deploy -- No secrets in environment variables +## Contributing -## Documentation -- [Architecture](docs/ARCHITECTURE.md) -- [Tech Decisions](docs/TECH_DECISIONS.md) -- [Security](docs/SECURITY.md) - -## Author -**Edwin** | DevOps & Cloud Engineer | Nigeria -[LinkedIn](https://linkedin.com/in/YOUR_LINKEDIN) · [Hashnode](https://YOUR_HASHNODE.hashnode.dev) - ---- -> Built to solve real infrastructure problems. Not a tutorial project. +Submit a narrowly scoped issue or pull request with reproducible steps, tests, observed behavior and security implications. Do not post secrets or real customer data. Proposed production claims require independently verifiable evidence. From 3fdc3be246fe388369f05f0614e3217455be5331 Mon Sep 17 00:00:00 2001 From: Edwin Jonathan Date: Sat, 7 Nov 2026 09:51:00 -0500 Subject: [PATCH 10/26] Detect public ingress port ranges and unresolved literal rules --- scripts/check_infrastructure_safety.py | 44 +++++++++++++++----------- 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/scripts/check_infrastructure_safety.py b/scripts/check_infrastructure_safety.py index 13c9239..25dc93f 100644 --- a/scripts/check_infrastructure_safety.py +++ b/scripts/check_infrastructure_safety.py @@ -1,5 +1,8 @@ #!/usr/bin/env python3 -"""Fail-closed checks for known dangerous ARCHNET infrastructure defaults.""" +"""Conservative guard for simple inline Terraform ingress declarations. + +This is not a complete HCL parser or a substitute for plan-time policy. +""" from pathlib import Path import re import sys @@ -7,27 +10,32 @@ ROOT = Path(__file__).resolve().parents[1] TERRAFORM = ROOT / "terraform" / "main.tf" -def check(): - failures = [] +def check_source(source: str) -> list[str]: + errors = [] + blocks = re.findall(r"\bingress\s*\{([^{}]*)\}", source, re.S) + if not blocks: + return ["No inline ingress blocks found; manual policy review required"] + for block in blocks: + if not re.search(r'\b(?:cidr_blocks)\s*=\s*\[[^\]]*"0\.0\.0\.0/0"[^\]]*\]', block, re.S): + continue + start = re.search(r"\bfrom_port\s*=\s*(\d+)", block) + end = re.search(r"\bto_port\s*=\s*(\d+)", block) + protocol = re.search(r'\bprotocol\s*=\s*"([^"]+)"', block) + if not start or not end or not protocol: + errors.append("Unresolved public ingress rule; manual review required") + continue + lo, hi = int(start.group(1)), int(end.group(1)) + if protocol.group(1).lower() in {"tcp", "6", "-1"} and (lo <= 22 <= hi or lo <= 6443 <= hi or protocol.group(1) == "-1"): + errors.append("Public SSH or Kubernetes API ingress detected") + return errors + +def check() -> list[str]: if not TERRAFORM.is_file(): return ["Missing terraform/main.tf"] - source = TERRAFORM.read_text(encoding="utf-8") - # Inspect ingress blocks, not egress rules. This is a conservative guard, - # not a complete Terraform parser or a replacement for plan/policy checks. - ingress_blocks = re.findall(r"\bingress\s*\{([^{}]*)\}", source, re.S) - if not ingress_blocks: - failures.append("No ingress blocks found; review Terraform manually") - for block in ingress_blocks: - if re.search(r'cidr_blocks\s*=\s*\[\s*"0\.0\.0\.0/0"\s*\]', block): - ports = re.findall(r'(?:from_port|to_port)\s*=\s*(\d+)', block) - if any(p in {"22", "6443"} for p in ports): - failures.append("Public SSH or Kubernetes API ingress detected") - return failures + return check_source(TERRAFORM.read_text(encoding="utf-8")) if __name__ == "__main__": errors = check() for error in errors: print("ERROR:", error, file=sys.stderr) - if errors: - sys.exit(1) - print("Known public control-plane ingress checks passed") + sys.exit(1 if errors else 0) From 8a563a978c705e913f5ae0c2af14fbda1fdf10e4 Mon Sep 17 00:00:00 2001 From: Edwin Jonathan Date: Sat, 7 Nov 2026 09:51:08 -0500 Subject: [PATCH 11/26] Test ingress guard against open ports and range bypasses --- tests/test_infrastructure_safety.py | 35 +++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 tests/test_infrastructure_safety.py diff --git a/tests/test_infrastructure_safety.py b/tests/test_infrastructure_safety.py new file mode 100644 index 0000000..549c6a3 --- /dev/null +++ b/tests/test_infrastructure_safety.py @@ -0,0 +1,35 @@ +import unittest +from scripts.check_infrastructure_safety import check_source + +def rule(start, end, cidrs='"0.0.0.0/0"', protocol="tcp"): + return f'''ingress {{ + from_port = {start} + to_port = {end} + protocol = "{protocol}" + cidr_blocks = [{cidrs}] + }}''' + +class IngressTests(unittest.TestCase): + def test_public_ssh_denied(self): + self.assertTrue(check_source(rule(22, 22))) + + def test_public_kubernetes_api_denied(self): + self.assertTrue(check_source(rule(6443, 6443))) + + def test_port_range_denied(self): + self.assertTrue(check_source(rule(1, 65535))) + + def test_public_https_allowed(self): + self.assertFalse(check_source(rule(443, 443))) + + def test_private_ssh_allowed_by_narrow_guard(self): + self.assertFalse(check_source(rule(22, 22, '"10.0.0.0/8"'))) + + def test_unresolved_public_port_denied(self): + self.assertTrue(check_source('ingress { from_port = var.port to_port = 6443 protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] }')) + + def test_no_inline_rules_requires_review(self): + self.assertTrue(check_source('resource "aws_security_group" "example" {}')) + +if __name__ == "__main__": + unittest.main() From 53ab8f1e5874054a3d6a4465d956d08f2018e1ba Mon Sep 17 00:00:00 2001 From: Edwin Jonathan Date: Sat, 7 Nov 2026 09:51:13 -0500 Subject: [PATCH 12/26] Run ingress safety regression tests in CI --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 92adeba..2f0985d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,6 +49,9 @@ jobs: test -d "$dir" || { echo "Missing: $dir" >&2; exit 1; } done + - name: Test infrastructure safety guard + run: python -m unittest discover -s tests -v + - name: Deny public SSH and Kubernetes API ingress run: python scripts/check_infrastructure_safety.py From f2bfb4b91d3e6f49c77a7e76b099f8001be38ce5 Mon Sep 17 00:00:00 2001 From: Edwin Jonathan Date: Sat, 7 Nov 2026 09:51:59 -0500 Subject: [PATCH 13/26] Add real Terraform init and validate gate to CI --- .github/workflows/ci.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2f0985d..98f43ae 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -55,6 +55,19 @@ jobs: - name: Deny public SSH and Kubernetes API ingress run: python scripts/check_infrastructure_safety.py + terraform: + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v4 + - uses: hashicorp/setup-terraform@v3 + - name: Initialize without remote backend + working-directory: terraform + run: terraform init -backend=false -input=false + - name: Validate Terraform configuration + working-directory: terraform + run: terraform validate -no-color + security: runs-on: ubuntu-latest timeout-minutes: 10 From 6d60ba1be4fcb8b822cdd0838cf2035064974862 Mon Sep 17 00:00:00 2001 From: Edwin Jonathan Date: Sat, 7 Nov 2026 09:52:55 -0500 Subject: [PATCH 14/26] Require Terraform formatting before validation --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 98f43ae..cb9f329 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -64,6 +64,9 @@ jobs: - name: Initialize without remote backend working-directory: terraform run: terraform init -backend=false -input=false + - name: Check Terraform formatting + working-directory: terraform + run: terraform fmt -check -recursive -diff - name: Validate Terraform configuration working-directory: terraform run: terraform validate -no-color From 5e441182ab6cd073073320b3c505d0428a940c5d Mon Sep 17 00:00:00 2001 From: Edwin Jonathan Date: Sat, 7 Nov 2026 09:53:02 -0500 Subject: [PATCH 15/26] Add static regression guard for cloud provisioning safety lock --- scripts/check_cloud_lock.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 scripts/check_cloud_lock.py diff --git a/scripts/check_cloud_lock.py b/scripts/check_cloud_lock.py new file mode 100644 index 0000000..8ae533a --- /dev/null +++ b/scripts/check_cloud_lock.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python3 +"""Verify the explicit cloud deployment safety lock has not been removed. + +Static defense-in-depth only: does not evaluate Terraform expressions or plans. +""" +from pathlib import Path +import re +import sys + +root = Path(__file__).resolve().parents[1] / "terraform" +variables = (root / "variables.tf").read_text(encoding="utf-8") +main = (root / "main.tf").read_text(encoding="utf-8") +checks = { + "cloud deployment variable defaults false": + bool(re.search(r'variable\s+"enable_cloud_deployment"\s*\{[^{}]*\bdefault\s*=\s*false', variables, re.S)), + "cloud deployment variable has deny-true validation": + bool(re.search(r'condition\s*=\s*var\.enable_cloud_deployment\s*==\s*false', variables)), + "EC2 resource is controlled by cloud deployment variable": + bool(re.search(r'resource\s+"aws_instance"\s+"k3s"\s*\{\s*count\s*=\s*var\.enable_cloud_deployment\s*\?\s*1\s*:\s*0', main)), +} +for label, ok in checks.items(): + print(("PASS" if ok else "FAIL") + ": " + label) +sys.exit(0 if all(checks.values()) else 1) From ae1f1348f3565a4e98ad78ff97c27fe9ba1c7731 Mon Sep 17 00:00:00 2001 From: Edwin Jonathan Date: Sat, 7 Nov 2026 09:53:07 -0500 Subject: [PATCH 16/26] Enforce cloud deployment safety lock in CI --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cb9f329..0ea7d2f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -52,6 +52,9 @@ jobs: - name: Test infrastructure safety guard run: python -m unittest discover -s tests -v + - name: Verify cloud deployment safety lock + run: python scripts/check_cloud_lock.py + - name: Deny public SSH and Kubernetes API ingress run: python scripts/check_infrastructure_safety.py From a97798ba6f00045d74fa3efa00e4363337644acc Mon Sep 17 00:00:00 2001 From: Edwin Jonathan Date: Sat, 7 Nov 2026 09:55:04 -0500 Subject: [PATCH 17/26] Align Terraform EC2 arguments to satisfy fmt gate --- terraform/main.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/terraform/main.tf b/terraform/main.tf index 86b4563..67e41bf 100644 --- a/terraform/main.tf +++ b/terraform/main.tf @@ -88,7 +88,7 @@ resource "aws_security_group" "archnet" { } resource "aws_instance" "k3s" { - count = var.enable_cloud_deployment ? 1 : 0 + count = var.enable_cloud_deployment ? 1 : 0 ami = data.aws_ami.ubuntu.id instance_type = var.instance_type subnet_id = aws_subnet.public.id From b260163d4861a2f476d35707c024079bf7ecf026 Mon Sep 17 00:00:00 2001 From: Edwin Jonathan Date: Sat, 7 Nov 2026 09:59:25 -0500 Subject: [PATCH 18/26] Apply exact terraform fmt diff from failed CI job logs --- terraform/main.tf | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/terraform/main.tf b/terraform/main.tf index 67e41bf..0f900a5 100644 --- a/terraform/main.tf +++ b/terraform/main.tf @@ -28,7 +28,7 @@ resource "aws_vpc" "archnet" { cidr_block = "10.0.0.0/16" enable_dns_hostnames = true enable_dns_support = true - tags = { Name = "archnet-vpc" } + tags = { Name = "archnet-vpc" } } resource "aws_subnet" "public" { @@ -36,7 +36,7 @@ resource "aws_subnet" "public" { cidr_block = "10.0.1.0/24" availability_zone = "${var.aws_region}a" map_public_ip_on_launch = true - tags = { Name = "archnet-public" } + tags = { Name = "archnet-public" } } resource "aws_internet_gateway" "archnet" { @@ -47,8 +47,8 @@ resource "aws_internet_gateway" "archnet" { resource "aws_route_table" "public" { vpc_id = aws_vpc.archnet.id route { - cidr_block = "0.0.0.0/0" - gateway_id = aws_internet_gateway.archnet.id + cidr_block = "0.0.0.0/0" + gateway_id = aws_internet_gateway.archnet.id } tags = { Name = "archnet-rt" } } From 3cf2a3797101def28ef8510814f35d8d6ce1a07f Mon Sep 17 00:00:00 2001 From: Edwin Jonathan Date: Thu, 12 Nov 2026 04:57:10 -0500 Subject: [PATCH 19/26] Enforce zero-change Terraform baseline and add disposable Kubernetes smoke test --- .github/workflows/ci.yml | 67 +++++++++++++++++++--- README.md | 34 ++---------- docs/DEPLOYMENT_SAFETY.md | 45 ++++----------- docs/ENGINEERING_AUDIT.md | 17 ++++++ scripts/check_cloud_lock.py | 35 ++++++------ scripts/smoke_kind.sh | 28 ++++++++++ terraform/main.tf | 107 +----------------------------------- terraform/outputs.tf | 4 -- terraform/variables.tf | 19 ++----- 9 files changed, 142 insertions(+), 214 deletions(-) create mode 100644 docs/ENGINEERING_AUDIT.md create mode 100644 scripts/smoke_kind.sh delete mode 100644 terraform/outputs.tf diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0ea7d2f..b389ee3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,7 +14,9 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 10 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false - name: Validate YAML syntax shell: bash @@ -55,15 +57,19 @@ jobs: - name: Verify cloud deployment safety lock run: python scripts/check_cloud_lock.py - - name: Deny public SSH and Kubernetes API ingress - run: python scripts/check_infrastructure_safety.py - terraform: runs-on: ubuntu-latest timeout-minutes: 15 steps: - - uses: actions/checkout@v4 - - uses: hashicorp/setup-terraform@v3 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + - uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2 + with: + terraform_version: 1.12.2 + terraform_wrapper: false + - name: Check provider-free source before Terraform execution + run: python scripts/check_cloud_lock.py - name: Initialize without remote backend working-directory: terraform run: terraform init -backend=false -input=false @@ -74,14 +80,59 @@ jobs: working-directory: terraform run: terraform validate -no-color + - name: Verify empty Terraform plan and deny enablement + working-directory: terraform + env: + CHECKPOINT_DISABLE: '1' + AWS_EC2_METADATA_DISABLED: 'true' + run: | + set -euo pipefail + plan_dir=$(mktemp -d) + trap 'rm -rf -- "$plan_dir"' EXIT + terraform plan -input=false -refresh=false -detailed-exitcode -out="$plan_dir/disabled.tfplan" + terraform show -json "$plan_dir/disabled.tfplan" | jq -e ' + .format_version == "1.2" and .errored == false and + .variables.enable_cloud_deployment.value == false and + ((.resource_changes // []) | length == 0) and + ((.resource_drift // []) | length == 0) and + ((.output_changes // {}) | length == 0) and + ((.configuration.provider_config // {}) | length == 0) and + ((.configuration.root_module.resources // []) | length == 0) and + ((.configuration.root_module.module_calls // {}) | length == 0) + ' + if terraform plan -input=false -refresh=false -no-color -var=enable_cloud_deployment=true >"$plan_dir/denied.log" 2>&1; then + echo "Cloud enablement unexpectedly passed" >&2 + exit 1 + fi + cat "$plan_dir/denied.log" + grep -F 'Cloud deployment is intentionally disabled.' "$plan_dir/denied.log" + + kubernetes: + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + - name: Install pinned kind CLI + run: | + GOBIN="$RUNNER_TEMP/bin" go install sigs.k8s.io/kind@v0.30.0 + echo "$RUNNER_TEMP/bin" >> "$GITHUB_PATH" + - name: Verify disposable Kubernetes control plane + run: bash scripts/smoke_kind.sh + security: runs-on: ubuntu-latest timeout-minutes: 10 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false - name: Scan filesystem with Trivy - uses: aquasecurity/trivy-action@master + uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # 0.35.0 with: + cache: 'false' + version: v0.69.3 scan-type: fs scan-ref: . severity: CRITICAL diff --git a/README.md b/README.md index 8e87cf1..26c1557 100644 --- a/README.md +++ b/README.md @@ -1,32 +1,8 @@ -# ARCHNET — Internal Developer Platform research +# ARCHNET -**Status: experimental / not production-ready.** This repository is undergoing a source, deployment and operational safety audit. No customer production use is authorized or validated. +Experimental repository for reviewing a cloud-disabled infrastructure baseline in pull requests. CI checks whether the Terraform root proposes zero changes without cloud credentials and rejects an attempt to enable provisioning. A disposable kind cluster checks local Kubernetes API readiness and object reconciliation prerequisites. -## Current scope +- [Architecture, demand evidence and alternatives](docs/ENGINEERING_AUDIT.md) +- [Cost boundary and local Kubernetes instructions](docs/DEPLOYMENT_SAFETY.md) -The repository contains Terraform for an AWS VPC and EC2 instance, an Argo CD Application reference, and CI checks. The original README described additional security, observability and automation features that have **not been independently verified** in this audit. Do not interpret the architecture as an operational deployment. - -## Deployment safety - -**Cloud instance creation is disabled by default and explicitly locked.** The earlier bootstrap script referenced by Terraform was missing; it has not been replaced with an unreviewed remote installer. The default instance size was `t3.medium` and must not be provisioned without a complete cost assessment. - -Do not run `terraform apply` expecting a functioning k3s cluster. The current infrastructure does not implement a secure administrative access path or a validated cluster bootstrap. - -See [deployment safety](docs/DEPLOYMENT_SAFETY.md). - -## Continuous integration - -The CI workflow parses YAML, checks shell syntax and required directories, rejects known public SSH/Kubernetes API ingress rules, and runs a critical-severity filesystem scan. These are limited static checks, not evidence of a working deployment. - -## Planned operator acceptance criteria - -1. A second engineer can install and recover a disposable environment from written instructions. -2. Access controls, secrets and dependency provenance are reviewed. -3. Backups are restored successfully under test. -4. Release and rollback paths are reproducible. -5. Reliability objectives and chaos-test results are measured and published. -6. No cloud costs are incurred without a documented estimate and authorization. - -## Contributing - -Submit a narrowly scoped issue or pull request with reproducible steps, tests, observed behavior and security implications. Do not post secrets or real customer data. Proposed production claims require independently verifiable evidence. +The Argo CD Application is reference configuration. The Kubernetes smoke test does not deploy it, validate application workloads or demonstrate production readiness. diff --git a/docs/DEPLOYMENT_SAFETY.md b/docs/DEPLOYMENT_SAFETY.md index b714063..da59d1d 100644 --- a/docs/DEPLOYMENT_SAFETY.md +++ b/docs/DEPLOYMENT_SAFETY.md @@ -1,42 +1,19 @@ # Deployment safety and cost boundary -Status: **not validated for production use**. Do not apply the current Terraform to customer infrastructure. +The infrastructure ceiling is **$10/month**. This baseline authorizes **$0 in live cloud spend**. There are no executable AWS resources, AWS provider, remote backend, AMI reads or deployment steps in the Terraform root. The previous AWS design is available in [the audited revision](https://github.com/EdwinJdevops/ARCHNET/tree/b260163d4861a2f476d35707c024079bf7ecf026/terraform). The empty-root test uses fresh checkout state. Anyone who has previously applied the old root should keep that state separate and review it independently before running Terraform against it. -## Immediate security controls +CI checks the Terraform source inventory before initialization, requires a no-change saved plan and inspects its JSON for resources, drift, outputs, providers and module calls. Setting `enable_cloud_deployment=true` must fail with the specific validation diagnostic. The earlier literal ingress checker remains covered by its unit tests and is scoped to the retired AWS design. Repository checks can be edited in a PR, so any future enforced merge rule needs trusted review of workflow and policy changes. -The initial AWS configuration exposed SSH (22/TCP) and the Kubernetes API -(6443/TCP) to the public internet. This branch removes both ingress rules. -The cluster must not be assumed remotely manageable after this change. -A separate, authenticated administrative access design and recovery procedure -must be reviewed before deployment. +## Disposable Kubernetes smoke test -The existing public HTTP and HTTPS ingress rules also require an application -threat model, TLS configuration and verification before deployment. +On Linux or WSL with a working Linux Docker engine, Bash and Go, run from the repository root: -The static guard in `scripts/check_infrastructure_safety.py` detects only -simple literal Terraform ingress blocks. It is not a complete security -policy engine and does not analyze modules, dynamic expressions or actual -cloud state. Review `terraform plan` and cloud permissions independently. +```sh +go install sigs.k8s.io/kind@v0.30.0 +export PATH="$(go env GOPATH)/bin:$PATH" +bash scripts/smoke_kind.sh +``` -## Budget: maximum $10/month +With a custom GOBIN, add that directory to PATH instead. The script uses a digest-pinned Kubernetes node image, a unique cluster name and a temporary kubeconfig. It checks node readiness, CoreDNS rollout and a ConfigMap round trip. It attempts cluster teardown on exit and fails if teardown fails. The test uses public dependency downloads and local compute. It covers neither Argo CD nor workload deployment, DNS queries, recovery or production operations. -Default: no continuously running cloud infrastructure. Use local disposable -environments and short-lived CI tests. Never provision AWS resources without -an independently checked estimate including compute, EBS, IPv4, data transfer, -monitoring, snapshots and tax. Use account budgets and alerts; alerts are not -hard spending limits. - -No chaos experiments against customer infrastructure. Tests must be isolated, -authorized, bounded, and followed by recovery verification. - -## Required before any production-readiness claim - -- Verified installation from a clean environment by a second operator -- Locked and reviewed dependency versions and supply-chain scanning -- Working deployment, rollback and disaster recovery runbooks -- Access control and secret management review -- Measured backup restoration, SLOs and failure behavior -- Reviewed Terraform plan and complete cost estimate -- Passing CI on the exact commit being released - -A successful static check or CI run does not establish production readiness. +Any future AWS proposal needs separate authorization, a reviewed bootstrap and admin access path, and a full cost estimate covering compute, storage, public IPs, data transfer, monitoring, snapshots and tax. Billing alerts do not cap spending. diff --git a/docs/ENGINEERING_AUDIT.md b/docs/ENGINEERING_AUDIT.md new file mode 100644 index 0000000..382fc62 --- /dev/null +++ b/docs/ENGINEERING_AUDIT.md @@ -0,0 +1,17 @@ +# ARCHNET engineering audit + +## Candidate workflow + +A platform engineer reviewing changes to an intentionally cloud-disabled sandbox needs commit-specific proof that the disabled Terraform root still proposes zero changes, without granting the pull request cloud credentials. Limit this experiment to the zero-change contract and a disposable local Kubernetes check. The original repo combined AWS scaffolding, a reference Argo CD Application and static CI. At the audited revision, the false flag suppressed EC2 while six networking resources remained unconditional and an AMI data source could still query AWS. Argo CD pointed at the apps directory on main with manual sync. The repository provides no verified bootstrap, recovery or operator handoff. + +## Evidence and alternatives + +- [Atlantis issue #3297](https://github.com/runatlantis/atlantis/issues/3297) records a request to isolate plan from apply and the maintenance burden of provider allowlists. [Atlantis security documentation](https://www.runatlantis.io/docs/security.html) explains why planning pull-request code with infrastructure credentials creates a trust boundary. [Issue #2144](https://github.com/runatlantis/atlantis/issues/2144) describes separate Terraform project scopes and credentials. These are examples of plan execution and credential scoping friction. They do not demonstrate demand for ARCHNET or establish how often teams need this specific zero-change check. A repository issue search returned no ARCHNET issues. +- [Terraform provider mocks](https://developer.hashicorp.com/terraform/language/tests/mocking) suit testing modules with provider resources and data sources when real credentials are unavailable. Here the root intentionally contains no providers or resources, so a real empty plan directly tests the narrower invariant. +- [Conftest](https://open-policy-agent.github.io/conftest/) can evaluate JSON with reusable policy across repositories. A small plan predicate is enough for this one invariant. Atlantis suits teams that need a coordinated plan/apply workflow and can operate the required trust boundaries. [kind](https://kind.sigs.k8s.io/docs/user/quick-start/) supplies the local cluster directly. + +Before expanding scope, ask independent platform engineers for recent examples, how often the review repeats, how they verify it today, and whether they would adopt this check instead of their existing CI. Test one real sandbox PR with a second operator and record time to a reviewable result and any missed regressions. Adoption, savings and differentiation remain unvalidated. + +## Verification baseline + +[Run #19](https://github.com/EdwinJdevops/ARCHNET/actions/runs/42384163801) passed validation, Terraform formatting/configuration and Trivy on [the audited commit](https://github.com/EdwinJdevops/ARCHNET/commit/b260163d4861a2f476d35707c024079bf7ecf026). That run did not execute a Terraform plan or start a cluster. Review the checks on the current PR head for the new gates. diff --git a/scripts/check_cloud_lock.py b/scripts/check_cloud_lock.py index 8ae533a..1ede483 100644 --- a/scripts/check_cloud_lock.py +++ b/scripts/check_cloud_lock.py @@ -1,23 +1,22 @@ #!/usr/bin/env python3 -"""Verify the explicit cloud deployment safety lock has not been removed. - -Static defense-in-depth only: does not evaluate Terraform expressions or plans. -""" +"""Reject any executable Terraform configuration beyond the reviewed empty root.""" from pathlib import Path -import re import sys -root = Path(__file__).resolve().parents[1] / "terraform" -variables = (root / "variables.tf").read_text(encoding="utf-8") -main = (root / "main.tf").read_text(encoding="utf-8") -checks = { - "cloud deployment variable defaults false": - bool(re.search(r'variable\s+"enable_cloud_deployment"\s*\{[^{}]*\bdefault\s*=\s*false', variables, re.S)), - "cloud deployment variable has deny-true validation": - bool(re.search(r'condition\s*=\s*var\.enable_cloud_deployment\s*==\s*false', variables)), - "EC2 resource is controlled by cloud deployment variable": - bool(re.search(r'resource\s+"aws_instance"\s+"k3s"\s*\{\s*count\s*=\s*var\.enable_cloud_deployment\s*\?\s*1\s*:\s*0', main)), +repo = Path(__file__).resolve().parents[1] +expected = { + "terraform/main.tf": "terraform {\n required_version = \"= 1.12.2\"\n}\n", + "terraform/variables.tf": "variable \"enable_cloud_deployment\" {\n description = \"Cloud provisioning is disabled.\"\n type = bool\n default = false\n nullable = false\n\n validation {\n condition = var.enable_cloud_deployment == false\n error_message = \"Cloud deployment is intentionally disabled.\"\n }\n}\n", +} +actual = { + path.relative_to(repo).as_posix() + for path in repo.rglob("*") + if path.name.endswith((".tf", ".tf.json")) } -for label, ok in checks.items(): - print(("PASS" if ok else "FAIL") + ": " + label) -sys.exit(0 if all(checks.values()) else 1) +if actual != expected.keys(): + sys.exit(f"Unexpected Terraform source inventory: {sorted(actual)}") +for name, source in expected.items(): + path = repo / name + if path.is_symlink() or path.read_text(encoding="utf-8") != source: + sys.exit(f"Terraform source differs from the reviewed cloud-disabled root: {name}") +print("PASS: provider-free Terraform source and deny-true lock") diff --git a/scripts/smoke_kind.sh b/scripts/smoke_kind.sh new file mode 100644 index 0000000..28d480a --- /dev/null +++ b/scripts/smoke_kind.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +set -euo pipefail + +[[ $(kind version) == "kind v0.30.0 "* ]] +work_dir=$(mktemp -d) +cluster="archnet-$(basename "$work_dir" | tr '[:upper:]' '[:lower:]' | tr -cd '[:alnum:]')" +export KUBECONFIG="$work_dir/kubeconfig" +export KIND_EXPERIMENTAL_PROVIDER=docker +cleanup() { + local status=$? + trap - EXIT + kind delete cluster --name "$cluster" || status=1 + rm -rf -- "$work_dir" + exit "$status" +} +trap cleanup EXIT + +kind create cluster --name "$cluster" --wait 180s \ + --image kindest/node:v1.34.0@sha256:7416a61b42b1662ca6ca89f02028ac133a309a2a30ba309614e8ec94d976dc5a + +kubectl() { + docker exec "$cluster-control-plane" kubectl --kubeconfig=/etc/kubernetes/admin.conf "$@" +} +kubectl wait --for=condition=Ready node --all --timeout=120s +kubectl rollout status deployment/coredns -n kube-system --timeout=120s +kubectl create namespace archnet-smoke +kubectl -n archnet-smoke create configmap smoke --from-literal=ready=true +[[ $(kubectl -n archnet-smoke get configmap smoke -o 'jsonpath={.data.ready}') == true ]] diff --git a/terraform/main.tf b/terraform/main.tf index 0f900a5..f86be10 100644 --- a/terraform/main.tf +++ b/terraform/main.tf @@ -1,108 +1,3 @@ terraform { - required_providers { - aws = { - source = "hashicorp/aws" - version = "~> 4.50" - } - } -} - -provider "aws" { - region = var.aws_region -} - -data "aws_ami" "ubuntu" { - most_recent = true - owners = ["099720109477"] - filter { - name = "name" - values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"] - } - filter { - name = "root-device-type" - values = ["ebs"] - } -} - -resource "aws_vpc" "archnet" { - cidr_block = "10.0.0.0/16" - enable_dns_hostnames = true - enable_dns_support = true - tags = { Name = "archnet-vpc" } -} - -resource "aws_subnet" "public" { - vpc_id = aws_vpc.archnet.id - cidr_block = "10.0.1.0/24" - availability_zone = "${var.aws_region}a" - map_public_ip_on_launch = true - tags = { Name = "archnet-public" } -} - -resource "aws_internet_gateway" "archnet" { - vpc_id = aws_vpc.archnet.id - tags = { Name = "archnet-igw" } -} - -resource "aws_route_table" "public" { - vpc_id = aws_vpc.archnet.id - route { - cidr_block = "0.0.0.0/0" - gateway_id = aws_internet_gateway.archnet.id - } - tags = { Name = "archnet-rt" } -} - -resource "aws_route_table_association" "public" { - subnet_id = aws_subnet.public.id - route_table_id = aws_route_table.public.id -} - -resource "aws_security_group" "archnet" { - vpc_id = aws_vpc.archnet.id - name = "archnet-sg" - description = "Archnet security group" - - ingress { - from_port = 80 - to_port = 80 - protocol = "tcp" - cidr_blocks = ["0.0.0.0/0"] - } - - ingress { - from_port = 443 - to_port = 443 - protocol = "tcp" - cidr_blocks = ["0.0.0.0/0"] - } - - egress { - from_port = 0 - to_port = 0 - protocol = "-1" - cidr_blocks = ["0.0.0.0/0"] - } - - tags = { Name = "archnet-sg" } -} - -resource "aws_instance" "k3s" { - count = var.enable_cloud_deployment ? 1 : 0 - ami = data.aws_ami.ubuntu.id - instance_type = var.instance_type - subnet_id = aws_subnet.public.id - vpc_security_group_ids = [aws_security_group.archnet.id] - key_name = var.key_name - - root_block_device { - volume_size = 50 - volume_type = "gp3" - delete_on_termination = true - } - - # No bootstrap is configured: the previous referenced script was absent. - # Cloud provisioning is disabled until an audited bootstrap and access path exist. - user_data = null - tags = { Name = "archnet-k3s-master" } + required_version = "= 1.12.2" } diff --git a/terraform/outputs.tf b/terraform/outputs.tf deleted file mode 100644 index 8e73a97..0000000 --- a/terraform/outputs.tf +++ /dev/null @@ -1,4 +0,0 @@ -output "k3s_public_ip" { - value = try(aws_instance.k3s[0].public_ip, null) - description = "Null while cloud deployment is disabled" -} diff --git a/terraform/variables.tf b/terraform/variables.tf index 8321398..188604e 100644 --- a/terraform/variables.tf +++ b/terraform/variables.tf @@ -1,22 +1,11 @@ -variable "aws_region" { - default = "us-east-1" -} - -variable "instance_type" { - default = "t3.medium" -} - -variable "key_name" { - description = "EC2 key pair name (must exist in AWS)" - type = string -} - variable "enable_cloud_deployment" { - description = "Safety lock: cloud deployment is disabled until bootstrap, access and cost gates are implemented." + description = "Cloud provisioning is disabled." type = bool default = false + nullable = false + validation { condition = var.enable_cloud_deployment == false - error_message = "Cloud deployment is intentionally disabled pending audited bootstrap and cost review." + error_message = "Cloud deployment is intentionally disabled." } } From 1f5cb64e9dfd31766915ec75ae7db815034de40e Mon Sep 17 00:00:00 2001 From: Edwin Jonathan Date: Thu, 12 Nov 2026 04:58:33 -0500 Subject: [PATCH 20/26] Remove orphaned root-level AWS inputs flagged by CI --- variables.tf | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 variables.tf diff --git a/variables.tf b/variables.tf deleted file mode 100644 index 1591354..0000000 --- a/variables.tf +++ /dev/null @@ -1,12 +0,0 @@ -variable "aws_region" { - default = "us-east-1" -} - -variable "instance_type" { - default = "t3.medium" -} - -variable "key_name" { - description = "EC2 key pair name (must exist in AWS)" - type = string -} From 628a382688e70ebc7cae04b4753832d9fda1ea5d Mon Sep 17 00:00:00 2001 From: Edwin Jonathan Date: Thu, 12 Nov 2026 05:30:08 -0500 Subject: [PATCH 21/26] Benchmark the cloud-disabled Terraform policy with a repeatable regression fixture --- .github/workflows/ci.yml | 40 ++------------- docs/CLOUD_LOCK_BENCHMARK.md | 21 ++++++++ docs/ENGINEERING_AUDIT.md | 2 +- scripts/benchmark_cloud_lock.py | 51 +++++++++++++++++++ scripts/verify_cloud_plan.sh | 33 ++++++++++++ tests/fixtures/unexpected-resource.tf.fixture | 3 ++ 6 files changed, 112 insertions(+), 38 deletions(-) create mode 100644 docs/CLOUD_LOCK_BENCHMARK.md create mode 100644 scripts/benchmark_cloud_lock.py create mode 100644 scripts/verify_cloud_plan.sh create mode 100644 tests/fixtures/unexpected-resource.tf.fixture diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b389ee3..db26aa3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -68,44 +68,10 @@ jobs: with: terraform_version: 1.12.2 terraform_wrapper: false - - name: Check provider-free source before Terraform execution - run: python scripts/check_cloud_lock.py - - name: Initialize without remote backend - working-directory: terraform - run: terraform init -backend=false -input=false - - name: Check Terraform formatting - working-directory: terraform - run: terraform fmt -check -recursive -diff - - name: Validate Terraform configuration - working-directory: terraform - run: terraform validate -no-color - + - name: Benchmark source policy without Terraform execution + run: python scripts/benchmark_cloud_lock.py - name: Verify empty Terraform plan and deny enablement - working-directory: terraform - env: - CHECKPOINT_DISABLE: '1' - AWS_EC2_METADATA_DISABLED: 'true' - run: | - set -euo pipefail - plan_dir=$(mktemp -d) - trap 'rm -rf -- "$plan_dir"' EXIT - terraform plan -input=false -refresh=false -detailed-exitcode -out="$plan_dir/disabled.tfplan" - terraform show -json "$plan_dir/disabled.tfplan" | jq -e ' - .format_version == "1.2" and .errored == false and - .variables.enable_cloud_deployment.value == false and - ((.resource_changes // []) | length == 0) and - ((.resource_drift // []) | length == 0) and - ((.output_changes // {}) | length == 0) and - ((.configuration.provider_config // {}) | length == 0) and - ((.configuration.root_module.resources // []) | length == 0) and - ((.configuration.root_module.module_calls // {}) | length == 0) - ' - if terraform plan -input=false -refresh=false -no-color -var=enable_cloud_deployment=true >"$plan_dir/denied.log" 2>&1; then - echo "Cloud enablement unexpectedly passed" >&2 - exit 1 - fi - cat "$plan_dir/denied.log" - grep -F 'Cloud deployment is intentionally disabled.' "$plan_dir/denied.log" + run: bash scripts/verify_cloud_plan.sh kubernetes: runs-on: ubuntu-latest diff --git a/docs/CLOUD_LOCK_BENCHMARK.md b/docs/CLOUD_LOCK_BENCHMARK.md new file mode 100644 index 0000000..30b1693 --- /dev/null +++ b/docs/CLOUD_LOCK_BENCHMARK.md @@ -0,0 +1,21 @@ +# Cloud-disabled Terraform benchmark + +Run from the repository root on Linux or WSL with Python 3, Bash, Terraform 1.12.2 and jq. + +```sh +python scripts/benchmark_cloud_lock.py +bash scripts/verify_cloud_plan.sh +``` + +Use a fresh checkout without existing Terraform state. These are the same commands CI runs. The source benchmark copies the checked-in root and policy into a temporary directory and adds a provider-free `terraform_data` declaration from the fixture. It never invokes Terraform and leaves the checkout untouched. The real plan script checks the live source inventory before initializing Terraform. + +| Case | Expected evidence | +| --- | --- | +| Clean source | Source guard exits 0. | +| Unexpected declaration | Source guard exits 1 and names the added file. | +| Disabled Terraform root | Terraform exits 0 with zero changes, and the plan JSON policy passes. | +| Explicit enablement | Terraform rejects the input with the cloud-disabled diagnostic. | + +The job fails if any decision differs. Logs report elapsed time for each source check and each Terraform plan command. Source timings include Python process startup. Timings describe that run and exclude queue time and setup. View the Terraform job in the [workflow history](https://github.com/EdwinJdevops/ARCHNET/actions/workflows/ci.yml) for the commit under review. Kubernetes smoke results appear in a separate job. + +This benchmark verifies the disabled-root contract and one representative regression. It provides no evidence about adoption, production operation or the performance of a live AWS plan. diff --git a/docs/ENGINEERING_AUDIT.md b/docs/ENGINEERING_AUDIT.md index 382fc62..d89ac12 100644 --- a/docs/ENGINEERING_AUDIT.md +++ b/docs/ENGINEERING_AUDIT.md @@ -10,7 +10,7 @@ A platform engineer reviewing changes to an intentionally cloud-disabled sandbox - [Terraform provider mocks](https://developer.hashicorp.com/terraform/language/tests/mocking) suit testing modules with provider resources and data sources when real credentials are unavailable. Here the root intentionally contains no providers or resources, so a real empty plan directly tests the narrower invariant. - [Conftest](https://open-policy-agent.github.io/conftest/) can evaluate JSON with reusable policy across repositories. A small plan predicate is enough for this one invariant. Atlantis suits teams that need a coordinated plan/apply workflow and can operate the required trust boundaries. [kind](https://kind.sigs.k8s.io/docs/user/quick-start/) supplies the local cluster directly. -Before expanding scope, ask independent platform engineers for recent examples, how often the review repeats, how they verify it today, and whether they would adopt this check instead of their existing CI. Test one real sandbox PR with a second operator and record time to a reviewable result and any missed regressions. Adoption, savings and differentiation remain unvalidated. +The [reproducible benchmark](CLOUD_LOCK_BENCHMARK.md) lets a solo maintainer publish review evidence for clean and regressed configurations. Before expanding scope, look for concrete reports from unrelated teams and record their existing safeguards and unresolved gaps. Adoption, savings and differentiation remain unvalidated. ## Verification baseline diff --git a/scripts/benchmark_cloud_lock.py b/scripts/benchmark_cloud_lock.py new file mode 100644 index 0000000..a415272 --- /dev/null +++ b/scripts/benchmark_cloud_lock.py @@ -0,0 +1,51 @@ +"""Run the real source guard against an isolated copy of the approved root.""" +from pathlib import Path +import shutil +import subprocess +import sys +import tempfile +import time + +REPO = Path(__file__).resolve().parents[1] +GUARD = Path("scripts/check_cloud_lock.py") +FIXTURE = REPO / "tests/fixtures/unexpected-resource.tf.fixture" + + +def check(root, case, expected_code, expected_message): + started = time.perf_counter() + result = subprocess.run( + [sys.executable, str(root / GUARD)], + capture_output=True, + text=True, + timeout=10, + ) + elapsed = time.perf_counter() - started + output = result.stdout + result.stderr + print(f"{case}: exit={result.returncode}, elapsed={elapsed:.3f}s", flush=True) + print(output, end="", flush=True) + if result.returncode != expected_code or expected_message not in output: + raise SystemExit(f"Unexpected policy decision for {case}") + + +def main(): + with tempfile.TemporaryDirectory(prefix="archnet-policy-") as directory: + root = Path(directory) + (root / GUARD.parent).mkdir() + shutil.copyfile(REPO / GUARD, root / GUARD) + (root / "terraform").mkdir() + for name in ("main.tf", "variables.tf"): + relative = Path("terraform") / name + shutil.copyfile(REPO / relative, root / relative) + check(root, "clean source", 0, "PASS: provider-free Terraform source") + shutil.copyfile(FIXTURE, root / "terraform/unexpected.tf") + check( + root, + "unexpected declaration", + 1, + "Unexpected Terraform source inventory: ['terraform/main.tf', " + "'terraform/unexpected.tf', 'terraform/variables.tf']", + ) + + +if __name__ == "__main__": + main() diff --git a/scripts/verify_cloud_plan.sh b/scripts/verify_cloud_plan.sh new file mode 100644 index 0000000..5fea457 --- /dev/null +++ b/scripts/verify_cloud_plan.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +set -euo pipefail + +repo_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd) +python "$repo_dir/scripts/check_cloud_lock.py" +cd -- "$repo_dir/terraform" +export CHECKPOINT_DISABLE=1 AWS_EC2_METADATA_DISABLED=true +terraform init -backend=false -input=false +terraform fmt -check -recursive -diff +terraform validate -no-color + +TIMEFORMAT='Elapsed: %3R s' +plan_dir=$(mktemp -d) +trap 'rm -rf -- "$plan_dir"' EXIT +echo 'CASE: disabled baseline must produce zero changes' +time terraform plan -input=false -refresh=false -detailed-exitcode -out="$plan_dir/disabled.tfplan" +terraform show -json "$plan_dir/disabled.tfplan" | jq -e ' + .format_version == "1.2" and .errored == false and + .variables.enable_cloud_deployment.value == false and + ((.resource_changes // []) | length == 0) and + ((.resource_drift // []) | length == 0) and + ((.output_changes // {}) | length == 0) and + ((.configuration.provider_config // {}) | length == 0) and + ((.configuration.root_module.resources // []) | length == 0) and + ((.configuration.root_module.module_calls // {}) | length == 0) +' +echo 'CASE: explicit enablement must be rejected' +if time terraform plan -input=false -refresh=false -no-color -var=enable_cloud_deployment=true >"$plan_dir/denied.log" 2>&1; then + echo "Cloud enablement unexpectedly passed" >&2 + exit 1 +fi +cat "$plan_dir/denied.log" +grep -F 'Cloud deployment is intentionally disabled.' "$plan_dir/denied.log" diff --git a/tests/fixtures/unexpected-resource.tf.fixture b/tests/fixtures/unexpected-resource.tf.fixture new file mode 100644 index 0000000..535cde4 --- /dev/null +++ b/tests/fixtures/unexpected-resource.tf.fixture @@ -0,0 +1,3 @@ +resource "terraform_data" "unexpected" { + input = "cloud-lock regression fixture" +} From 726a281b4e1f3c660b826dae84a2fa082c655198 Mon Sep 17 00:00:00 2001 From: Edwin Jonathan Date: Thu, 12 Nov 2026 05:37:06 -0500 Subject: [PATCH 22/26] Record public demand evidence and merge verification limits --- docs/ENGINEERING_AUDIT.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/docs/ENGINEERING_AUDIT.md b/docs/ENGINEERING_AUDIT.md index d89ac12..44dd297 100644 --- a/docs/ENGINEERING_AUDIT.md +++ b/docs/ENGINEERING_AUDIT.md @@ -12,6 +12,21 @@ A platform engineer reviewing changes to an intentionally cloud-disabled sandbox The [reproducible benchmark](CLOUD_LOCK_BENCHMARK.md) lets a solo maintainer publish review evidence for clean and regressed configurations. Before expanding scope, look for concrete reports from unrelated teams and record their existing safeguards and unresolved gaps. Adoption, savings and differentiation remain unvalidated. +## Public evidence review + +| Source | Observed friction | Scope of the evidence | +| --- | --- | --- | +| [Atlantis #3297](https://github.com/runatlantis/atlantis/issues/3297) | A requester wanted isolated planning and reported maintenance cost for provider allowlists. | Supports plan isolation as a concern. The reported workflow includes live infrastructure. | +| [AWS provider #29141](https://github.com/hashicorp/terraform-provider-aws/issues/29141) | A plan failed to resolve credentials from the configured file. The reporter used an environment variable as a workaround. | A concrete credential failure. No cloud-disabled PR workflow is described. | +| [Terraform #17034](https://github.com/hashicorp/terraform/issues/17034) | Terraform maintainers describe why plan can read external data even when implicit refresh is disabled. | Explains the mechanism that made ARCHNET's AMI declaration relevant. It is a design record. | +| [Terragrunt authentication](https://docs.terragrunt.com/features/units/authentication/) | Documents separate read-only plan and read-write apply roles. | A practical alternative when a plan must read live infrastructure. | + +The public record corroborates credential and planning friction. The repository audit is the only directly observed example in this review of a cloud-disabled root that still declared live AWS dependencies. Keep the benchmark as a reusable engineering baseline. Expand the product only when another concrete case identifies the same contract and an unmet need. + +## Merge verification + +The four checks run on the draft PR. Active required-check enforcement on `main` could not be verified from available repository rules access. A green run confirms execution for the tested revision. Before a future merge, inspect the active rule for `main` and confirm it requires the expected checks and protects changes to the workflow and policy. The PR remains draft. + ## Verification baseline [Run #19](https://github.com/EdwinJdevops/ARCHNET/actions/runs/42384163801) passed validation, Terraform formatting/configuration and Trivy on [the audited commit](https://github.com/EdwinJdevops/ARCHNET/commit/b260163d4861a2f476d35707c024079bf7ecf026). That run did not execute a Terraform plan or start a cluster. Review the checks on the current PR head for the new gates. From 209439a7a86e1324690c2f50a39ce15875d5a65b Mon Sep 17 00:00:00 2001 From: Edwin Jonathan Date: Thu, 12 Nov 2026 06:11:18 -0500 Subject: [PATCH 23/26] Test Terraform resource regression inside an approved source file --- README.md | 1 + docs/CLOUD_LOCK_BENCHMARK.md | 5 +++-- scripts/benchmark_cloud_lock.py | 14 +++++++++++++- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 26c1557..631f0be 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ Experimental repository for reviewing a cloud-disabled infrastructure baseline in pull requests. CI checks whether the Terraform root proposes zero changes without cloud credentials and rejects an attempt to enable provisioning. A disposable kind cluster checks local Kubernetes API readiness and object reconciliation prerequisites. +- [Reproduce the cloud-disabled Terraform benchmark](docs/CLOUD_LOCK_BENCHMARK.md) - [Architecture, demand evidence and alternatives](docs/ENGINEERING_AUDIT.md) - [Cost boundary and local Kubernetes instructions](docs/DEPLOYMENT_SAFETY.md) diff --git a/docs/CLOUD_LOCK_BENCHMARK.md b/docs/CLOUD_LOCK_BENCHMARK.md index 30b1693..229cb5a 100644 --- a/docs/CLOUD_LOCK_BENCHMARK.md +++ b/docs/CLOUD_LOCK_BENCHMARK.md @@ -7,15 +7,16 @@ python scripts/benchmark_cloud_lock.py bash scripts/verify_cloud_plan.sh ``` -Use a fresh checkout without existing Terraform state. These are the same commands CI runs. The source benchmark copies the checked-in root and policy into a temporary directory and adds a provider-free `terraform_data` declaration from the fixture. It never invokes Terraform and leaves the checkout untouched. The real plan script checks the live source inventory before initializing Terraform. +Use a fresh checkout without existing Terraform state. These are the same commands CI runs. The source benchmark copies the checked-in root and policy into a temporary directory and uses the same provider-free `terraform_data` fixture in two isolated regression cases. It first adds an unexpected file, then removes that file and appends the declaration to the approved `main.tf`. It never invokes Terraform and leaves the checkout untouched. The real plan script checks the live source inventory before initializing Terraform. | Case | Expected evidence | | --- | --- | | Clean source | Source guard exits 0. | | Unexpected declaration | Source guard exits 1 and names the added file. | +| Declaration appended to approved file | Source guard exits 1 and identifies the modified `terraform/main.tf`. | | Disabled Terraform root | Terraform exits 0 with zero changes, and the plan JSON policy passes. | | Explicit enablement | Terraform rejects the input with the cloud-disabled diagnostic. | The job fails if any decision differs. Logs report elapsed time for each source check and each Terraform plan command. Source timings include Python process startup. Timings describe that run and exclude queue time and setup. View the Terraform job in the [workflow history](https://github.com/EdwinJdevops/ARCHNET/actions/workflows/ci.yml) for the commit under review. Kubernetes smoke results appear in a separate job. -This benchmark verifies the disabled-root contract and one representative regression. It provides no evidence about adoption, production operation or the performance of a live AWS plan. +This benchmark verifies the disabled-root contract and two representative source regressions. It provides no evidence about adoption, production operation or the performance of a live AWS plan. diff --git a/scripts/benchmark_cloud_lock.py b/scripts/benchmark_cloud_lock.py index a415272..0781b6f 100644 --- a/scripts/benchmark_cloud_lock.py +++ b/scripts/benchmark_cloud_lock.py @@ -37,7 +37,8 @@ def main(): relative = Path("terraform") / name shutil.copyfile(REPO / relative, root / relative) check(root, "clean source", 0, "PASS: provider-free Terraform source") - shutil.copyfile(FIXTURE, root / "terraform/unexpected.tf") + unexpected = root / "terraform/unexpected.tf" + shutil.copyfile(FIXTURE, unexpected) check( root, "unexpected declaration", @@ -45,6 +46,17 @@ def main(): "Unexpected Terraform source inventory: ['terraform/main.tf', " "'terraform/unexpected.tf', 'terraform/variables.tf']", ) + unexpected.unlink() + + with (root / "terraform/main.tf").open("ab") as source: + source.write(b"\n" + FIXTURE.read_bytes()) + check( + root, + "declaration appended to approved file", + 1, + "Terraform source differs from the reviewed cloud-disabled root: " + "terraform/main.tf", + ) if __name__ == "__main__": From 093fd78fa42cda2b3d63196e3eaa75aa899218ad Mon Sep 17 00:00:00 2001 From: Edwin Jonathan Date: Fri, 13 Nov 2026 09:04:36 -0500 Subject: [PATCH 24/26] Test two upstream IAM policy renderers with network-isolated plans --- .github/workflows/iam-experiment.yml | 20 ++++++++ docs/OFFLINE_IAM_EXPERIMENT.md | 5 ++ scripts/test_offline_iam.sh | 35 ++++++++++++++ tests/fixtures/iam/cloudposse.tf.fixture | 61 ++++++++++++++++++++++++ tests/fixtures/iam/eks.tf.fixture | 52 ++++++++++++++++++++ 5 files changed, 173 insertions(+) create mode 100644 .github/workflows/iam-experiment.yml create mode 100644 docs/OFFLINE_IAM_EXPERIMENT.md create mode 100644 scripts/test_offline_iam.sh create mode 100644 tests/fixtures/iam/cloudposse.tf.fixture create mode 100644 tests/fixtures/iam/eks.tf.fixture diff --git a/.github/workflows/iam-experiment.yml b/.github/workflows/iam-experiment.yml new file mode 100644 index 0000000..4078bfc --- /dev/null +++ b/.github/workflows/iam-experiment.yml @@ -0,0 +1,20 @@ +name: Offline IAM policy experiment +on: + pull_request: + branches: [main] +permissions: + contents: read +jobs: + offline-policy: + runs-on: ubuntu-latest + timeout-minutes: 8 + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + with: + persist-credentials: false + - uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd + with: + terraform_version: 1.12.2 + terraform_wrapper: false + - name: Run extracted renderers without network access + run: bash scripts/test_offline_iam.sh diff --git a/docs/OFFLINE_IAM_EXPERIMENT.md b/docs/OFFLINE_IAM_EXPERIMENT.md new file mode 100644 index 0000000..a577a4d --- /dev/null +++ b/docs/OFFLINE_IAM_EXPERIMENT.md @@ -0,0 +1,5 @@ +# Offline IAM policy rendering experiment + +The fixtures extract the trust policy renderer from [EKS v20.37.2](https://github.com/terraform-aws-modules/terraform-aws-eks/blob/v20.37.2/main.tf) and [Cloud Posse at 0c2347c](https://github.com/cloudposse/terraform-aws-eks-iam-role/blob/0c2347ca06fb54579a0e57d03e20aed3c1045157/main.tf). They supply fixed inputs for surrounding module logic and account identity. The actual AWS provider renders the document. The harness compares a principal in its generated JSON before and after changing a renderer input. + +CI downloads the provider before testing. Plans run in a separate Linux network namespace with an empty environment and literal dummy credentials. The fixtures declare no managed resources and the harness runs only plans. The experiment tests isolated policy rendering. It does not establish that complete upstream modules can plan without cloud reads, that the policies are production-ready, or that maintainers need a new tool. diff --git a/scripts/test_offline_iam.sh b/scripts/test_offline_iam.sh new file mode 100644 index 0000000..3df16fb --- /dev/null +++ b/scripts/test_offline_iam.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +set -euo pipefail + +root=$(mktemp -d) +trap 'rm -rf -- "$root"' EXIT +terraform_bin=$(command -v terraform) +for module in eks cloudposse; do + mkdir "$root/$module" + cp "tests/fixtures/iam/$module.tf.fixture" "$root/$module/main.tf" + ( + cd "$root/$module" + CHECKPOINT_DISABLE=1 terraform init -backend=false -input=false + ) +done + +render() { + sudo unshare --net -- env -i PATH=/usr/bin:/bin CHECKPOINT_DISABLE=1 \ + AWS_EC2_METADATA_DISABLED=true "$terraform_bin" -chdir="$1" \ + plan -input=false -refresh=false -no-color -out="$1/result.plan" + "$terraform_bin" -chdir="$1" show -json "$1/result.plan" | + jq -e --argjson expected "$2" '.planned_values.outputs.principal_matches.value == $expected' +} + +for module in eks cloudposse; do + dir="$root/$module" + echo "$module: original principal must match" + render "$dir" true + if [[ "$module" == eks ]]; then + sed -i 's/identifiers = \["eks.amazonaws.com"\]/identifiers = ["ec2.amazonaws.com"]/' "$dir/main.tf" + else + sed -i '/aws_account_number *=/s/111122223333/444455556666/' "$dir/main.tf" + fi + echo "$module: changed principal must fail the same assertion" + render "$dir" false +done diff --git a/tests/fixtures/iam/cloudposse.tf.fixture b/tests/fixtures/iam/cloudposse.tf.fixture new file mode 100644 index 0000000..9bc4681 --- /dev/null +++ b/tests/fixtures/iam/cloudposse.tf.fixture @@ -0,0 +1,61 @@ +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = "= 5.100.0" + } + } +} +provider "aws" { + region = "us-east-1" + access_key = "offline-fixture" + secret_key = "offline-fixture" + skip_credentials_validation = true + skip_requesting_account_id = true + skip_metadata_api_check = true + skip_region_validation = true +} +locals { + enabled = true + aws_account_number = "111122223333" + eks_cluster_oidc_issuer = "example.invalid" + service_account_namespace_name_list = ["default:example"] +} +data "aws_iam_policy_document" "service_account_assume_role" { + count = local.enabled ? 1 : 0 + + statement { + actions = [ + "sts:AssumeRoleWithWebIdentity" + ] + + effect = "Allow" + + principals { + type = "Federated" + identifiers = [format("arn:%s:iam::%s:oidc-provider/%s", "aws", local.aws_account_number, local.eks_cluster_oidc_issuer)] + } + + condition { + test = "StringLike" + values = formatlist("system:serviceaccount:%s", local.service_account_namespace_name_list) + variable = format("%s:sub", local.eks_cluster_oidc_issuer) + + } + condition { + test = "StringEquals" + values = ["sts.amazonaws.com"] + variable = format("%s:aud", local.eks_cluster_oidc_issuer) + } + } + + lifecycle { + precondition { + condition = length(local.eks_cluster_oidc_issuer) > 0 + error_message = "The eks_cluster_oidc_issuer_url value must have a value." + } + } +} +output "principal_matches" { + value = try(jsondecode(data.aws_iam_policy_document.service_account_assume_role[0].json).Statement[0].Principal.Federated == "arn:aws:iam::111122223333:oidc-provider/example.invalid", false) +} diff --git a/tests/fixtures/iam/eks.tf.fixture b/tests/fixtures/iam/eks.tf.fixture new file mode 100644 index 0000000..ae5056b --- /dev/null +++ b/tests/fixtures/iam/eks.tf.fixture @@ -0,0 +1,52 @@ +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = "= 5.100.0" + } + } +} +provider "aws" { + region = "us-east-1" + access_key = "offline-fixture" + secret_key = "offline-fixture" + skip_credentials_validation = true + skip_requesting_account_id = true + skip_metadata_api_check = true + skip_region_validation = true +} +locals { + create = true + create_outposts_local_cluster = false +} +variable "create_iam_role" { + default = true +} +data "aws_iam_policy_document" "assume_role_policy" { + count = local.create && var.create_iam_role ? 1 : 0 + + statement { + sid = "EKSClusterAssumeRole" + actions = [ + "sts:AssumeRole", + "sts:TagSession", + ] + + principals { + type = "Service" + identifiers = ["eks.amazonaws.com"] + } + + dynamic "principals" { + for_each = local.create_outposts_local_cluster ? [1] : [] + + content { + type = "Service" + identifiers = ["ec2.amazonaws.com"] + } + } + } +} +output "principal_matches" { + value = try(jsondecode(data.aws_iam_policy_document.assume_role_policy[0].json).Statement[0].Principal.Service == "eks.amazonaws.com", false) +} From 0e810086a5523f3de47faf3bb57d816e108bbf4d Mon Sep 17 00:00:00 2001 From: Edwin Jonathan Date: Fri, 13 Nov 2026 10:30:47 -0500 Subject: [PATCH 25/26] Exercise native offline IAM checks from pinned upstream module roots --- .github/workflows/full-module-iam.yml | 98 +++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 .github/workflows/full-module-iam.yml diff --git a/.github/workflows/full-module-iam.yml b/.github/workflows/full-module-iam.yml new file mode 100644 index 0000000..ba4ae63 --- /dev/null +++ b/.github/workflows/full-module-iam.yml @@ -0,0 +1,98 @@ +name: Full module IAM acceptance +on: pull_request +permissions: + contents: read +jobs: + native: + runs-on: ubuntu-latest + timeout-minutes: 12 + strategy: + fail-fast: false + matrix: + include: + - repository: terraform-aws-modules/terraform-aws-eks + ref: 8a0efdbbc84180a26e0bacfd2b6fcfceac53b3b60 + policy: assume_role_policy + principal: Service + expected: eks.amazonaws.com + - repository: cloudposse/terraform-aws-eks-iam-role + ref: 0c2347ca06fb54579a0e57d03e20aed3c1045157 + policy: service_account_assume_role + principal: Federated + expected: arn:aws:iam::111122223333:oidc-provider/example.invalid + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + with: + repository: ${{ matrix.repository }} + ref: ${{ matrix.ref }} + persist-credentials: false + - uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd + with: + terraform_version: 1.12.2 + terraform_wrapper: false + - name: Test actual policy from upstream root + shell: bash + env: + POLICY: ${{ matrix.policy }} + PRINCIPAL: ${{ matrix.principal }} + EXPECTED: ${{ matrix.expected }} + CHECKPOINT_DISABLE: '1' + run: | + set -euo pipefail + mkdir archnet-tests + cat > archnet-tests/policy.tftest.hcl <> archnet-tests/policy.tftest.hcl <<'EOF' + + variables { + eks_cluster_oidc_issuer_url = "https://example.invalid" + service_account_namespace = "default" + service_account_name = "example" + } + + override_data { + target = data.aws_caller_identity.current[0] + override_during = plan + values = { account_id = "111122223333" } + } + EOF + fi + terraform init -backend=false -input=false + tf=$(command -v terraform) + run_offline() { + sudo unshare --net -- env -i PATH=/usr/bin:/bin CHECKPOINT_DISABLE=1 AWS_EC2_METADATA_DISABLED=true "$tf" test -test-directory=archnet-tests -no-color + } + run_offline + if [[ "$PRINCIPAL" == Service ]]; then + sed -i '0,/identifiers = \["eks.amazonaws.com"\]/{s/identifiers = \["eks.amazonaws.com"\]/identifiers = ["ec2.amazonaws.com"]/;}' main.tf + else + sed -i 's/account_id = "111122223333"/account_id = "444455556666"/' archnet-tests/policy.tftest.hcl + fi + if run_offline > "$RUNNER_TEMP/negative.log" 2>&1; then + echo "Changed principal passed unexpectedly" >&2 + exit 1 + fi + cat "$RUNNER_TEMP/negative.log" + grep -Fx 'ARCHNET_PRINCIPAL_MISMATCH' "$RUNNER_TEMP/negative.log" From 02efecf5bbadd8292151b9df50543200e23de156 Mon Sep 17 00:00:00 2001 From: Edwin Jonathan Date: Fri, 13 Nov 2026 10:32:34 -0500 Subject: [PATCH 26/26] Use the upstream EKS release ref for acceptance checkout --- .github/workflows/full-module-iam.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/full-module-iam.yml b/.github/workflows/full-module-iam.yml index ba4ae63..19447ef 100644 --- a/.github/workflows/full-module-iam.yml +++ b/.github/workflows/full-module-iam.yml @@ -11,7 +11,7 @@ jobs: matrix: include: - repository: terraform-aws-modules/terraform-aws-eks - ref: 8a0efdbbc84180a26e0bacfd2b6fcfceac53b3b60 + ref: v20.37.2 policy: assume_role_policy principal: Service expected: eks.amazonaws.com