From 549b98ad755d223ee42a7de90d30a3abb27dacdb Mon Sep 17 00:00:00 2001 From: Tate McCauley Date: Fri, 10 Jul 2026 10:49:15 -0600 Subject: [PATCH] chore(deploy): trim to a Docker-only bundle for public release MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove Paramify-internal / heavy deployment direction from deploy/ ahead of the public release: - Delete the entire deploy/k8s/ tree — the Kubernetes CronJob manifests + local→EKS walkthrough, plus the multi-account hub-and-spoke bundle (Terraform, AWS_MULTI_ACCOUNT.md, aws-config.configmap.yaml). We don't want to prescribe that level of deployment. The Kubernetes *fetchers* (fetchers/k8s/) are untouched, and the Dockerfile keeps kubectl since those fetchers need it. - Fix an internal leak: entrypoint.sh defaulted uploads to stage.paramify.com; now app.paramify.com (matching the uploader's own production default). Same correction in .env.example and a "defaults to stage" note in deploy/README.md. - deploy/README.md is now a clean Docker/compose guide: dropped the k8s table row, the ConfigMap aside, and the K8s-CronJob production note; genericized the ECS/EKS secret-injection option. - Scrub the stale deploy/k8s/ link + Kubernetes/multi-account prose from docs/design.md, and "Docker or Kubernetes" -> "Docker" in the root README. No broken links remain to the deleted paths. Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 2 +- deploy/.env.example | 4 +- deploy/README.md | 24 +-- deploy/entrypoint.sh | 2 +- deploy/k8s/AWS_MULTI_ACCOUNT.md | 128 ----------- deploy/k8s/LOCAL_K8S.md | 199 ------------------ deploy/k8s/aws-config.configmap.yaml | 35 --- deploy/k8s/cronjob-aws.yaml | 111 ---------- deploy/k8s/cronjob.yaml | 102 --------- deploy/k8s/terraform/.terraform.lock.hcl | 25 --- deploy/k8s/terraform/README.md | 76 ------- deploy/k8s/terraform/home_role.tf | 67 ------ .../terraform/modules/target_account/main.tf | 36 ---- .../modules/target_account/outputs.tf | 4 - .../modules/target_account/variables.tf | 9 - deploy/k8s/terraform/outputs.tf | 14 -- deploy/k8s/terraform/providers.tf | 17 -- deploy/k8s/terraform/target_roles.tf | 21 -- deploy/k8s/terraform/variables.tf | 52 ----- deploy/k8s/terraform/versions.tf | 9 - docs/design.md | 2 +- 21 files changed, 14 insertions(+), 925 deletions(-) delete mode 100644 deploy/k8s/AWS_MULTI_ACCOUNT.md delete mode 100644 deploy/k8s/LOCAL_K8S.md delete mode 100644 deploy/k8s/aws-config.configmap.yaml delete mode 100644 deploy/k8s/cronjob-aws.yaml delete mode 100644 deploy/k8s/cronjob.yaml delete mode 100644 deploy/k8s/terraform/.terraform.lock.hcl delete mode 100644 deploy/k8s/terraform/README.md delete mode 100644 deploy/k8s/terraform/home_role.tf delete mode 100644 deploy/k8s/terraform/modules/target_account/main.tf delete mode 100644 deploy/k8s/terraform/modules/target_account/outputs.tf delete mode 100644 deploy/k8s/terraform/modules/target_account/variables.tf delete mode 100644 deploy/k8s/terraform/outputs.tf delete mode 100644 deploy/k8s/terraform/providers.tf delete mode 100644 deploy/k8s/terraform/target_roles.tf delete mode 100644 deploy/k8s/terraform/variables.tf delete mode 100644 deploy/k8s/terraform/versions.tf diff --git a/README.md b/README.md index 96e34c1..d1277fd 100644 --- a/README.md +++ b/README.md @@ -145,7 +145,7 @@ export PARAMIFY_UPLOAD_API_TOKEN= # see uploaders/paramify_evidenc paramify upload # push the latest run ``` -Each service has a credential setup guide in its fetcher directory — for example, [`fetchers/okta/README.md`](fetchers/okta/README.md) covers creating an Okta API token and the required admin role. See [`examples/`](examples/) for complete worked manifests (multi-region AWS, GitLab fanout, etc.) and [`deploy/README.md`](deploy/README.md) for running on a schedule in Docker or Kubernetes. +Each service has a credential setup guide in its fetcher directory — for example, [`fetchers/okta/README.md`](fetchers/okta/README.md) covers creating an Okta API token and the required admin role. See [`examples/`](examples/) for complete worked manifests (multi-region AWS, GitLab fanout, etc.) and [`deploy/README.md`](deploy/README.md) for running on a schedule in Docker. The full command surface: diff --git a/deploy/.env.example b/deploy/.env.example index 98ff3b7..75089e1 100644 --- a/deploy/.env.example +++ b/deploy/.env.example @@ -19,8 +19,8 @@ # ---- Option B: provide values directly (below) ------------------------------- # ---- Upload to Paramify (required for the upload step) ----------------------- PARAMIFY_UPLOAD_API_TOKEN= -# Optional; defaults to stage (https required, except localhost): -# PARAMIFY_API_BASE_URL=https://stage.paramify.com/api/v0 +# Optional; defaults to https://app.paramify.com/api/v0 (https required, except localhost): +# PARAMIFY_API_BASE_URL=https://your-instance.paramify.com/api/v0 # ---- Per-platform credentials (uncomment what your manifests actually use) --- # Names below are illustrative — confirm each with `paramify describe `. diff --git a/deploy/README.md b/deploy/README.md index efc9a17..97a72d0 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -21,7 +21,6 @@ is a guided run against a test AWS Secrets Manager secret. | `crontab` | The schedule — maps a time to a manifest path | | `run-and-upload.sh` | Chains `paramify run ` → upload to Paramify | | `.env.example` | Template for the secrets you inject at run time | -| `k8s/` | Kubernetes `CronJob`s + a local→EKS walkthrough ([`k8s/LOCAL_K8S.md`](k8s/LOCAL_K8S.md)) and multi-account setup ([`k8s/AWS_MULTI_ACCOUNT.md`](k8s/AWS_MULTI_ACCOUNT.md)) | ### Where manifests come from There's one source: the repo-root `manifests/` — the same folder `paramify tui` @@ -32,7 +31,6 @@ image; rebuild to pick up changes. Name them however makes sense for your schedu `manifests/` is gitignored (your manifests are yours, not committed), so a fresh clone has none — the repo ships ready-to-run samples in `examples/` (also baked in) that every command below uses, so the bundle works before you've built your own. -On Kubernetes the manifest is injected by a `ConfigMap` instead (see [`k8s/`](k8s/)). ## 1. Configure secrets @@ -66,17 +64,16 @@ Three options, in order of preference: also serves as the AWS *fetchers'* identity — they don't use SM, they use the role directly via `auth.passthrough_env`.) -2. **Orchestrator-native injection (cleaner on ECS/EKS).** Skip the entrypoint - fetch and let the platform map SM → env vars: on **ECS**, the task definition's - `secrets:` field (SM ARN → env var); on **EKS**, the External Secrets Operator - or Secrets Store CSI driver (SM → a K8s Secret → `envFrom`). No app change. +2. **Orchestrator-native injection.** Skip the entrypoint fetch and let your + platform map secrets → env vars directly (most schedulers and orchestrators can + inject a secret store into a task's environment). No app change. 3. **Plain values in `deploy/.env`** — fine for local dev; not for production. > Caveats for the long-running `scheduler`: SM is read once at container start, so > rotated secrets need a restart; and the cron env-snapshot writes secrets to a -> file inside the container (`/tmp`). Both are reasons to prefer option 2 + -> CronJobs for production. +> file inside the container (`/tmp`). Both are reasons to prefer option 2 for +> production. ## 2. Build @@ -171,7 +168,7 @@ pf run --rm collector paramify run examples/minimal_run.yaml ls -R evidence/ # 6. (Optional) Full chain — collect AND upload. Hits real Paramify; uses -# PARAMIFY_UPLOAD_API_TOKEN from the secret (PARAMIFY_API_BASE_URL defaults to stage). +# PARAMIFY_UPLOAD_API_TOKEN from the secret (PARAMIFY_API_BASE_URL defaults to production). pf run --rm collector ./deploy/run-and-upload.sh examples/minimal_run.yaml # 7. Confirm no secrets are baked into the image (prints nothing = good). @@ -226,9 +223,6 @@ You don't SSH in — use Docker: in an uploader config if needed (see `examples/upload.yaml`). - **Pin the source.** This image `COPY`s your working tree. For reproducible customer images, build from a tagged commit. -- **Prefer compose/K8s scheduling for production.** In-container cron is fine for - a single host; on Kubernetes use a `CronJob` per manifest (same image, secrets - via K8s Secrets, AWS via IRSA). Apply-and-watch YAML + a local walkthrough live - in [`k8s/`](k8s/) ([`k8s/LOCAL_K8S.md`](k8s/LOCAL_K8S.md)). For the AWS fetchers - specifically — ambient single-account vs. multi-account hub-and-spoke (IRSA + - assume-role + example Terraform) — see [`k8s/AWS_MULTI_ACCOUNT.md`](k8s/AWS_MULTI_ACCOUNT.md). +- **In-container cron is single-host.** It's fine for one machine; for production, + schedule the container with your platform's own scheduler (a managed cron / task + scheduler) so each run is an isolated, ephemeral task. diff --git a/deploy/entrypoint.sh b/deploy/entrypoint.sh index ea30e98..1f10cb8 100755 --- a/deploy/entrypoint.sh +++ b/deploy/entrypoint.sh @@ -10,7 +10,7 @@ cd /app # Default upload host if the caller didn't set one. Must be https (the uploader # refuses to send the bearer token over cleartext to a non-loopback host). -export PARAMIFY_API_BASE_URL="${PARAMIFY_API_BASE_URL:-https://stage.paramify.com/api/v0}" +export PARAMIFY_API_BASE_URL="${PARAMIFY_API_BASE_URL:-https://app.paramify.com/api/v0}" # Read-only `paramify` subcommands only inspect the local fetcher catalog, so # they need no secrets. Skip hydration for them (and for help/empty, which falls diff --git a/deploy/k8s/AWS_MULTI_ACCOUNT.md b/deploy/k8s/AWS_MULTI_ACCOUNT.md deleted file mode 100644 index 0666d7e..0000000 --- a/deploy/k8s/AWS_MULTI_ACCOUNT.md +++ /dev/null @@ -1,128 +0,0 @@ -# AWS on Kubernetes: ambient single-account → multi-account hub-and-spoke - -The AWS fetchers resolve credentials through the **AWS CLI's own credential -chain**. `profile` and `region` are optional manifest fields: - -- **Omit them** → the fetcher uses the *ambient* identity/region of wherever the - Pod runs ("collect the account I'm deployed into"). The smooth single-account path. -- **Set a `profile:` per target** → fanout: the fetcher assumes that account's role - and collects it. One deployment, N accounts. - -No fetcher reads credentials directly and none of this is fetcher code — it's the -manifest (`profile`/`region`), `~/.aws/config` (the profile map), and IAM. - ---- - -## A. Single account, local (kind / Docker Desktop) - -The fetchers need an identity in the Pod. Locally there's no IRSA, so mount a -credentials file. - -```bash -# 1. Re-auth on the host, then capture creds into a [default] profile. -aws sso login # or `aws configure` for static keys — however you auth to the host -printf '[default]\naws_access_key_id = %s\naws_secret_access_key = %s\naws_session_token = %s\n' \ - "$(aws configure get aws_access_key_id)" \ - "$(aws configure get aws_secret_access_key)" \ - "$(aws configure get aws_session_token)" > /tmp/aws-credentials -kubectl create secret generic aws-cli --from-file=credentials=/tmp/aws-credentials && rm /tmp/aws-credentials - -# 2. Upload token + the manifest. -kubectl create secret generic paramify-upload --from-literal=PARAMIFY_UPLOAD_API_TOKEN= -kubectl create configmap aws-manifest --from-file=aws_ambient.yaml=examples/aws_ambient.yaml -``` - -In [`cronjob-aws.yaml`](cronjob-aws.yaml) uncomment the **LOCAL ONLY** `aws-cli` -volume + mount (`/root/.aws/credentials`), then: - -```bash -kubectl apply -f deploy/k8s/cronjob-aws.yaml -kubectl create job --from=cronjob/paramify-aws test-1 -kubectl logs -f job/test-1 -``` - -The `[default]` profile + ambient (no-profile) manifest targets means the CLI uses -those creds directly. The evidence is named `aws__ambient[_].json`, -with the real `account_id` in the metadata. - -> Want to verify the tool itself first, no cluster? From the repo root: -> `aws sso login && paramify run examples/aws_ambient.yaml` — the AWS category -> passthrough lets your ambient creds through, so it collects this account. - ---- - -## B. Multi-account on EKS (hub-and-spoke via IRSA + assume-role) - -One deployment reaches into N accounts. The Pod has a single **HomeRole** (via -IRSA); each target account has a read-only **TargetRole** that trusts HomeRole; -the CLI assumes each in turn. - -```mermaid -flowchart TB - subgraph AWS["AWS IAM — Terraform (deploy/k8s/terraform/)"] - OIDC["cluster OIDC provider"] - HOME["HomeRole (EKS account)
trust: OIDC + ServiceAccount
can: sts:AssumeRole on TargetRoles"] - TA["TargetRole @ Account A
trust: HomeRole"] - TB["TargetRole @ Account B
trust: HomeRole"] - OIDC --> HOME - HOME -. sts:AssumeRole .-> TA - HOME -. sts:AssumeRole .-> TB - end - subgraph K8S["EKS — K8s YAML"] - SA["ServiceAccount
annotation: role-arn = HomeRole"] - CM["ConfigMap aws-config
profile map (assume-role)"] - end - SA -->|IRSA token| HOME - CM -->|~/.aws/config| HOME -``` - -### Steps - -1. **Provision IAM** — [`terraform/`](terraform/): creates HomeRole (IRSA trust) - + a read-only role per target account. Outputs `home_role_arn` and - `target_role_arns`. -2. **Annotate the ServiceAccount** — paste `home_role_arn` into the - `eks.amazonaws.com/role-arn` annotation (PROD SWAP #1) in - [`cronjob-aws.yaml`](cronjob-aws.yaml). Drop the LOCAL `aws-cli` mount. -3. **Profile map** — fill [`aws-config.configmap.yaml`](aws-config.configmap.yaml): - `[profile home]` = HomeRole + the IRSA token file; one `[profile ]` per - account = its TargetRole, `source_profile = home`. Apply it. -4. **Manifest** — give each fetcher a `profile:` per account (see the shipped - sample [`../../examples/multi_region_aws.yaml`](../../examples/multi_region_aws.yaml), - or your own in `manifests/`), and point the CronJob's ConfigMap + `args` at it - instead of `aws_ambient.yaml`. -5. **Image** — point at your registry (PROD SWAP #2), unsuspend the CronJob. - -### What the CLI actually does (per target) - -The fetcher runs `aws ...` with `AWS_PROFILE=acct-prod` (set by the runner from -the target). The CLI: - -1. reads `~/.aws/config` → `acct-prod` = assume `TargetRole`, `source_profile = home`; -2. resolves `home` → reads the **IRSA web-identity token** (a signed JWT) and calls - `sts:AssumeRoleWithWebIdentity` → temp creds for **HomeRole**; -3. signs `sts:AssumeRole(TargetRole)` with those → temp creds for **Account A**; -4. makes the describe/list calls with the Account-A creds. - -The temp creds are minted by **STS** and live only for that one short-lived Pod. -The token file is an *identity assertion*, not credentials — the CLI trades it at -STS. IRSA injects the token file; you provide `~/.aws/config`. - ---- - -## The two prod changes (recap) - -Both flagged inline in [`cronjob-aws.yaml`](cronjob-aws.yaml): -1. **PROD SWAP #1** — replace the local `aws-cli` credentials Secret with the - IRSA-annotated ServiceAccount (HomeRole). No static keys in the cluster. -2. **PROD SWAP #2** — local image → registry image. - -## Troubleshooting - -| Symptom | Fix | -|---|---| -| `Unable to locate credentials` (local) | Re-auth (`aws sso login`) and recreate the `aws-cli` Secret; uncomment the LOCAL mount. | -| `Not authorized to perform sts:AssumeRoleWithWebIdentity` | HomeRole trust policy doesn't match this namespace/ServiceAccount — check the Terraform `k8s_namespace`/`k8s_service_account`. | -| `AccessDenied … sts:AssumeRole … TargetRole` | TargetRole trust policy doesn't allow HomeRole, or HomeRole lacks `sts:AssumeRole` on it — re-apply Terraform. | -| `profile (acct-x) could not be found` | The manifest names a `profile:` with no matching `[profile acct-x]` in the aws-config ConfigMap. | -| `You must specify a region` | A regional fetcher with no region — add `region:` to the target or set `AWS_REGION` (it's set in `cronjob-aws.yaml`). | diff --git a/deploy/k8s/LOCAL_K8S.md b/deploy/k8s/LOCAL_K8S.md deleted file mode 100644 index 7861fe7..0000000 --- a/deploy/k8s/LOCAL_K8S.md +++ /dev/null @@ -1,199 +0,0 @@ -# Run the collector on local Kubernetes (apply-and-watch) - -A `CronJob` spins -up a throwaway Pod, secrets land as env vars, the collector runs collect→upload, -the Pod disappears, evidence is transient. **~90% of this YAML is what runs on -real EKS** — the two prod differences are flagged as `PROD SWAP #1/#2` in -[`cronjob.yaml`](cronjob.yaml) and recapped at the bottom. - -> Not on the beta critical path — this is to build intuition. Budget ~an afternoon. -> All commands run from the **repo root**. - -> **Collecting AWS instead of Okta?** The AWS fetchers use the AWS credential -> chain (ambient single-account, or multi-account assume-role), which is a -> different wiring — see [`AWS_MULTI_ACCOUNT.md`](AWS_MULTI_ACCOUNT.md) and -> [`cronjob-aws.yaml`](cronjob-aws.yaml). - -## How the pieces fit - -| Piece | Local (here) | Real EKS | -|---|---|---| -| **Scheduler** | the `CronJob` controller | same | -| **What runs** | the `:beta` image you loaded | a registry image (SWAP #2) | -| **AWS identity** (reads Secrets Manager) | static creds in the `aws-creds` Secret | IRSA-annotated ServiceAccount (SWAP #1) | -| **App tokens** (Okta, upload) | hydrated from Secrets Manager by `entrypoint.sh` | same | -| **The manifest** | a `ConfigMap`, mounted (not baked in) | same | -| **Evidence** | `emptyDir` — written, uploaded, gone with the Pod | same | - -The `aws-creds` Secret is **not** for an AWS fetcher. Its only job is to let the -entrypoint read AWS Secrets Manager (exactly like the Docker run in -[`../README.md`](../README.md)), which hydrates the -`OKTA_*` + `PARAMIFY_UPLOAD_API_TOKEN` the `minimal_run.yaml` manifest references. On -EKS, IRSA replaces those static creds — that's SWAP #1. - -> This walkthrough schedules the shipped sample `examples/minimal_run.yaml`, which -> collects an Okta target plus two GitLab targets. Without `GITLAB_TOKEN_*` in your -> secret the GitLab targets just report a missing secret — harmless for proving the -> plumbing. Point the CronJob at any manifest you like; your own live in `manifests/` -> (baked into the image), and you swap the ConfigMap source + mount path to match. - -## Prerequisites - -- A cluster is up and `kubectl` points at it (`kubectl get nodes` shows `Ready`). -- The image is visible to the cluster: - - **kind**: `kind load docker-image paramify-fetchers:beta` - - **Docker Desktop**: nothing to do — it shares your local image. (`imagePullPolicy: Never` in the CronJob covers both.) -- The **test Secrets Manager secret** from the Docker run exists (a flat JSON - object of `VAR -> value`, e.g. `{"PARAMIFY_UPLOAD_API_TOKEN":"…","OKTA_API_TOKEN":"…","OKTA_ORG_URL":"…"}`). -- **Temporary AWS creds** handy: `aws configure export-credentials --format env-no-export`. - -> No Secrets Manager secret? Use the **[no-Secrets-Manager alternative](#alternative-no-secrets-manager)** below — put the app tokens straight into a K8s Secret. - -## 1. Create the AWS-creds Secret (the local IRSA stand-in) - -The cleanest way to feed your current creds in without copying/pasting each value: - -```bash -# Pull current temp creds into the shell, then create the Secret from them. -eval "$(aws configure export-credentials --format env-no-export | sed 's/^/export /')" -kubectl create secret generic aws-creds \ - --from-literal=AWS_ACCESS_KEY_ID="$AWS_ACCESS_KEY_ID" \ - --from-literal=AWS_SECRET_ACCESS_KEY="$AWS_SECRET_ACCESS_KEY" \ - --from-literal=AWS_SESSION_TOKEN="$AWS_SESSION_TOKEN" \ - --from-literal=AWS_REGION="${AWS_REGION:-us-east-1}" -``` - -> These creds are short-lived. If a run later fails to read the secret, they -> expired — `kubectl delete secret aws-creds` and re-run this block. - -## 2. Create the manifest ConfigMap - -This is the step that bit earlier — the flag is `--from-file=KEY=PATH`, with an -**`=`** (not `/`) between the key and the path, and the path is from the repo root -(here a shipped sample; for your own use `manifests/.yaml`): - -```bash -kubectl create configmap paramify-manifest \ - --from-file=minimal_run.yaml=examples/minimal_run.yaml -``` - -`minimal_run.yaml` is the key (the filename the Pod sees); `examples/minimal_run.yaml` -is the file on disk. Verify it landed: - -```bash -kubectl describe configmap paramify-manifest # shows a `minimal_run.yaml:` data key -``` - -This is the "manifest via ConfigMap" lesson: edit the ConfigMap (or recreate it -from an edited file) and re-trigger to change what's collected — no image rebuild. - -## 3. Apply the CronJob - -Point `PARAMIFY_SECRETS_ID` at your real SM secret first (edit the value in -[`cronjob.yaml`](cronjob.yaml), or patch it after applying): - -```bash -kubectl apply -f deploy/k8s/cronjob.yaml -# if you didn't edit the file, set your secret id now: -kubectl set env cronjob/paramify-collector PARAMIFY_SECRETS_ID= -``` - -The CronJob is created **suspended** (`suspend: true`) so it won't fire on the -02:00 schedule mid-exercise — you trigger it by hand. - -## 4. Trigger a run on demand - -```bash -kubectl create job --from=cronjob/paramify-collector test-1 -``` - -## 5. Watch the lifecycle - -```bash -kubectl get pods -w # Pending → ContainerCreating → Running → Completed -``` - -In **k9s**: your stuff is in the `default` namespace (press `1` to scope to it, or -`0` for all namespaces — the control plane lives in `kube-system`). Press `l` on -the Pod for logs, `d` to describe. - -Read the logs (works while running or after it completes): - -```bash -kubectl logs -f job/test-1 -``` - -You should see, in order: -1. `[entrypoint] loading secrets from AWS Secrets Manager: …` — the static creds reading SM -2. `==> collect: examples/minimal_run.yaml` — `paramify run` over the ConfigMap manifest -3. `==> upload latest run -> https://stage.paramify.com/api/v0` — the uploader - -> A fetcher that reaches a real tool and **fails with 401/DNS** still proves the -> plumbing (secret hydration → collect → upload). Exit 0 with empty data would be -> the bug. `run-and-upload.sh` uploads partial results even when a fetcher fails. - -## 6. Observe what just happened - -- The Pod went `Completed` and then ages out (per `successfulJobsHistoryLimit`). -- There was **no persistent volume** and **no long-running process** — the Pod was - born to do one collection, shipped the evidence to Paramify, and died. The - evidence on `/app/evidence` is gone with it. That's the model. - -## 7. Teardown - -```bash -kubectl delete -f deploy/k8s/cronjob.yaml # CronJob + ServiceAccount -kubectl delete configmap paramify-manifest -kubectl delete secret aws-creds -kubectl delete job test-1 # if it's still around -# and: kind delete cluster (or disable Docker Desktop → Kubernetes) -``` - -## The two changes for real EKS - -Both are flagged inline in [`cronjob.yaml`](cronjob.yaml): - -1. **`PROD SWAP #1` — identity.** Delete the `aws-creds` Secret and its `envFrom`; - uncomment the `eks.amazonaws.com/role-arn` annotation on the ServiceAccount. - The Pod's AWS identity (which reads Secrets Manager) comes from IRSA, so there - are **no static keys in the cluster**. IRSA is EKS-only — can't be tested here. -2. **`PROD SWAP #2` — image.** Replace `paramify-fetchers:beta` with your registry - image and set `imagePullPolicy: IfNotPresent` (or `Always`). - -Everything else — the CronJob, the ConfigMap-mounted manifest, the env contract, -the `emptyDir` evidence scratch — is identical. - -## Alternative: no Secrets Manager - -If you'd rather not involve SM at all, put the app tokens directly in a K8s Secret -and skip the AWS plumbing: - -```bash -kubectl create secret generic paramify-secrets \ - --from-literal=PARAMIFY_UPLOAD_API_TOKEN=... \ - --from-literal=OKTA_API_TOKEN=... \ - --from-literal=OKTA_ORG_URL=... -``` - -Then in [`cronjob.yaml`](cronjob.yaml): remove the `PARAMIFY_SECRETS_ID` env and -the `aws-creds` `envFrom`, and uncomment the `paramify-secrets` `envFrom` block. -(On real EKS this is the External Secrets Operator / Secrets Store CSI path — -SM → a K8s Secret → `envFrom` — see [`../README.md`](../README.md) option 2.) - -## What to internalize - -- **Kubernetes is the scheduler** (the `CronJob` controller) — there's no host process you run. -- **Each run is an ephemeral Pod** — secrets and evidence live and die with it; no persistent volume, no long-running process. -- **Manifest via ConfigMap** = change what's collected without rebuilding the image. -- **The YAML here is what runs on real EKS**, give or take the two `PROD SWAP`s above (an IRSA-annotated ServiceAccount instead of static creds; a registry image). - -## Troubleshooting - -| Symptom | Likely cause / fix | -|---|---| -| `ImagePullBackOff` / `ErrImageNeverPull` | Image not on the node. kind: `kind load docker-image paramify-fetchers:beta`. Confirm the tag matches `:beta`. | -| Pod stuck `CreateContainerConfigError` | A referenced Secret/ConfigMap is missing. `kubectl describe pod ` names it — you skipped step 1 or 2. | -| `[entrypoint] ERROR: cannot read secret …` | AWS creds expired or wrong region → recreate `aws-creds` (step 1); or wrong `PARAMIFY_SECRETS_ID`. | -| `secret … must be a flat JSON object` | The SM secret isn't `{"VAR":"value", …}` — fix the secret's shape. | -| Fetcher logs "missing secret" | A `${env:VAR}` in `minimal_run.yaml` has no matching key in your SM secret — align the names. | -| k9s shows nothing | You're on the empty `default` namespace before triggering, or looking at `kube-system`. Trigger step 4, then watch `default`. | diff --git a/deploy/k8s/aws-config.configmap.yaml b/deploy/k8s/aws-config.configmap.yaml deleted file mode 100644 index cd208e3..0000000 --- a/deploy/k8s/aws-config.configmap.yaml +++ /dev/null @@ -1,35 +0,0 @@ -# Example ~/.aws/config — the multi-account assume-role "profile map". -# -# Mounted by deploy/k8s/cronjob-aws.yaml at /root/.aws/config. Each manifest -# target's `profile:` names a profile here; the AWS CLI resolves it via its -# credential chain (no keys in this file). HARMLESS for the ambient single- -# account case — if no target names a profile, this file is simply never read. -# -# The `home` profile IS the Pod's IRSA identity (the projected web-identity -# token). Each account profile assumes that account's read-only role using -# `home` as the source. Fill in real account IDs (the Terraform in -# deploy/k8s/terraform/ provisions HomeRole + the per-account TargetRoles). -# -# Apply: kubectl apply -f deploy/k8s/aws-config.configmap.yaml -# (this manifest embeds the example config below — edit the account IDs first). -apiVersion: v1 -kind: ConfigMap -metadata: - name: aws-config -data: - config: | - # Base identity = IRSA. The webhook injects this token file into the Pod. - [profile home] - web_identity_token_file = /var/run/secrets/eks.amazonaws.com/serviceaccount/token - role_arn = arn:aws:iam:::role/paramify-fetchers - - # One profile per target account — assumes that account's read-only role, - # sourced from `home`. The manifest references these by the [profile ] name. - [profile acct-prod] - role_arn = arn:aws:iam:::role/paramify-readonly - source_profile = home - # external_id = # if the TargetRole trust policy requires one - - [profile acct-dev] - role_arn = arn:aws:iam:::role/paramify-readonly - source_profile = home diff --git a/deploy/k8s/cronjob-aws.yaml b/deploy/k8s/cronjob-aws.yaml deleted file mode 100644 index 648ef60..0000000 --- a/deploy/k8s/cronjob-aws.yaml +++ /dev/null @@ -1,111 +0,0 @@ -# Paramify AWS fetchers on Kubernetes. -# -# DEFAULT shape: AMBIENT single-account — the Pod's AWS identity collects the -# account it runs in (no profiles). MULTI-ACCOUNT: name a `profile:` per target in -# the manifest and define it in the aws-config ConfigMap (assume-role fanout). -# Full walkthrough: deploy/k8s/AWS_MULTI_ACCOUNT.md. -# -# The two prod changes are flagged inline as PROD SWAP #1/#2. -# -# Apply order: -# kubectl create secret generic paramify-upload --from-literal=PARAMIFY_UPLOAD_API_TOKEN=... -# kubectl create configmap aws-manifest --from-file=aws_ambient.yaml=examples/aws_ambient.yaml -# kubectl apply -f deploy/k8s/aws-config.configmap.yaml # multi-account only (safe to skip — the cronjob mounts it optionally) -# kubectl apply -f deploy/k8s/cronjob-aws.yaml -# kubectl create job --from=cronjob/paramify-aws test-1 # trigger now ---- -apiVersion: v1 -kind: ServiceAccount -metadata: - name: paramify-fetchers - # ── PROD SWAP #1 (EKS + IRSA) — the Pod's AWS identity ─────────────────────── - # Uncomment to bind this SA to the HomeRole (provision it with - # deploy/k8s/terraform/). The AWS fetchers then use that identity directly for - # the local account, or assume per-account roles via the aws-config profiles — - # NO static keys in the cluster. IRSA is EKS-only (can't be tested on kind). - # annotations: - # eks.amazonaws.com/role-arn: arn:aws:iam:::role/paramify-fetchers ---- -apiVersion: batch/v1 -kind: CronJob -metadata: - name: paramify-aws -spec: - schedule: "0 2 * * *" # 02:00 UTC — pick the cadence you want - suspend: true # manual-trigger only for the walkthrough; unset for a real cadence - concurrencyPolicy: Forbid - successfulJobsHistoryLimit: 3 - failedJobsHistoryLimit: 3 - jobTemplate: - spec: - backoffLimit: 0 # one Pod per trigger - template: - spec: - serviceAccountName: paramify-fetchers - restartPolicy: Never - containers: - - name: collector - # ── PROD SWAP #2 (registry image) ─────────────────────────────── - # Local: the image you `kind load`ed. Prod: a registry image, e.g. - # .dkr.ecr..amazonaws.com/paramify-fetchers: - # with imagePullPolicy: IfNotPresent. - image: paramify-fetchers:beta - imagePullPolicy: Never - # Keep the image ENTRYPOINT; this overrides only the CMD. Collect the - # AWS manifest, then upload (same glue as the Docker scheduler). - args: ["./deploy/run-and-upload.sh", "examples/aws_ambient.yaml"] - env: - # Region for ambient/global calls when a target doesn't set one. - - name: AWS_REGION - value: "us-east-1" - envFrom: - # Paramify upload token (the upload step). AWS *identity* comes from - # IRSA (PROD SWAP #1), not from here. - - secretRef: - name: paramify-upload - volumeMounts: - # The manifest comes from a ConfigMap (edit + re-trigger to change - # what's collected, no rebuild). Overlays the baked-in example; for - # your own, mount over manifests/.yaml instead. - - name: manifest - mountPath: /app/examples/aws_ambient.yaml - subPath: aws_ambient.yaml - readOnly: true - # Multi-account assume-role profile map. Optional (see the volume - # below): a single-account ambient run never reads it, so the - # aws-config ConfigMap may be skipped entirely. subPath keeps - # /root/.aws writable for the CLI's credential cache. - - name: aws-config - mountPath: /root/.aws/config - subPath: config - readOnly: true - # ── LOCAL ONLY (no IRSA): mount static creds as the ambient identity. - # Create the Secret from a credentials file with a [default] profile: - # printf '[default]\naws_access_key_id=...\naws_secret_access_key=...\naws_session_token=...\n' > /tmp/cr - # kubectl create secret generic aws-cli --from-file=credentials=/tmp/cr && rm /tmp/cr - # - name: aws-cli - # mountPath: /root/.aws/credentials - # subPath: credentials - # readOnly: true - # Transient evidence scratch — written, uploaded, gone with the Pod. - - name: evidence - mountPath: /app/evidence - resources: - requests: { cpu: "100m", memory: "256Mi" } - limits: { cpu: "1", memory: "1Gi" } - volumes: - - name: manifest - configMap: - name: aws-manifest - - name: aws-config - configMap: - name: aws-config - # optional: a single-account ambient run never reads this file, so - # an absent ConfigMap must NOT block Pod startup. Without this the - # Pod fails with CreateContainerConfigError when aws-config is skipped. - optional: true - # - name: aws-cli # LOCAL ONLY — see the commented mount above - # secret: - # secretName: aws-cli - - name: evidence - emptyDir: {} diff --git a/deploy/k8s/cronjob.yaml b/deploy/k8s/cronjob.yaml deleted file mode 100644 index 5b0c7f1..0000000 --- a/deploy/k8s/cronjob.yaml +++ /dev/null @@ -1,102 +0,0 @@ -# Paramify fetchers on Kubernetes — scheduled collect→upload as an ephemeral Pod. -# -# This is the LOCAL (kind / Docker Desktop) shape. ~90% of it is identical to -# what runs on EKS; the only two prod changes are flagged inline as -# "PROD SWAP #1/#2" and summarized in deploy/k8s/LOCAL_K8S.md. -# -# Apply order (full walkthrough: deploy/k8s/LOCAL_K8S.md): -# 1. kubectl create secret generic aws-creds \ -# --from-literal=AWS_ACCESS_KEY_ID=... --from-literal=AWS_SECRET_ACCESS_KEY=... \ -# --from-literal=AWS_SESSION_TOKEN=... --from-literal=AWS_REGION=... -# 2. kubectl create configmap paramify-manifest \ -# --from-file=minimal_run.yaml=examples/minimal_run.yaml # note: KEY=PATH, '=' not '/' -# 3. (edit PARAMIFY_SECRETS_ID below) kubectl apply -f deploy/k8s/cronjob.yaml -# 4. kubectl create job --from=cronjob/paramify-collector test-1 # trigger now, don't wait for 02:00 ---- -apiVersion: v1 -kind: ServiceAccount -metadata: - name: paramify-fetchers - # ── PROD SWAP #1 (EKS + IRSA) ─────────────────────────────────────────────── - # On EKS: give this ServiceAccount an IAM role via IRSA (annotation below) and - # DELETE the `aws-creds` Secret + its `envFrom` further down. The Pod then gets - # its AWS identity — used to read AWS Secrets Manager — from the role instead of - # static keys in the cluster. IRSA is EKS-only and cannot be tested locally. - # annotations: - # eks.amazonaws.com/role-arn: arn:aws:iam:::role/paramify-fetchers ---- -apiVersion: batch/v1 -kind: CronJob -metadata: - name: paramify-collector -spec: - schedule: "0 2 * * *" # 02:00 UTC — pick the cadence you want. - suspend: true # manual-trigger only for this exercise (no surprise - # 02:00 run). Set false / remove for a real cadence. - concurrencyPolicy: Forbid # never overlap a still-running collection - successfulJobsHistoryLimit: 3 # keep a few finished Pods around so you can read logs - failedJobsHistoryLimit: 3 - jobTemplate: - spec: - backoffLimit: 0 # exactly one Pod per trigger — no retry storm while learning - template: - spec: - serviceAccountName: paramify-fetchers - restartPolicy: Never - containers: - - name: collector - # ── PROD SWAP #2 (registry image) ─────────────────────────────── - # Local: the image you `kind load`ed (or built into Docker Desktop). - # Prod: a registry image, e.g. - # .dkr.ecr..amazonaws.com/paramify-fetchers: - # with imagePullPolicy: IfNotPresent (or Always). - image: paramify-fetchers:beta - imagePullPolicy: Never # use the locally-loaded image; never pull - # We KEEP the image's ENTRYPOINT (entrypoint.sh) so it still: - # (a) hydrates secrets from AWS Secrets Manager when PARAMIFY_SECRETS_ID is set, and - # (b) defaults PARAMIFY_API_BASE_URL (https://stage.paramify.com/api/v0). - # Setting `args` overrides only the CMD, so the entrypoint runs this: - args: ["./deploy/run-and-upload.sh", "examples/minimal_run.yaml"] - env: - # The SM secret (a flat JSON object of VAR->value) the entrypoint - # hydrates — PARAMIFY_UPLOAD_API_TOKEN, OKTA_API_TOKEN, OKTA_ORG_URL, … - # This is an ID/ARN, not a secret value, so plain env is fine. EDIT IT. - - name: PARAMIFY_SECRETS_ID - value: "paramify/fetchers/beta" - envFrom: - # Static AWS creds = the LOCAL stand-in for IRSA. Their only job is - # to let the entrypoint read AWS Secrets Manager. DELETE for PROD SWAP #1. - - secretRef: - name: aws-creds - # ── ALTERNATIVE (no Secrets Manager) ───────────────────────────── - # Skip SM entirely: drop PARAMIFY_SECRETS_ID + the aws-creds ref - # above, create a Secret holding the app tokens directly, and use it: - # kubectl create secret generic paramify-secrets \ - # --from-literal=PARAMIFY_UPLOAD_API_TOKEN=... \ - # --from-literal=OKTA_API_TOKEN=... --from-literal=OKTA_ORG_URL=... - # - secretRef: - # name: paramify-secrets - volumeMounts: - # Manifests are baked into the image, but a ConfigMap mounted over - # one lets you change what's collected by editing the ConfigMap + - # re-triggering — no image rebuild. Here it overlays the baked - # examples/minimal_run.yaml; for your own, mount over manifests/.yaml. - - name: manifest - mountPath: /app/examples/minimal_run.yaml - subPath: minimal_run.yaml - readOnly: true - # Transient scratch for collected evidence: written here, shipped to - # Paramify by run-and-upload.sh, then GONE when the Pod ends. There is - # no PersistentVolume — the upload is what persists evidence, not the - # cluster. This is the whole point of the ephemeral-Pod model. - - name: evidence - mountPath: /app/evidence - resources: # friendly starting points; tune for prod - requests: { cpu: "100m", memory: "256Mi" } - limits: { cpu: "1", memory: "1Gi" } - volumes: - - name: manifest - configMap: - name: paramify-manifest - - name: evidence - emptyDir: {} diff --git a/deploy/k8s/terraform/.terraform.lock.hcl b/deploy/k8s/terraform/.terraform.lock.hcl deleted file mode 100644 index cdc1668..0000000 --- a/deploy/k8s/terraform/.terraform.lock.hcl +++ /dev/null @@ -1,25 +0,0 @@ -# This file is maintained automatically by "terraform init". -# Manual edits may be lost in future updates. - -provider "registry.terraform.io/hashicorp/aws" { - version = "5.100.0" - constraints = "~> 5.0" - hashes = [ - "h1:Ijt7pOlB7Tr7maGQIqtsLFbl7pSMIj06TVdkoSBcYOw=", - "zh:054b8dd49f0549c9a7cc27d159e45327b7b65cf404da5e5a20da154b90b8a644", - "zh:0b97bf8d5e03d15d83cc40b0530a1f84b459354939ba6f135a0086c20ebbe6b2", - "zh:1589a2266af699cbd5d80737a0fe02e54ec9cf2ca54e7e00ac51c7359056f274", - "zh:6330766f1d85f01ae6ea90d1b214b8b74cc8c1badc4696b165b36ddd4cc15f7b", - "zh:7c8c2e30d8e55291b86fcb64bdf6c25489d538688545eb48fd74ad622e5d3862", - "zh:99b1003bd9bd32ee323544da897148f46a527f622dc3971af63ea3e251596342", - "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", - "zh:9f8b909d3ec50ade83c8062290378b1ec553edef6a447c56dadc01a99f4eaa93", - "zh:aaef921ff9aabaf8b1869a86d692ebd24fbd4e12c21205034bb679b9caf883a2", - "zh:ac882313207aba00dd5a76dbd572a0ddc818bb9cbf5c9d61b28fe30efaec951e", - "zh:bb64e8aff37becab373a1a0cc1080990785304141af42ed6aa3dd4913b000421", - "zh:dfe495f6621df5540d9c92ad40b8067376350b005c637ea6efac5dc15028add4", - "zh:f0ddf0eaf052766cfe09dea8200a946519f653c384ab4336e2a4a64fdd6310e9", - "zh:f1b7e684f4c7ae1eed272b6de7d2049bb87a0275cb04dbb7cda6636f600699c9", - "zh:ff461571e3f233699bf690db319dfe46aec75e58726636a0d97dd9ac6e32fb70", - ] -} diff --git a/deploy/k8s/terraform/README.md b/deploy/k8s/terraform/README.md deleted file mode 100644 index f4d40ea..0000000 --- a/deploy/k8s/terraform/README.md +++ /dev/null @@ -1,76 +0,0 @@ -# Terraform: the AWS hub-and-spoke IAM for the collector - -Provisions the IAM side of [`../AWS_MULTI_ACCOUNT.md`](../AWS_MULTI_ACCOUNT.md): - -- **HomeRole** in the EKS account — the Pod's base identity, assumed via IRSA - (the cluster OIDC provider → this ServiceAccount). Created in `home_role.tf`. -- **A read-only role in each target account** that trusts HomeRole to assume it. - The `modules/target_account` module; instantiate once per account. -- HomeRole gets `sts:AssumeRole` on those target roles + broad read-only - (`SecurityAudit` + `ViewOnlyAccess`) for same-account collection. - -This is an **example** — scope the read-only policies down for production. - -## Inputs - -| Variable | What | -|---|---| -| `eks_oidc_provider_arn` | Your cluster's IAM OIDC provider ARN | -| `eks_oidc_provider_url` | OIDC issuer URL **without** `https://` | -| `target_accounts` | `{ name = "account_id" }` — the spoke accounts (name = the manifest `profile:`) | -| `k8s_namespace` / `k8s_service_account` | Where the collector runs (defaults: `default` / `paramify-fetchers`) | - -Get the OIDC values from the cluster: - -```bash -aws eks describe-cluster --name --query cluster.identity.oidc.issuer --output text -# -> https://oidc.eks..amazonaws.com/id/XXXX (strip https:// for the URL var; -# the provider ARN is the IAM OIDC provider you registered for that issuer) -``` - -## Multi-account: one provider per spoke - -Terraform providers can't be `for_each`'d, so each target account needs its own -**aliased provider** (assuming a role you can already use there, e.g. -`OrganizationAccountAccessRole`) and an explicit module block. In `providers.tf`: - -```hcl -provider "aws" { - alias = "prod" - region = var.region - assume_role { role_arn = "arn:aws:iam:::role/OrganizationAccountAccessRole" } -} -``` - -Then replace the `for_each` `module "target"` in `target_roles.tf` with one block -per account, wiring the provider: - -```hcl -module "target_prod" { - source = "./modules/target_account" - providers = { aws = aws.prod } - role_name = var.readonly_role_name - home_role_arn = aws_iam_role.home.arn -} -``` - -(The shipped `for_each` form validates and is correct for a single-account demo — -all roles land in the default provider's account.) - -## Apply - -```bash -terraform init -terraform apply \ - -var 'eks_oidc_provider_arn=arn:aws:iam:::oidc-provider/oidc.eks..amazonaws.com/id/XXXX' \ - -var 'eks_oidc_provider_url=oidc.eks..amazonaws.com/id/XXXX' \ - -var 'target_accounts={ acct-prod = "111111111111", acct-dev = "222222222222" }' -``` - -## Wire the outputs back - -- `home_role_arn` → the ServiceAccount annotation in - [`../cronjob-aws.yaml`](../cronjob-aws.yaml) (PROD SWAP #1) **and** the - `[profile home] role_arn` in [`../aws-config.configmap.yaml`](../aws-config.configmap.yaml). -- `target_role_arns` → the `role_arn` of each `[profile ]` in the - aws-config ConfigMap. diff --git a/deploy/k8s/terraform/home_role.tf b/deploy/k8s/terraform/home_role.tf deleted file mode 100644 index 7987dd9..0000000 --- a/deploy/k8s/terraform/home_role.tf +++ /dev/null @@ -1,67 +0,0 @@ -# HomeRole — the Pod's base identity, lives in the EKS account. IRSA lets the -# collector's ServiceAccount assume it via the cluster's OIDC provider (no keys). - -locals { - oidc_sub = "system:serviceaccount:${var.k8s_namespace}:${var.k8s_service_account}" - target_role_arns = [ - for id in values(var.target_accounts) : - "arn:aws:iam::${id}:role/${var.readonly_role_name}" - ] -} - -# Trust policy: the cluster OIDC provider may assume HomeRole, but only for OUR -# ServiceAccount (sub) and the STS audience. -data "aws_iam_policy_document" "home_trust" { - statement { - effect = "Allow" - actions = ["sts:AssumeRoleWithWebIdentity"] - principals { - type = "Federated" - identifiers = [var.eks_oidc_provider_arn] - } - condition { - test = "StringEquals" - variable = "${var.eks_oidc_provider_url}:sub" - values = [local.oidc_sub] - } - condition { - test = "StringEquals" - variable = "${var.eks_oidc_provider_url}:aud" - values = ["sts.amazonaws.com"] - } - } -} - -resource "aws_iam_role" "home" { - name = var.home_role_name - assume_role_policy = data.aws_iam_policy_document.home_trust.json -} - -# Spoke access: HomeRole may assume each target account's read-only role. -data "aws_iam_policy_document" "home_assume_targets" { - count = length(local.target_role_arns) > 0 ? 1 : 0 - statement { - effect = "Allow" - actions = ["sts:AssumeRole"] - resources = local.target_role_arns - } -} - -resource "aws_iam_role_policy" "home_assume_targets" { - count = length(local.target_role_arns) > 0 ? 1 : 0 - name = "assume-target-readonly" - role = aws_iam_role.home.id - policy = data.aws_iam_policy_document.home_assume_targets[0].json -} - -# Same-account collection: if the collector also reads the EKS account itself -# (ambient, no assume-role), HomeRole needs read perms directly. Scope down for prod. -resource "aws_iam_role_policy_attachment" "home_securityaudit" { - role = aws_iam_role.home.name - policy_arn = "arn:aws:iam::aws:policy/SecurityAudit" -} - -resource "aws_iam_role_policy_attachment" "home_viewonly" { - role = aws_iam_role.home.name - policy_arn = "arn:aws:iam::aws:policy/job-function/ViewOnlyAccess" -} diff --git a/deploy/k8s/terraform/modules/target_account/main.tf b/deploy/k8s/terraform/modules/target_account/main.tf deleted file mode 100644 index decb868..0000000 --- a/deploy/k8s/terraform/modules/target_account/main.tf +++ /dev/null @@ -1,36 +0,0 @@ -# A read-only role in a target (spoke) account that trusts HomeRole to assume it. -# Instantiate once per account, with an aws provider scoped to that account. - -data "aws_iam_policy_document" "trust" { - statement { - effect = "Allow" - actions = ["sts:AssumeRole"] - principals { - type = "AWS" - identifiers = [var.home_role_arn] - } - # Optional hardening (confused-deputy): require a shared ExternalId, and set - # the same external_id in the aws-config profile. - # condition { - # test = "StringEquals" - # variable = "sts:ExternalId" - # values = [""] - # } - } -} - -resource "aws_iam_role" "readonly" { - name = var.role_name - assume_role_policy = data.aws_iam_policy_document.trust.json -} - -# Broad read-only coverage for the fetchers. Scope down to least privilege for prod. -resource "aws_iam_role_policy_attachment" "securityaudit" { - role = aws_iam_role.readonly.name - policy_arn = "arn:aws:iam::aws:policy/SecurityAudit" -} - -resource "aws_iam_role_policy_attachment" "viewonly" { - role = aws_iam_role.readonly.name - policy_arn = "arn:aws:iam::aws:policy/job-function/ViewOnlyAccess" -} diff --git a/deploy/k8s/terraform/modules/target_account/outputs.tf b/deploy/k8s/terraform/modules/target_account/outputs.tf deleted file mode 100644 index af22ec7..0000000 --- a/deploy/k8s/terraform/modules/target_account/outputs.tf +++ /dev/null @@ -1,4 +0,0 @@ -output "role_arn" { - description = "ARN of the read-only role created in this account." - value = aws_iam_role.readonly.arn -} diff --git a/deploy/k8s/terraform/modules/target_account/variables.tf b/deploy/k8s/terraform/modules/target_account/variables.tf deleted file mode 100644 index 97328ff..0000000 --- a/deploy/k8s/terraform/modules/target_account/variables.tf +++ /dev/null @@ -1,9 +0,0 @@ -variable "role_name" { - description = "Name of the read-only role to create in this account." - type = string -} - -variable "home_role_arn" { - description = "ARN of the HomeRole allowed to assume this role." - type = string -} diff --git a/deploy/k8s/terraform/outputs.tf b/deploy/k8s/terraform/outputs.tf deleted file mode 100644 index c19a16d..0000000 --- a/deploy/k8s/terraform/outputs.tf +++ /dev/null @@ -1,14 +0,0 @@ -output "home_role_arn" { - description = "Use in the ServiceAccount annotation (PROD SWAP #1) and the aws-config [profile home] role_arn." - value = aws_iam_role.home.arn -} - -output "service_account_annotation" { - description = "Copy into deploy/k8s/cronjob-aws.yaml's ServiceAccount." - value = "eks.amazonaws.com/role-arn: ${aws_iam_role.home.arn}" -} - -output "target_role_arns" { - description = "Per-account read-only role ARNs — feed into the aws-config profiles." - value = { for k, m in module.target : k => m.role_arn } -} diff --git a/deploy/k8s/terraform/providers.tf b/deploy/k8s/terraform/providers.tf deleted file mode 100644 index 8a6383f..0000000 --- a/deploy/k8s/terraform/providers.tf +++ /dev/null @@ -1,17 +0,0 @@ -provider "aws" { - region = var.region -} - -# For MULTI-ACCOUNT collection, add one ALIASED provider per target account that -# assumes a role you can already use there (e.g. the Organizations-managed -# OrganizationAccountAccessRole), and pass it to the target module (see -# target_roles.tf + README.md). Terraform providers can't be for_each'd, so this -# is one static block per account. Example: -# -# provider "aws" { -# alias = "prod" -# region = var.region -# assume_role { -# role_arn = "arn:aws:iam:::role/OrganizationAccountAccessRole" -# } -# } diff --git a/deploy/k8s/terraform/target_roles.tf b/deploy/k8s/terraform/target_roles.tf deleted file mode 100644 index c93558e..0000000 --- a/deploy/k8s/terraform/target_roles.tf +++ /dev/null @@ -1,21 +0,0 @@ -# Read-only role in each target account, trusting HomeRole to assume it (spokes). -# -# Each target role lives in a DIFFERENT account, so in real use you pass an -# ALIASED provider per account (providers can't be for_each'd) — one block each: -# -# module "target_prod" { -# source = "./modules/target_account" -# providers = { aws = aws.prod } -# role_name = var.readonly_role_name -# home_role_arn = aws_iam_role.home.arn -# } -# -# The for_each form below validates and is correct for a SINGLE-account demo -# (every role created in the default provider's account). Replace it with explicit -# per-account module blocks (above) for true multi-account. -module "target" { - source = "./modules/target_account" - for_each = var.target_accounts - role_name = var.readonly_role_name - home_role_arn = aws_iam_role.home.arn -} diff --git a/deploy/k8s/terraform/variables.tf b/deploy/k8s/terraform/variables.tf deleted file mode 100644 index c77c844..0000000 --- a/deploy/k8s/terraform/variables.tf +++ /dev/null @@ -1,52 +0,0 @@ -variable "region" { - description = "Region for the AWS providers (any region; IAM is global)." - type = string - default = "us-east-1" -} - -# --- The EKS cluster's OIDC provider (where the HomeRole lives) ---------------- -# Get these from your cluster, e.g.: -# aws eks describe-cluster --name --query cluster.identity.oidc.issuer --output text -# The ARN is the IAM OIDC identity provider you registered for that issuer. -variable "eks_oidc_provider_arn" { - description = "ARN of the cluster's IAM OIDC identity provider." - type = string -} - -variable "eks_oidc_provider_url" { - description = "Cluster OIDC issuer URL WITHOUT the https:// prefix (e.g. oidc.eks.us-east-1.amazonaws.com/id/ABC123)." - type = string -} - -variable "k8s_namespace" { - description = "Namespace the collector runs in." - type = string - default = "default" -} - -variable "k8s_service_account" { - description = "ServiceAccount name the CronJob uses (matches deploy/k8s/cronjob-aws.yaml)." - type = string - default = "paramify-fetchers" -} - -variable "home_role_name" { - description = "Name of the HomeRole created in the EKS account (the Pod's base identity)." - type = string - default = "paramify-fetchers" -} - -variable "readonly_role_name" { - description = "Name of the read-only role created in EACH target account." - type = string - default = "paramify-readonly" -} - -# --- Target accounts to collect from ----------------------------------------- -# name => 12-digit account id. The name is what you use as the `profile:` in the -# manifest and in deploy/k8s/aws-config.configmap.yaml. -variable "target_accounts" { - description = "Map of profile name => target AWS account id." - type = map(string) - default = {} -} diff --git a/deploy/k8s/terraform/versions.tf b/deploy/k8s/terraform/versions.tf deleted file mode 100644 index 5fd43e4..0000000 --- a/deploy/k8s/terraform/versions.tf +++ /dev/null @@ -1,9 +0,0 @@ -terraform { - required_version = ">= 1.3" - required_providers { - aws = { - source = "hashicorp/aws" - version = "~> 5.0" - } - } -} diff --git a/docs/design.md b/docs/design.md index dd866e1..2b8a704 100644 --- a/docs/design.md +++ b/docs/design.md @@ -448,7 +448,7 @@ What's deferred: ## Near-term deployment: the containerized bundle -The MVP deployment is the bundle in `deploy/` — a Docker image run on a schedule (compose/cron on a single host; a Kubernetes `CronJob` per cadence in-cluster). The collector runs collect→upload and the pod/container is transient. Secrets are hydrated at startup from the environment (or AWS Secrets Manager when `PARAMIFY_SECRETS_ID` is set); AWS auth uses the ambient credential chain (IRSA / instance role in-cluster, a named profile locally), with optional multi-account assume-role fanout. See [`deploy/README.md`](../deploy/README.md) and [`deploy/k8s/`](../deploy/k8s/). +The MVP deployment is the bundle in `deploy/` — a Docker image run on a schedule (compose/cron on a single host). The collector runs collect→upload and the container is transient. Secrets are hydrated at startup from the environment (or AWS Secrets Manager when `PARAMIFY_SECRETS_ID` is set); AWS auth uses the ambient credential chain (an instance role / IRSA in cloud, a named profile locally). See [`deploy/README.md`](../deploy/README.md). Two principles this honors, so the deployment doesn't constrain the eventual framework: