Clouds Against Humanity
A small, cheap, locked-down DigitalOcean environment built with OpenTofu, applied by Spacelift, fed secrets by Doppler, and reachable only through Twingate. No droplet has a single public inbound port open. There is no VPN, no bastion host and no SSH on the internet.
It is a cloud extension of a homelab, and it exists to teach: this repository is the reference environment, and the lessons that use it live in the wiki.
### Start with the wiki
> The wiki is the real point of this project. Every lesson, flowchart, exercise and decision (including the ones that were wrong the first time) is written there; this repository is the environment those lessons use.
> - Wiki home: the map of everything.
- Start here if you are new: cloud, Terraform and zero trust from zero.
- Why this exists: where this comes from and who it is for.
- Reading order: the paths, with time estimates.
- Contents: every page once, grouped by theme, with what each is for.
- Git from zero: if you have never used git.
- Repository growth: stars, forks and weekly work, drawn from GitHub data; the as-of date is printed in the picture, and stars measure attention, not use.
> Who this assumes: a basic power user. You know how a computer works, may have built or repaired one, and have set up a router and done basic networking. If you could work a help desk or a retail repair counter, you can start; if not, IT 101 level material such as CompTIA A+ objectives is the benchmark (not an endorsement).
> Cost: the reading and the exercises are free and always will be, and nothing here is sold. Hands-on work uses your own accounts and your provider's bill, nothing you spend goes to the maintainer, and you choose what you can afford to run. The cost gates are explained in docs/cost.md. If you open a DigitalOcean account, my referral link is optional and nothing is gated behind it; the wiki says exactly what it is. The same pages are in docs/wiki/ if you prefer reading them in the repo.
### Found a mistake or have a question? Open an issue
> Something wrong, a command that failed, a confusing explanation, or a topic you wish was here? Please say so. Feedback is free, and it is how the lessons get better. Give feedback and open an issue walks through it for a first-timer. Never paste tokens, keys, account names, real IP addresses or hostnames, or anything that identifies you. Security problems go through SECURITY.md, not a public issue. See also CONTRIBUTING.md.
Free, and not for sale
I grew up in a divorced home, struggling to find resources to level up my tech knowledge, and my whole career was built on second hand machines. My first brand new computer, a System76 Pangolin I still have, came roughly ten years in. If knowledge is power and getting to it costs money, then the power is only available to people who can afford it, and I refuse to be selfish and enrich myself by gatekeeping what everyone has a right to know. No bullshit, no "90K in 90 days" lies, no "you need this cert or degree." Just knowledge.
You can tip, donate or support me financially through Patreon, StreamElements or the cryptocurrency section, but you are under no obligation. The biggest way to support this project is to share it with everyone who needs it. The full version, including what I think of college and certs, is Why this is free and not for sale.
Recommending other springboards is encouraged throughout this project. The point is to get you thinking in different ways and outside the box, not to hand you one path to copy, so expect pages to point at things to try beyond what they cover.
How it fits together
!Architecture: Twingate connectors dial out, nothing listens inbound, Spacelift applies OpenTofu
Diagram source (Mermaid)
flowchart LR
you["You<br/>Twingate client"]:::you
tg["Twingate<br/>controller + relays"]:::ctrl
subgraph vpc["DigitalOcean VPC (tor1)"]
ca["connector a"]:::cloud
cb["connector b"]:::cloud
end
net["Public internet"]:::ext
subgraph cp["Control plane"]
gh["GitHub PR + CI"]:::ctrl
sl["Spacelift<br/>plan, human confirms, apply"]:::ctrl
dp["Doppler<br/>every secret"]:::secret
end
you -- "per-resource access" --> tg
ca -- "outbound only" --> tg
cb -- "outbound only" --> tg
you == "end-to-end encrypted" ==> ca
net -. "no inbound rule exists" .-> vpc
gh --> sl
dp -- "read-only token" --> sl
sl -- "OpenTofu" --> vpc
classDef you fill:#132a1f,stroke:#4ade80,color:#dcfce7
classDef ext fill:#3b1d1d,stroke:#f87171,color:#fde2e2
classDef ctrl fill:#1e1b3a,stroke:#a78bfa,color:#ede9fe
classDef cloud fill:#0f2a3d,stroke:#38bdf8,color:#e0f2fe
classDef secret fill:#3a2a0f,stroke:#fbbf24,color:#fef3c7
| Piece | Job | Why this and not the obvious alternative | |---|---|---| | Twingate (ZTNA) | Gets an authenticated person to one specific host and port | Connectors dial out, so nothing listens on the internet. A VPN would mean a public listener and network-wide access after one login. More | | DigitalOcean | Droplets, VPC, cloud firewalls | Predictable pricing and a firewall that attaches by tag, so a droplet is protected from its first boot. More | | OpenTofu | Every resource as code, in layered stacks | Open-source (MPL) Terraform fork; same language, no license surprise. More | | Spacelift | Runs plan and apply, holds state, waits for a human | Nothing applies from a laptop or from CI, and pull request plans never see credentials. More | | Doppler | Holds every secret | Spacelift holds one read-only token; nothing in this repo is a credential. More | | GitHub Actions | Tests, scanners, cost gate | Calls hardened shared workflows, every action pinned by commit. More |
Why not just a VPN?
Because a remote-access VPN authenticates you once and then puts your device on the network, and the box doing that authentication has to listen on the internet. Both halves of that have been the starting point of serious breaches: stolen credentials on a legacy single-factor VPN profile (Colonial Pipeline, 2021) and a long run of pre-authentication exploits in VPN appliances from Ivanti, Fortinet, Palo Alto, Citrix and Pulse Secure. ZTNA checks every connection against a per-resource policy and, as built here, leaves nothing listening at all.
That is not "VPNs are always wrong". WireGuard is a very different animal from an SSL VPN appliance, site-to-site tunnels are still fine, and ZTNA means trusting a vendor's control plane. The full argument, sources and the honest tradeoffs are on Why ZTNA and not a VPN.
Status
| Stage | What | Monthly | Status |
|---|---|---|---|
| 0 | Foundation: project, VPC, baseline firewall with zero inbound rules | $0 | Built |
| 1 | Access: two Twingate connectors, connector firewall, per-host SSH resources | $12 | Built |
| 2 | Logs to the on-prem SIEM, cloud-to-home access | $0 in the cloud | In progress: the cloud-side code (settings layer, telemetry stack, opt-in node exporter, Wazuh agent and auditd, dashboards) is merged but nothing is applied |
| 3 | Workload host: Portainer, five daemons, Spaces bucket, Functions | est. $17 | Planned |
| 4 | Grafana, on-prem | $0 in the cloud | Planned (a cloud Grafana is a learner option, see Build your own mini SOC) |
| Edge | Public edge (DMZ) and hardening | past the ceiling (design option A adds a $6 droplet) | Shared network code written, disabled by default and never applied (stacks/50-edge creates nothing until the stage is approved); each service becomes its own stack later (stage page) |
| AWS | The AWS twin of stages 0 and 1: modules/aws/, stacks/aws/ | $0 and $20.84 | Written and tested with mocked providers, never applied. Azure and GCP have no code |
Working ceiling: $30 per month in the current phase (stage 3), shared by every cloud; each phase carries its own ceiling in docs/stages/approved.json, so a later phase can raise it on purpose. A cost gate in CI fails any change that raises a cloud's planned total above its approved total, or the active clouds' approved totals above the current phase's ceiling (the AWS twin is marked inactive, because it does not fit beside stage 1). See the roadmap.
Growth
Stars measure attention, not use, but the project is young enough that the curve is worth a look. The chart is drawn by a daily workflow from GitHub data (no chart service), and the as-of date is printed in the picture; the Repository growth page has the numbers as a table.
Layout
Clouds-Against-Humanity/
stacks/00-foundation/ project, tag, VPC, baseline firewall
stacks/10-access/ site, SSH key lookup, two connectors, connector firewall, group, SSH resources
stacks/20-telemetry/ stage 2 telemetry settings layer (see docs/stages/02-telemetry.md)
stacks/30-workloads/ stage 3 workload host: tag, firewall (inbound from the connector tag only), one Docker and Portainer droplet, two Twingate resources; disabled by default, never applied (see docs/stages/03-workloads.md)
stacks/50-edge/ the shared edge network (DMZ): VPC, tags, Cloud Firewall; disabled by default, never applied (see docs/stages/05-edge.md)
stacks/aws/{00-foundation,10-access} the AWS twin of stages 0 and 1 (mocked tests only, never applied)
modules/vpc/ per-region VPC
modules/firewall/ tag-based deny-inbound firewall
modules/twingate-bootstrap/ shared connector boot script and tokens, called by every provider's connector
modules/twingate-connector/ connector, its tokens, its droplet and cloud-init
modules/edge-host/ reusable edge host: droplet, reserved IP, project attachment and a small cloud-init (no SSH, no connector, no service)
modules/docker-host/ hardened Docker host: one droplet, Docker Engine and Portainer CE from cloud-init (no connector, no public inbound)
modules/spaces-bucket/ private versioned Spaces bucket for daemon state, off by default, not approved
modules/aws/{vpc,firewall,twingate-connector} the AWS versions of those modules
dashboards/ importable Grafana dashboards (sites overview, droplet health, Wazuh agents); see docs/wiki/Grafana-Dashboards.md
scripts/cost_check.py cost gate
scripts/ci_cost_gate.sh the cost gate as CI runs it: compares approved.json with the base branch on pull requests
scripts/cost_raise.py proposes (dry run) or makes a per-phase ceiling change in approved.json
scripts/identifier_check.py fails on cloud account ids in tracked files (never prints them)
scripts/gen_settings_doc.py writes docs/wiki/Settings-Reference.md from docs/settings.json (--check fails when stale)
scripts/gen_project_status.py writes docs/wiki/Project-Status.md from docs/wiki/.status-snapshot.json (--check fails when stale; --refresh fetches live data with the maintainer login)
scripts/gen_repo_metrics.py draws docs/wiki/images/repo-growth.png and writes docs/wiki/Repository-Growth.md from docs/wiki/.metrics-snapshot.json (--check is offline and needs no plotting library; --refresh fetches per-day counts with gh and needs matplotlib (run locally it reads every star time; in CI it runs --no-star-times and records one daily star count in a ledger); --verify-bundle checks what the daily workflow publishes; --repo, --snapshot, --out and --page reuse it for another repository)
scripts/carry_metrics_snapshot.sh the carry half: reads the previous snapshot from the metrics branch (size checked before anything is written; fails closed) and prints the tip commit it read
scripts/publish_metrics_branch.sh the publish half of the daily growth chart: re-verifies the bundle against the baseline and the previous snapshot, then fast-forward pushes a child commit of the tip it was told to expect to the metrics branch (no force, never main)
scripts/growth-chart-requirements.txt hash-pinned matplotlib and Pillow for the daily growth chart (pip install --require-hashes)
scripts/gen_wiki_nav.py writes docs/wiki/Contents.md and docs/wiki/_Sidebar.md from docs/wiki/_nav.yaml (--check fails when stale or when a page is missing from the manifest)
scripts/gen_example_tests.py writes each live stack's example.tftest.hcl from its terraform.tfvars.example
scripts/gen_timezones.py regenerates modules/twingate-bootstrap/timezones.txt from the local tz database
scripts/hcl_variables.py helper module: a fail-closed reader of variable blocks (used by tests and the generators)
scripts/plan_noop_check.py fails unless a saved plan is a no-op over the golden addresses (run by hand, not in CI)
scripts/zero_ports_check.py external zero-open-ports check of the connectors: targets only from the DigitalOcean API, no address argument (run by hand with Doppler, not in CI)
scripts/threat_dragon_layout.py lays out the Threat Dragon models in docs/threat-model (nodes, boundary boxes, flow routes; --check fails when stale)
scripts/spacelift/doppler-env.sh hook that loads Doppler secrets into a run
scripts/render_diagrams.py renders every Mermaid diagram to a dark-mode PNG with its source kept underneath (--check fails when a picture is stale)
scripts/gen-wiki.sh writes the wiki pages and copies docs/wiki/images from docs/wiki (the Wiki workflow publishes them)
scripts/strip_png_metadata.py removes text, EXIF and time chunks from wiki screenshots (--check fails when any remain)
scripts/demo_monitoring.sh runs the synthetic multi-site monitoring demo in labs/demo-monitoring (up, down, silent-site, test, screenshots)
tests/ pytest for the cost gate, settings, module policy, secret separation, the hook, the wiki and more
.spacelift/config.yml which secrets each stack loads
.github/workflows/ci.yml GYST callers only (tofu, python, security) + CI green
.github/workflows/wiki.yml publishes docs/wiki through GYST's wiki-publish.yml
.github/ISSUE_TEMPLATE/ issue forms (lesson feedback, command failed, question, topic request) and config.yml
SECURITY.md, CONTRIBUTING.md private security reporting; how to give feedback and contribute
.spacelift/policies/ Spacelift push and plan policies and their OPA tests
labs/demo-monitoring/ synthetic Prometheus + Grafana demo: two sites, dashboards, alert rules and their promtool tests (no real hosts)
docs/wiki/ the wiki pages (source of truth)
docs/stages/ operator guides: decisions, verify, rollback, per stage (00-foundation, 01-access, 02-telemetry, aws-access)
docs/cost.md cost gate explained
docs/cost-prices.json, docs/cost-prices/ price tables (DigitalOcean, and one file per other cloud)
docs/diagrams.json, docs/diagrams-theme.json source hash of each diagram picture, and the dark theme it is rendered with
docs/settings.json the settings manifest the Settings reference is generated from
docs/threat-model/ the OWASP Threat Dragon model
docs/golden-addresses/ resource addresses the plan no-op check compares against
docs/superpowers/ dated design records (specs and plans), not maintained as guides
docs/spacelift-secrets.md how secrets reach a run, rotation, upgrades
board.yaml the project board as code (Scrum Around and Find Out): fields, sprints, views
media/ icons and images for the README's Support section
Modules are reusable building blocks. Stacks are the things you actually apply, one Spacelift stack each. Each module and stack has its tests in .
Run the checks
Install OpenTofu 1.8 or newer, Python 3 and, for the policy tests, OPA 1.21.0, then:
# cost gate: totals the planned resources and compares with docs/stages/approved.json
python3 -I scripts/cost_check.py --root .
the settings page must match docs/settings.json
python3 -I scripts/gen_settings_doc.py --check
python3 -I scripts/gen_project_status.py --check
the repository tests (needs a virtualenv with pytest and pyyaml installed)
python3 -I -m pytest tests -q
no cloud account ids in tracked files
python3 -I scripts/identifier_check.py --root .
tests for the Spacelift push policy
opa test .spacelift/policies -v
tests for one module or stack (no cloud account needed, providers are mocked)
tofu -chdir=modules/firewall init -backend=false
tofu -chdir=modules/firewall test
Run the last pair for each directory in the directories list of the tofu job in .github/workflows/ci.yml: modules/vpc, modules/firewall, modules/twingate-bootstrap, modules/twingate-connector, modules/edge-host, modules/docker-host, modules/spaces-bucket, modules/aws/vpc, modules/aws/firewall, modules/aws/twingate-connector, stacks/00-foundation, stacks/10-access, stacks/20-telemetry, stacks/30-workloads, stacks/50-edge, stacks/aws/00-foundation and stacks/aws/10-access. CI runs all of it through git-your-ship-together's shared workflows; the only job this repository defines itself is the CI green gate.
Documentation
| For | Read |
|---|---|
| Learning how and why it works | The wiki (start at Home) |
| New to cloud, Terraform, zero trust or CI | Start here if you are new, then the eight start-from-zero primers it links |
| Building or operating a stage | Stage 0: foundation, Stage 1: access |
| Secrets, rotation, troubleshooting runs | Spacelift and Doppler secrets |
| The money | Cost |
| How the work is planned: the board, sprints, sizes, definition of done | How this project is planned, the project board and board.yaml |
| What could go wrong | Threat model, Lessons learned |
The wiki is generated from docs/wiki/, so corrections go in as pull requests and get the same review and secret scanning as the code. The Wiki workflow publishes them on every merge to main.
Public by design
This repository is public from its first commit, so the GitHub code-scanning and dependency-review checks run in CI. Everything that varies per person or per account (tokens, tenant names, user ids, address ranges you do not want shared) is a Doppler variable, not a file. If a piece ever cannot meet that rule, it moves to a separate private repository and this one calls it.
Secrets
Secrets live in Doppler; nothing in this repository is a credential. Spacelift holds one read-only Doppler token and fetches the rest at run time; see Spacelift and Doppler secrets for how that works and how to rotate it.
Required secrets
A required secret that is empty, unset or a placeholder stops the thing that needs it. One rule decides what a placeholder is, in scripts/secret_rule.py: empty, or starting with your_, your-, changeme, change-me, replace-with, xxx or dp.st.your, wrapped in <...>, containing example, placeholder or changeme, or exactly todo, tbd, admin, password, secret or test (case-insensitive). The error names the variable and never prints the value. No variable in the stacks or modules has a default that is a password or token.
| Secret | Needed by | Without it, or with a placeholder | Where it goes |
| --- | --- | --- | --- |
| GRAFANA_ADMIN_PASSWORD | the monitoring demo (scripts/demo_monitoring.sh) | A placeholder in the shell or .env stops the demo. Empty or missing: the script writes a random one to the untracked .env (other lines kept). Direct docker compose up refuses empty but does not apply the placeholder rule. | shell environment, or the untracked labs/demo-monitoring/.env |
| ORIGIN_SECRET | the WordPress-at-scale lab (lab.sh up, lab.ps1 up) | A placeholder or an empty shell value stops the lab. Empty or missing in .env: a random one is written. | shell environment, or the untracked labs/wordpress-at-scale/.env |
| DIGITALOCEAN_TOKEN, TWINGATE_API_TOKEN, TWINGATE_NETWORK | stacks 00, 10 and 20 (the provider settings) | The Spacelift hook refuses an empty value, and refuses a placeholder for the two tokens. Outside Spacelift the provider itself fails on an empty token; the placeholder rule is not applied there. | Doppler, fetched by the hook (how) |
| DOPPLER_TOKEN | the Spacelift hook | Empty or a placeholder: the hook refuses and fetches nothing. | the cah-doppler Spacelift context |
| log_shipping_bootstrap_token (TF_VAR_log_shipping_bootstrap_token) | stack 10 when log_shipping.enabled | Empty with log shipping on: the plan fails. A placeholder-shaped value: the plan fails. Log shipping off: never rendered. | a sensitive Spacelift stack variable, never a value file |
| WAZUH_ENROLLMENT_PASSWORD | each connector with log shipping on, pulled at first boot | Empty or missing: enrollment fails and the agent is never started. A placeholder is not checked on the droplet (the manager decides whether it matches). | the Doppler cah- config |
The loopback-only lab in labs/cross-tenant-dlp uses fixed labels (lab-a, lab-b) as the keys of two fake S3 servers on 127.0.0.1. They protect nothing and are not secrets.
labs/demo-monitoring/env.example ships GRAFANA_ADMIN_PASSWORD= empty on purpose. Never put a real value in an example file.
💝 Support This Project
If you find Clouds Against Humanity useful, consider supporting continued development. Everything is also collected at support.chiefgyk3d.com.
Recurring Support
Cryptocurrency Tips
bc1qztdzcy2wyavj2tsuandu4p0tcklzttvdnzalla |
84Y34QubRwQYK2HNviezeH9r6aRcPvgWmKtDkN3EwiuVbp6sNLhm9ffRgs6BA9X1n9jY7wEN16ZEpiEngZbecXseUrW8SeQ |
0x554f18cfB684889c3A60219BDBE7b050C39335ED |
5T8h3HbyvHgLxwXgchRYbHSqRjZyAr8J7uwjLN9Fh8Jh |