Skip to content

Settings Reference

github-actions[bot] edited this page Oct 18, 2026 · 15 revisions

Settings reference: every tunable setting

New to the tools? Read What is infrastructure as code first. This page is a lookup table, not a lesson.

This page lists every setting you can change in this repository, where each one lives, and what values it accepts. It is generated from one machine-readable file, docs/settings.json, and tests check that file against the code, so a variable cannot be added without being listed here.

Four kinds of place a value can live

Place What it holds In git? Example here
A variable (variables.tf) The name, type, description, validation and default of a setting. This is the definition, not the value for your site. Yes region, vpc_ip_range
A value file (terraform.tfvars) The values for your site. Ignored by git (.gitignore covers *.tfvars, *.tfvars.json, *.auto.tfvars.json and *.tfplan), so a real value never lands in a public repository by accident. A committed terraform.tfvars.example shows the shape with documentation-range values only. No (the .example file, yes) a private copy of the repository keeps its own
Environment variables (TF_VAR_*) The same values delivered to a run, from the Spacelift stack environment (non-secret) or from Doppler (private values and secrets). No TF_VAR_admin_user_ids
cost.json and approved.json Droplet sizes and the approved spend. The cost gate reads them, so the size lives there and nowhere else. Yes stacks/10-access/cost.json

Two rules follow. A secret is never a variable default and never in a value file: a value file is a plain file with no access control, and it is copied to laptops, chats and shell history. Secrets live in Doppler and arrive as an environment variable for one run. And a setting that is hard-coded today is listed too, so you can see it is not yet a knob.

Related pages: OpenTofu, stacks and modules (what a stack and a module are), Doppler and secrets (how a secret reaches a run), Fork, private stacks and the pipeline (keeping your real values in a private copy), Terraform reference (each declaration explained).

How to read the tables. Status is exists (in the code today) or planned (in the stage 2 design, not built yet). Sensitivity is public (safe for anyone), private (not a credential, but it identifies the owner's network or accounts) secret (a credential) or bootstrap-credential (a short-lived bearer token that has to be rendered into user_data so a machine can pull its own secrets once at first boot; it is a credential, not a plain private value, and only the log shipping bootstrap token has this class). Examples use documentation ranges (192.0.2.0/24, 198.51.100.0/24, 203.0.113.0/24), private ranges, and example.com only; none is a real value.

OpenTofu variables

Stack variables are the settings you set per site. Module variables are filled in by the calling stack; they are listed so the table is complete, and you normally leave them alone.

stacks/00-foundation

Setting Type Default Validation Set where Example Sensitivity Status
region
DigitalOcean region slug. Names are built from resource_key, which defaults to this. Check the sizes your account may use in the region (GET /v2/sizes) before changing it.
string "tor1" ^[a-z]{3}[0-9]$ (three lowercase letters and a digit); the pattern cannot know which sizes the account may use tfvars (local plans) or Spacelift stack environment (TF_VAR_*) tor1 public exists
vpc_ip_range
Private CIDR for the VPC. Must be the network address and must not overlap any range in reserved_ranges.
string "10.78.0.0/24" private IPv4 CIDR (10/8, 172.16/12, 192.168/16, also checked in modules/vpc); the network address (10.78.0.0/24, not 10.78.0.5/24); prefix length between /16 and /28; no overlap with reserved_ranges (a precondition on the project, because OpenTofu 1.8 cannot compare two variables in a validation) tfvars (local plans) or Spacelift stack environment (TF_VAR_*) 10.99.0.0/24 private (not a credential, but identifies the owner) exists
project_name
Name of the DigitalOcean project that groups the resources (the code has no description for it).
string "clouds-against-humanity" none today (DigitalOcean's allowed character set is UNVERIFIED) tfvars (local plans) or Spacelift stack environment (TF_VAR_*) clouds-against-humanity public exists
reserved_ranges
Private CIDRs the VPC range must not overlap (your homelab, other VPCs). Optional; they identify your network, so they are never committed. Prompts the human check, does not replace it.
list(string) [] each entry an IPv4 CIDR; vpc_ip_range must not overlap any entry (compared by first and last address) Spacelift stack environment (TF_VAR_reserved_ranges, a JSON list) or a private tfvars today; Doppler only once a hook line in .spacelift/config.yml requests it (the foundation hooks request only DIGITALOCEAN_TOKEN) ["10.50.0.0/16", "10.60.0.0/16"] private (not a credential, but identifies the owner) exists
resource_key
Key used in the names and tags of cloud and Twingate objects instead of region; empty means region, so every name is identical to a stack that never set it. IMMUTABLE for the life of a stack: changing it changes names, causes widespread plan changes and may replace resources, including through tags or user_data. Never derived from site_name.
string "" empty, or ^[a-z][a-z0-9-]{1,23}$; immutable on a live stack tfvars (local plans) or Spacelift stack environment (TF_VAR_*) tor1 public exists
site_name
Telemetry label for the site (metrics and log labels in a later stage); empty means site-<region>. In no resource name, tag or output; changing it replaces nothing.
string "" empty, or ^[a-z][a-z0-9-]{1,23}$; unique across sites (a rule for the person, one stack cannot test it) tfvars (local plans) or Spacelift stack environment (TF_VAR_*) site-tor1 public exists
provider_name
Label for the cloud provider; does not select modules.
string "digitalocean" one of the implemented providers tfvars (local plans) or Spacelift stack environment (TF_VAR_*) digitalocean public planned

stacks/10-access

Setting Type Default Validation Set where Example Sensitivity Status
host_firewall
A default-drop host firewall (nftables table inet cah_host, input chain only) on each connector, a second layer behind the cloud firewall. Only loopback, established traffic, the ICMP and DHCP the network card needs, TCP 22 from host_firewall_ssh_sources and, when connector_telemetry.metrics_port is set, that port from the same sources are let in. Default false is the live state: merging code with this unset plans no change. true adds a script, a systemd unit and the nftables package through user_data, which REPLACES the droplet of every connector in host_firewall_instances (both by default). The script checks the ruleset, applies it, verifies sshd still listens and removes the table if not. Roll out one connector at a time: true with host_firewall_instances ["a"], apply, check SSH through Twingate, then ["a", "b"]. Not yet tested on a real machine.
bool false none; non-nullable (null means false) tfvars (local plans) or Spacelift stack environment (TF_VAR_*) true public exists
host_firewall_instances
Which connectors get host_firewall: a, b or both. Only matters once host_firewall is true. Set ["a"] first, apply, check SSH through Twingate to the new connector, then ["a", "b"]. A connector not listed runs without the host firewall, as it does today.
list(string) ["a", "b"] at least one entry; each entry is a or b, no repeats tfvars (local plans) or Spacelift stack environment (TF_VAR_*) ["a"] public exists
host_firewall_ssh_sources
IPv4 CIDRs allowed to reach TCP 22 (and the connector metrics port) through the host firewall. Default the three private RFC 1918 blocks: the cloud firewall already limits SSH to the connector tag and this only narrows it to private space, because the module does not know the VPC CIDR. Narrow it to your VPC range if you know it. A /0 is refused on purpose: it would defeat the narrowing, and the cloud firewall stays the outer layer. Only matters once host_firewall is true.
list(string) ["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"] at least one IPv4 CIDR, no repeats; no leading zeros in octets or prefix; prefix 1 to 32 (/0 is refused on purpose); host bits and overlaps allowed tfvars (local plans) or Spacelift stack environment (TF_VAR_*) ["10.0.0.0/8"] public exists
node_exporter
Opt-in node exporter (prometheus-node-exporter from the distro archive) on each connector, bound to its private IPv4 address only. Default {} (enabled false) is the live state: merging code with this unset plans no change. enabled = true adds a package, a script and a systemd drop-in through user_data, which REPLACES the droplet of every connector in node_exporter_instances (both by default). port defaults to 9100. Opens no firewall port by itself: set metrics_source_tag as well. Roll out one connector at a time, checking SSH through Twingate and the memory headroom of the 1 GB droplet. NOT applied and NOT tested on a real machine yet.
object({enabled = optional(bool, false), port = optional(number, 9100)}) {} port a whole number from 1024 to 65535; non-nullable (null means {}) tfvars (local plans) or Spacelift stack environment (TF_VAR_*) { enabled = true, port = 9100 } public exists
node_exporter_instances
Which connectors get node_exporter: a, b or both. Only matters once node_exporter.enabled is true. Set ["a"] first, apply, check SSH through Twingate to the new connector, then ["a", "b"]. A connector not listed runs without the exporter, as it does today.
list(string) ["a", "b"] at least one entry; each entry is a or b, no repeats tfvars (local plans) or Spacelift stack environment (TF_VAR_*) ["a"] public exists
metrics_source_tag
DigitalOcean tag of the droplets that may scrape the connectors' metrics ports (the node exporter port and connector_telemetry.metrics_port, tcp). Empty (default) is the live state: no metrics rule exists. When set, the connector firewall gains one tag-sourced inbound rule (source_tags = [this tag]) per enabled metrics port: never an address, never a public source. The tag must already exist on the account. Changes the firewall only; replaces no droplet.
string "" empty, or ^[A-Za-z0-9:_-]{1,255}$ (a tag name: an address or CIDR is refused) tfvars (local plans) or Spacelift stack environment (TF_VAR_*) cah-telemetry-tor1 public exists
log_shipping
Opt-in log shipping: the Wazuh agent (from Wazuh's apt repository, key checked against apt_key_fingerprint) and auditd on each connector, enrolled at first boot with a secret pulled from Doppler. Default {} is the live state: merging code with this unset plans no change. enabled = true changes user_data and REPLACES the droplet of every connector in log_shipping_instances (both by default). Roll out one connector at a time, checking SSH through Twingate and the memory headroom of the 1 GB droplet (UNMEASURED). Required when enabled: manager_address (the manager as reached through Twingate), agent_name_prefix (agents are named <prefix>-connector-<a or b>; use the same value as site_name, but it is a separate input because it lands in user_data), apt_key_fingerprint (40 uppercase hex digits, no default on purpose) and log_shipping_bootstrap_token. manager_port 1514 and enroll_port 1515 must match the logging_servers ports in stack 20. The Twingate headless client the agent needs to reach the manager is NOT part of this change. NOT applied and NOT tested on a real machine.
object({enabled = optional(bool, false), manager_address = optional(string, ""), manager_port = optional(number, 1514), enroll_port = optional(number, 1515), agent_name_prefix = optional(string, ""), agent_group = optional(string, ""), apt_key_fingerprint = optional(string, ""), agent_version = optional(string, "")}) {} ports whole numbers 1-65535 (checked even while disabled); agent_name_prefix empty or ^[a-z][a-z0-9-]{1,40}$ and required when enabled; manager_address required when enabled (letters, digits, dots, hyphens); apt_key_fingerprint required when enabled and ^[0-9A-F]{40}$; agent_group empty or letters, digits, dot, underscore, hyphen; agent_version empty or like 4.9.2-1; non-nullable (null means {}) tfvars (local plans) or Spacelift stack environment (TF_VAR_*); the manager address is private { enabled = true, manager_address = "siem.example.com", agent_name_prefix = "site-tor1", apt_key_fingerprint = "<40 uppercase hex characters>" } private (not a credential, but identifies the owner) exists
log_shipping_instances
Which connectors get log_shipping: a, b or both. Only matters once log_shipping.enabled is true. Set ["a"] first, apply, check SSH through Twingate and the memory headroom, then ["a", "b"]. A connector not listed runs without the agent, as it does today.
list(string) ["a", "b"] at least one entry; each entry is a or b, no repeats tfvars (local plans) or Spacelift stack environment (TF_VAR_*) ["a"] public exists
log_shipping_bootstrap_token
The one secret-shaped value log shipping puts in user_data: a short-lived, read-only Doppler service token for the config cah-<resource_key>-hostboot, created out of band (not by OpenTofu). The first-boot script deletes the token file before using it. Required when log_shipping.enabled; never rendered otherwise. Sensitive, but note that sensitive hides a value from output and does not keep it out of state.
string "" empty or 16-512 characters of letters, digits, dot, underscore, hyphen; required when log_shipping.enabled Spacelift stack environment (TF_VAR_log_shipping_bootstrap_token), write-only; never a value file "" bootstrap credential (a short-lived bearer token rendered into user_data for first boot) exists
region
DigitalOcean region slug, from the foundation stack's region output. Names are built from resource_key, which defaults to this.
string required ^[a-z]{3}[0-9]$ (three lowercase letters and a digit) Spacelift stack dependency, mapped to a TF_VAR_* environment variable (output of stack 00) tor1 public exists
vpc_id
ID of the VPC created by stack 00 (the code has no description for it).
string required none today (planned: non-empty) Spacelift stack dependency, mapped to a TF_VAR_* environment variable (output of stack 00) 00000000-0000-4000-8000-000000000000 public exists
project_id
ID of the DigitalOcean project created by stack 00 (the code has no description for it).
string required none today (planned: non-empty) Spacelift stack dependency, mapped to a TF_VAR_* environment variable (output of stack 00) 00000000-0000-4000-8000-000000000001 public exists
baseline_tag
Baseline firewall tag created by stack 00; the connectors carry it.
string required starts with "cah-baseline-" Spacelift stack dependency, mapped to a TF_VAR_* environment variable (output of stack 00) cah-baseline-tor1 public exists
twingate_network
Bare Twingate network subdomain (the tenant name).
string required ^[a-z0-9-]+$ (no URL, no quotes) Doppler (config prd), exported as TF_VAR_* by the Spacelift hook (TWINGATE_NETWORK) example-tenant private (not a credential, but identifies the owner) exists
admin_ssh_public_key
Public SSH key for the cah-admin user on the connectors: '<algorithm> <key>' with an optional trailing comment of one word. The comment is dropped before cloud-init, so user_data does not depend on it.
string required ^(ssh-ed25519|ssh-rsa|ecdsa-sha2-nistp(256|384|521)) [A-Za-z0-9+/]+=*( [A-Za-z0-9@._+:=/-]{1,64})?$ (one optional comment word of safe characters; no line break or other whitespace); the same check in modules/twingate-bootstrap Doppler (config prd), exported as TF_VAR_* by the Spacelift hook (ADMIN_SSH_PUBLIC_KEY) ssh-ed25519 AAAAEXAMPLEONLYNOTAREALKEY private (not a credential, but identifies the owner) exists
admin_ssh_key_name
Name of an SSH key already registered on the DigitalOcean account.
string required non-empty Doppler (config prd), exported as TF_VAR_* by the Spacelift hook (ADMIN_SSH_KEY_NAME) example-admin-key private (not a credential, but identifies the owner) exists
admin_user_ids
Twingate user ids placed in the admin group.
list(string) required at least one entry Doppler (config prd), exported as TF_VAR_* by the Spacelift hook (ADMIN_USER_IDS, a JSON list) ["example-user-id"] private (not a credential, but identifies the owner) exists
resource_key
Key used in the names and tags of cloud and Twingate objects instead of region; empty means region, so every name is identical to a stack that never set it. IMMUTABLE for the life of a stack: changing it changes names, causes widespread plan changes and may replace resources, including through tags or user_data. Never derived from site_name.
string "" empty, or ^[a-z][a-z0-9-]{1,23}$; immutable on a live stack tfvars (local plans) or Spacelift stack environment (TF_VAR_*) tor1 public exists
site_name
Telemetry label for the site (metrics and log labels in a later stage); empty means site-<region>. In no resource name, tag or output; changing it replaces nothing.
string "" empty, or ^[a-z][a-z0-9-]{1,23}$ tfvars (local plans) or Spacelift stack environment (TF_VAR_*) site-tor1 public exists
provider_name
Label for the cloud provider; does not select modules.
string "digitalocean" one of the implemented providers tfvars (local plans) or Spacelift stack environment (TF_VAR_*) digitalocean public planned
reboot_times
Reboot time per connector (HH:MM, America/New_York, the droplet's local time). Keys a and b. The defaults are the times the live connectors use.
map(string) {"a": "04:00", "b": "05:00"} exactly the keys a and b; each HH:MM in 24-hour time; the two reboot times at least 60 minutes apart, measured around the clock across midnight, which also keeps each connector's weekly update (reboot + 30 minutes) at least 30 minutes from the other's reboot tfvars (local plans) or Spacelift stack environment (TF_VAR_*) { a = "04:00", b = "05:00" } public exists
timezone
IANA time zone of the machines, also the zone reboot_time(s) and the weekly connector update are read in. Replaces a literal in the cloud-init template; the default renders byte-identical user_data.
string "America/New_York" ^(UTC|[A-Za-z_]+(/[A-Za-z0-9_+-]+){1,2})$ (an IANA-shaped name; line breaks, spaces and YAML-special characters are rejected) and the name must be listed in modules/twingate-bootstrap/timezones.txt (generated by scripts/gen_timezones.py; enforced in the bootstrap module, which every stack goes through). Changing it changes user_data, which is force-new and replaces the droplets tfvars (local plans) or Spacelift stack environment (TF_VAR_*) America/New_York public exists
connector_telemetry_previous
What the connectors NOT listed in connector_telemetry_instances keep: the telemetry they run today. Default {} is right for the first activation. For a later change (say log_level 5 to 7) set connector_telemetry to the new value, this to the old value and connector_telemetry_instances to ["a"]; apply, check SSH through Twingate, then set instances to ["a", "b"] and clear this. Without it connector b would lose its telemetry and be replaced in the same plan as a.
object({status_updates_enabled = optional(bool), log_level = optional(number), connection_logs = optional(bool, false), metrics_port = optional(number), labels = optional(map(string), {})}) {} none (the bootstrap module validates it, the same checks as connector_telemetry); non-nullable (null means {}) tfvars (local plans) or Spacelift stack environment (TF_VAR_*) { log_level = 5 } public exists
connector_telemetry_instances
Which connectors get connector_telemetry: a, b or both. Only matters once connector_telemetry is set. Set ["a"] first, apply, check SSH through Twingate to the new connector, then ["a", "b"]: that is the one-at-a-time rollout. A connector not listed gets the default (nothing).
list(string) ["a", "b"] at least one entry; each entry is a or b, no repeats tfvars (local plans) or Spacelift stack environment (TF_VAR_*) ["a"] public exists
connector_telemetry
Twingate connector telemetry for both connectors. Default {} is the live state and changes nothing. status_updates_enabled is a provider setting that is not sent while unset, and replaces nothing. log_level (TWINGATE_LOG_LEVEL), connection_logs (true writes TWINGATE_LOG_ANALYTICS=v3), metrics_port (TWINGATE_METRICS_PORT) and labels (TWINGATE_LABEL_<KEY>) add lines to /etc/twingate/connector.conf: that changes user_data and the plan shows the droplets of every connector in connector_telemetry_instances REPLACED (both by default). Roll out one connector at a time with connector_telemetry_instances, with an SSH-through-Twingate check between (Trace-One-Change-End-to-End, variant C). Opens no firewall port by itself: set metrics_source_tag to admit the metrics port from that one tag. Labels are shown in the Twingate Admin Console: never put a secret in one.
object({status_updates_enabled = optional(bool), log_level = optional(number), connection_logs = optional(bool, false), metrics_port = optional(number), labels = optional(map(string), {})}) {} log_level in {3, 4, 5, 7} or unset; metrics_port a whole number 1024 to 65535 and not 9100, or unset; labels: at most 10, keys ^[A-Z][A-Z0-9_]{0,31}$, values ^[A-Za-z0-9._-]{1,64}$ (no spaces, quotes or line breaks, because they are written into the connector config); connector_telemetry is non-nullable (null means {}) tfvars or Spacelift stack environment (TF_VAR_connector_telemetry as JSON) { log_level = 5, connection_logs = true, metrics_port = 9999, labels = { DEPLOYED_BY = "opentofu" } } public exists
metadata_lock
Lock the cloud metadata endpoint (169.254.169.254, which serves each connector's user-data and so its Twingate tokens) to root and to the do-agent user (DigitalOcean's monitoring agent reads it; allowed only when that user exists). Default false is the live state: merging code with this unset plans no change. true adds an nftables table, a script and a systemd unit through user_data, which REPLACES the droplet of every connector in metadata_lock_instances (both by default). Roll out one connector at a time: true with metadata_lock_instances ["a"], apply, check SSH through Twingate, then ["a", "b"]. Not yet tested on a real machine.
bool false none; non-nullable (null means false) tfvars (local plans) or Spacelift stack environment (TF_VAR_*) true public exists
metadata_lock_instances
Which connectors get metadata_lock: a, b or both. Only matters once metadata_lock is true. Set ["a"] first, apply, check SSH through Twingate to the new connector, then ["a", "b"]. A connector not listed runs without the lock, as it does today.
list(string) ["a", "b"] at least one entry; each entry is a or b, no repeats tfvars (local plans) or Spacelift stack environment (TF_VAR_*) ["a"] public exists
metrics_enabled
Install node exporter and expose connector metrics.
bool false none tfvars (local plans) or Spacelift stack environment (TF_VAR_*) true public planned
node_exporter_port
Node exporter port.
number 9100 1024 to 65535 tfvars (local plans) or Spacelift stack environment (TF_VAR_*) 9100 public planned
metrics_port_connector
Connector metrics port.
number required 1024 to 65535; differs from node_exporter_port tfvars (local plans) or Spacelift stack environment (TF_VAR_*) 9101 public planned
wazuh_agent_group
Wazuh agent group for the cloud hosts.
string "cloud" ^[a-zA-Z0-9_.-]+$ tfvars (local plans) or Spacelift stack environment (TF_VAR_*) cloud-site-tor1 public planned
labels
Telemetry labels.
map(string) {} keys limited to env, role, owner_team; values at most 32 characters tfvars (local plans) or Spacelift stack environment (TF_VAR_*) {env = "lab"} public planned

stacks/aws/00-foundation

Setting Type Default Validation Set where Example Sensitivity Status
region
AWS region name. The price table was recorded for us-east-1; another region needs its own check.
string "us-east-1" ^[a-z]{2}(-[a-z]+)+-[0-9]$ (for example us-east-1) tfvars (local plans) or Spacelift stack environment (TF_VAR_*) us-east-1 public exists
vpc_ip_range
Private CIDR. Must not overlap any homelab range (confirmed in the stage guide before apply).
string "10.78.0.0/24" none in the stack; modules/aws/vpc checks it is a private IPv4 network address, /16 to /28 tfvars (local plans) or Spacelift stack environment (TF_VAR_*) 10.99.0.0/24 private (not a credential, but identifies the owner) exists
availability_zone
Availability zone of the one subnet. Null lets AWS choose.
string null none tfvars (local plans) or Spacelift stack environment (TF_VAR_*) us-east-1a public exists
project_name
Value of the Project tag that every AWS resource of the stack receives.
string "clouds-against-humanity" none tfvars (local plans) or Spacelift stack environment (TF_VAR_*) clouds-against-humanity public exists

stacks/aws/10-access

Setting Type Default Validation Set where Example Sensitivity Status
host_firewall
A default-drop host firewall (nftables table inet cah_host, input chain only) on each connector, a second layer behind the cloud firewall. Only loopback, established traffic, the ICMP and DHCP the network card needs, TCP 22 from host_firewall_ssh_sources and, when connector_telemetry.metrics_port is set, that port from the same sources are let in. Default false is the live state: merging code with this unset plans no change. true adds a script, a systemd unit and the nftables package through user_data, which REPLACES the instance of every connector in host_firewall_instances (both by default). The script checks the ruleset, applies it, verifies sshd still listens and removes the table if not. Roll out one connector at a time: true with host_firewall_instances ["a"], apply, check SSH through Twingate, then ["a", "b"]. Not yet tested on a real machine.
bool false none; non-nullable (null means false) tfvars (local plans) or Spacelift stack environment (TF_VAR_*) true public exists
host_firewall_instances
Which connectors get host_firewall: a, b or both. Only matters once host_firewall is true. Set ["a"] first, apply, check SSH through Twingate to the new connector, then ["a", "b"]. A connector not listed runs without the host firewall, as it does today.
list(string) ["a", "b"] at least one entry; each entry is a or b, no repeats tfvars (local plans) or Spacelift stack environment (TF_VAR_*) ["a"] public exists
host_firewall_ssh_sources
IPv4 CIDRs allowed to reach TCP 22 (and the connector metrics port) through the host firewall. Default the three private RFC 1918 blocks: the cloud firewall already limits SSH to the connector tag and this only narrows it to private space, because the module does not know the VPC CIDR. Narrow it to your VPC range if you know it. A /0 is refused on purpose: it would defeat the narrowing, and the cloud firewall stays the outer layer. Only matters once host_firewall is true.
list(string) ["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"] at least one IPv4 CIDR, no repeats; no leading zeros in octets or prefix; prefix 1 to 32 (/0 is refused on purpose); host bits and overlaps allowed tfvars (local plans) or Spacelift stack environment (TF_VAR_*) ["10.0.0.0/8"] public exists
region
AWS region name, the same value as stack 00.
string required ^[a-z]{2}(-[a-z]+)+-[0-9]$ (for example us-east-1) Spacelift stack dependency, mapped to a TF_VAR_* environment variable (output of stack 00) us-east-1 public exists
vpc_id
ID of the VPC created by stack 00.
string required ^vpc-[0-9a-f]+$ Spacelift stack dependency, mapped to a TF_VAR_* environment variable (output of stack 00) vpc-00000000000000000 public exists
subnet_id
ID of the public subnet created by stack 00.
string required ^subnet-[0-9a-f]+$ Spacelift stack dependency, mapped to a TF_VAR_* environment variable (output of stack 00) subnet-00000000000000000 public exists
project_name
Value of the Project tag that every AWS resource of the stack receives. Same as stack 00.
string "clouds-against-humanity" none tfvars (local plans) or Spacelift stack environment (TF_VAR_*) clouds-against-humanity public exists
twingate_network
Bare Twingate network subdomain (the tenant name).
string required ^[a-z0-9-]+$ (no URL, no quotes) Doppler (config prd), exported as TF_VAR_* by the Spacelift hook (TWINGATE_NETWORK) example-tenant private (not a credential, but identifies the owner) exists
admin_ssh_public_key
Public SSH key for the cah-admin user on the connectors (modules/twingate-bootstrap requires type and key with no comment). No AWS key pair is created.
string required none in the stack (the bootstrap module requires '<ssh-ed25519|ssh-rsa> <key>' with no comment) Doppler (config prd), exported as TF_VAR_* by the Spacelift hook (ADMIN_SSH_PUBLIC_KEY) ssh-ed25519 AAAAEXAMPLEONLYNOTAREALKEY private (not a credential, but identifies the owner) exists
admin_user_ids
Twingate user ids placed in the admin group.
list(string) required at least one entry Doppler (config prd), exported as TF_VAR_* by the Spacelift hook (ADMIN_USER_IDS, a JSON list) ["example-user-id"] private (not a credential, but identifies the owner) exists
timezone
IANA time zone of the machines, also the zone reboot_time(s) and the weekly connector update are read in. Replaces a literal in the cloud-init template; the default renders byte-identical user_data.
string "America/New_York" ^(UTC|[A-Za-z_]+(/[A-Za-z0-9_+-]+){1,2})$ (an IANA-shaped name; line breaks, spaces and YAML-special characters are rejected) and the name must be listed in modules/twingate-bootstrap/timezones.txt (generated by scripts/gen_timezones.py; enforced in the bootstrap module, which every stack goes through). Changing it changes user_data, which is force-new and replaces the droplets tfvars (local plans) or Spacelift stack environment (TF_VAR_*) America/New_York public exists
connector_telemetry_previous
What the connectors NOT listed in connector_telemetry_instances keep: the telemetry they run today. Default {} is right for the first activation. For a later change (say log_level 5 to 7) set connector_telemetry to the new value, this to the old value and connector_telemetry_instances to ["a"]; apply, check SSH through Twingate, then set instances to ["a", "b"] and clear this. Without it connector b would lose its telemetry and be replaced in the same plan as a.
object({status_updates_enabled = optional(bool), log_level = optional(number), connection_logs = optional(bool, false), metrics_port = optional(number), labels = optional(map(string), {})}) {} none (the bootstrap module validates it, the same checks as connector_telemetry); non-nullable (null means {}) tfvars (local plans) or Spacelift stack environment (TF_VAR_*) { log_level = 5 } public exists
connector_telemetry_instances
Which connectors get connector_telemetry: a, b or both. Only matters once connector_telemetry is set. Set ["a"] first, apply, check SSH through Twingate to the new connector, then ["a", "b"]: that is the one-at-a-time rollout. A connector not listed gets the default (nothing).
list(string) ["a", "b"] at least one entry; each entry is a or b, no repeats tfvars (local plans) or Spacelift stack environment (TF_VAR_*) ["a"] public exists
connector_telemetry
Twingate connector telemetry for both connectors. Default {} is the live state and changes nothing. status_updates_enabled is a provider setting that is not sent while unset, and replaces nothing. log_level (TWINGATE_LOG_LEVEL), connection_logs (true writes TWINGATE_LOG_ANALYTICS=v3), metrics_port (TWINGATE_METRICS_PORT) and labels (TWINGATE_LABEL_<KEY>) add lines to /etc/twingate/connector.conf: that changes user_data and the plan shows the instances of every connector in connector_telemetry_instances REPLACED (both by default). Roll out one connector at a time with connector_telemetry_instances, with an SSH-through-Twingate check between (Trace-One-Change-End-to-End, variant C). Opens no security group port by itself: set metrics_source_security_group_id to admit the metrics port from that one security group. Labels are shown in the Twingate Admin Console: never put a secret in one.
object({status_updates_enabled = optional(bool), log_level = optional(number), connection_logs = optional(bool, false), metrics_port = optional(number), labels = optional(map(string), {})}) {} log_level in {3, 4, 5, 7} or unset; metrics_port a whole number 1024 to 65535 and not 9100, or unset; labels: at most 10, keys ^[A-Z][A-Z0-9_]{0,31}$, values ^[A-Za-z0-9._-]{1,64}$ (no spaces, quotes or line breaks, because they are written into the connector config); connector_telemetry is non-nullable (null means {}) tfvars or Spacelift stack environment (TF_VAR_connector_telemetry as JSON) { log_level = 5, connection_logs = true, metrics_port = 9999, labels = { DEPLOYED_BY = "opentofu" } } public exists
metadata_lock
Lock the cloud metadata endpoint (169.254.169.254, which serves each connector's user-data and so its Twingate tokens) to root and to the do-agent user (DigitalOcean's monitoring agent reads it; allowed only when that user exists). Default false is the live state: merging code with this unset plans no change. true adds an nftables table, a script and a systemd unit through user_data, which REPLACES the instance of every connector in metadata_lock_instances (both by default). Roll out one connector at a time: true with metadata_lock_instances ["a"], apply, check SSH through Twingate, then ["a", "b"]. Not yet tested on a real machine.
bool false none; non-nullable (null means false) tfvars (local plans) or Spacelift stack environment (TF_VAR_*) true public exists
metadata_lock_instances
Which connectors get metadata_lock: a, b or both. Only matters once metadata_lock is true. Set ["a"] first, apply, check SSH through Twingate to the new connector, then ["a", "b"]. A connector not listed runs without the lock, as it does today.
list(string) ["a", "b"] at least one entry; each entry is a or b, no repeats tfvars (local plans) or Spacelift stack environment (TF_VAR_*) ["a"] public exists
node_exporter
Opt-in node exporter (prometheus-node-exporter from the distro archive) on each connector, bound to its private IPv4 address only. Default {} (enabled false) is the live state: merging code with this unset plans no change (user_data is byte-identical). enabled = true adds a package, a script and a systemd drop-in through user_data, which REPLACES the instance of every connector in node_exporter_instances (both by default; user_data_replace_on_change), and with it the instance's public address. port defaults to 9100. Opens no security group port by itself: set metrics_source_security_group_id as well. Roll out one connector at a time, checking SSH through Twingate. NOT applied and NOT tested on a real machine or account yet; Spacelift's AWS integration is still unchecked and no Spacelift stack exists for this code.
object({enabled = optional(bool, false), port = optional(number, 9100)}) {} port a whole number from 1024 to 65535; non-nullable (null means {}) tfvars (local plans) or Spacelift stack environment (TF_VAR_*) { enabled = true, port = 9100 } public exists
node_exporter_instances
Which connectors get node_exporter: a, b or both. Only matters once node_exporter.enabled is true. Set ["a"] first, apply, check SSH through Twingate to the new connector, then ["a", "b"]. A connector not listed runs without the exporter, as it does today.
list(string) ["a", "b"] at least one entry; each entry is a or b, no repeats tfvars (local plans) or Spacelift stack environment (TF_VAR_*) ["a"] public exists
metrics_source_security_group_id
Security group id of the machines that may scrape the connectors' metrics ports (the node exporter port and connector_telemetry.metrics_port, tcp), supplied by the operator. Empty (default) is the live state: no metrics rule exists. When set, the connector security group gains one inbound rule per enabled metrics port whose source is that group (referenced_security_group_id): never an address, an IPv6 range or a prefix list. The group must already exist in the same VPC. Changes the security group only; replaces no instance. Must differ from the connector security group itself (refused otherwise: it would admit connector-to-connector metrics). To change the source group, apply once with this empty, then set the new group: a different group is a different rule key.
string "" empty, or ^sg-[0-9a-f]{8,17}$ (a security group id: an address or CIDR is refused); must not equal the connector security group (a precondition on the rule and on the stack output) tfvars (local plans) or Spacelift stack environment (TF_VAR_*) sg-0123456789abcdef0 (illustrative) public exists

stacks/20-telemetry

Setting Type Default Validation Set where Example Sensitivity Status
region
DigitalOcean region slug of the site this stack serves, from the foundation stack's region output. Names are built from resource_key, which defaults to this.
string required ^[a-z]{3}[0-9]$ (three lowercase letters and a digit) Spacelift stack environment (foundation output mapped to TF_VAR_region) or tfvars tor1 public exists
resource_key
Key used in the names of the Twingate objects this stack creates (telemetry-<key>-logging); empty means region. IMMUTABLE for the life of a stack: changing it renames, and so replaces, every object. Never derived from site_name.
string "" empty, or ^[a-z][a-z0-9-]{1,23}$; immutable on a live stack tfvars (local plans) or Spacelift stack environment (TF_VAR_*) tor1 public exists
site_name
Telemetry label only (the site field of scrape_targets and site_identity); empty means site-<region>. In no resource name or tag; changing it replaces nothing.
string "" empty, or ^[a-z][a-z0-9-]{1,23}$; unique across sites (a rule for the person, one stack cannot test it) tfvars (local plans) or Spacelift stack environment (TF_VAR_*) site-tor1 public exists
logging_enabled
Create the logging Twingate service and one resource per logging_servers element. False creates nothing. Verify the Twingate plan allows the services first (UNVERIFIED).
bool false none tfvars (local plans) or Spacelift stack environment (TF_VAR_*) true public exists
metrics_enabled
Create the scrape Twingate service and one resource per scrape_allowlist entry per connector. False creates nothing.
bool false none tfvars (local plans) or Spacelift stack environment (TF_VAR_*) true public exists
logging_servers
Home logging endpoints reached only through Twingate; one Twingate resource per element, named from name and resource_key, never from the address.
list(object({name=string,address=string,port=number,protocol=string,purpose=string})) [] name unique and ^[a-z][a-z0-9-]{0,40}$ (so never an address); address an RFC 1918 or RFC 5737 IPv4 literal or a host name with no scheme (a public IPv4 literal is refused); port 1-65535; protocol tcp or udp; purpose one of wazuh-agent, wazuh-enroll, syslog, metrics-remote-write Per-site Doppler config as JSON (private, non-secret), exported as TF_VAR_logging_servers [{name="siem-home-agent", address="192.0.2.10", port=1514, protocol="tcp", purpose="wazuh-agent"}] private (not a credential, but identifies the owner) exists
home_remote_network_id
Id of the Twingate remote network that holds the home logging connector, where the logging resources are created. Required when logging_enabled (a precondition).
string "" none (a precondition on the logging service checks it when logging_enabled) Per-site Doppler config or Spacelift stack environment (TF_VAR_home_remote_network_id) example-home-remote-network-id private (not a credential, but identifies the owner) exists
cloud_remote_network_id
Id of this site's cloud Twingate remote network (access-<resource_key>, made by stack 10), where the scrape resources are created. Required when metrics_enabled (a precondition). Stack 10 does not output it today.
string "" none (a precondition on the scrape service checks it when metrics_enabled) Spacelift stack environment (TF_VAR_cloud_remote_network_id) example-cloud-remote-network-id private (not a credential, but identifies the owner) exists
node_exporter_port
TCP port the node exporter listens on, on each connector. Keep it equal to node_exporter.port in the access stack (stack 10), which installs the exporter (opt-in) and opens the port from metrics_source_tag; the stacks are separate, so the two values are kept in step by hand.
number 9100 whole number from 1024 to 65535 tfvars (local plans) or Spacelift stack environment (TF_VAR_*) 9100 public exists
metrics_port_connector
TCP port of the Twingate connector's own metrics endpoint. Keep it equal to connector_telemetry.metrics_port in the access stack (stack 10). No default: the connector's metrics bind address is UNVERIFIED. Required when metrics_enabled (a precondition).
number null unset, or a whole number from 1024 to 65535; when metrics_enabled it must be set, differ from node_exporter_port and not be 9100 (kept for the node exporter, as in the access stack) tfvars (local plans) or Spacelift stack environment (TF_VAR_*) 9101 (illustrative) public exists
scrape_allowlist
Which connector endpoints become Twingate resources for scraping, per connector. Required non-empty when metrics_enabled.
list(object({name=string,host_role=string,port=number})) [] name unique and ^[a-z][a-z0-9-]{0,30}$; host_role connector; port 1024-65535; when metrics_enabled each port must be node_exporter_port or metrics_port_connector tfvars (local plans) or Spacelift stack environment (TF_VAR_*) [{name="node", host_role="connector", port=9100}] public exists
connector_private_ips
Private addresses of the site's connectors, keyed a and b: the access stack's connector_private_ips output. Required when metrics_enabled. Used for the scrape resources and the sensitive scrape_targets output only.
map(string) {} keys only a and b; values private IPv4 addresses (10/8, 172.16/12, 192.168/16) Spacelift stack environment (TF_VAR_connector_private_ips, a JSON map) or by hand; a map through a stack dependency is UNVERIFIED {a="10.99.0.5", b="10.99.0.6"} private (not a credential, but identifies the owner) exists
wazuh_agent_group
Wazuh agent group for this site's cloud hosts (telemetry label; read by the host bootstrap in a later step).
string "cloud" ^[a-zA-Z0-9_.-]+$ tfvars (local plans) or Spacelift stack environment (TF_VAR_*) cloud-site-tor1 public exists
labels
Extra telemetry labels, kept few to bound metric cardinality.
map(string) {} keys limited to env, role, owner_team; values at most 32 characters tfvars (local plans) or Spacelift stack environment (TF_VAR_*) {env = "lab"} public exists

planned/optional-noc-soc (not built yet)

Setting Type Default Validation Set where Example Sensitivity Status
grafana_enabled
Create the optional Grafana host.
bool false none tfvars (local plans) or Spacelift stack environment (TF_VAR_*) false public planned
grafana_droplet_size
Droplet size for the Grafana host; belongs in cost.json like the connectors.
string "s-1vcpu-1gb" present in docs/cost-prices.json cost.json s-1vcpu-1gb public planned
metrics_retention_days
Days of metrics kept by the cloud local store.
number 15 1 to 400 tfvars (local plans) or Spacelift stack environment (TF_VAR_*) 15 public planned
log_retention_days
Days of logs kept by the cloud local store.
number 30 1 to 400 tfvars (local plans) or Spacelift stack environment (TF_VAR_*) 30 public planned
soc_admin_user_ids
Twingate user ids for the SOC tools.
list(string) required at least one when grafana_enabled Doppler (config prd), exported as TF_VAR_* by the Spacelift hook ["example-user-id"] private (not a credential, but identifies the owner) planned

modules/vpc

Setting Type Default Validation Set where Example Sensitivity Status
layer
Stack layer, used in names (for example foundation).
string required none set by the calling stack (not a user setting) foundation public exists
region
DigitalOcean region slug.
string required none set by the calling stack (not a user setting) tor1 public exists
ip_range
Private IPv4 CIDR for the VPC.
string required valid CIDR in 10/8, 172.16/12 or 192.168/16 set by the calling stack (not a user setting) 10.99.0.0/24 private (not a credential, but identifies the owner) exists
resource_key
Key used in resource names in place of region; empty means region. Set by the calling stack from its resource_key.
string "" empty, or ^[a-z][a-z0-9-]{1,23}$ set by the calling stack (not a user setting) tor1 public exists

modules/firewall

Setting Type Default Validation Set where Example Sensitivity Status
layer
Stack layer, used in names (the module has no description for it).
string required none set by the calling stack (not a user setting) access public exists
region
Region, used in names (the module has no description for it).
string required none set by the calling stack (not a user setting) tor1 public exists
purpose
Role in the firewall name (for example baseline or connector).
string required none set by the calling stack (not a user setting) connector public exists
tag
Droplet tag this firewall attaches to.
string required none set by the calling stack (not a user setting) cah-connector-tor1 public exists
extra_outbound_rules
Outbound rules appended to the defaults.
list(object({protocol=string,port_range=string})) [] none set by the calling stack (not a user setting) [{protocol="tcp", port_range="30000-31000"}] public exists
inbound_from_tag_rules
Inbound rules that admit only droplets carrying this firewall's own tag. Empty means deny-all inbound.
list(object({protocol=string,port_range=string})) [] protocol tcp or udp and a non-empty port_range set by the calling stack (not a user setting) [{protocol="tcp", port_range="22"}] public exists
outbound_to_tag_rules
Outbound rules that reach only droplets carrying this firewall's own tag (no addresses). Empty means none.
list(object({protocol=string,port_range=string})) [] protocol tcp or udp and a non-empty port_range set by the calling stack (not a user setting) [{protocol="tcp", port_range="22"}] public exists
inbound_from_other_tags
Inbound rules that admit only droplets carrying the given other tag (source_tags, never an address). Additive. Empty means none.
list(object({protocol=string,port_range=string,tag=string})) [] protocol tcp or udp; port_range a port or ascending range within 1..65535; tag 1 to 255 characters of letters, digits, colon, hyphen, underscore; no duplicate (protocol, port_range, tag) set by the calling stack (not a user setting) [{protocol="tcp", port_range="9100", tag="cah-connector-tor1"}] public exists
outbound_to_other_tags
Outbound rules that reach only droplets carrying the given other tag (destination_tags, never an address). Additive. Empty means none.
list(object({protocol=string,port_range=string,tag=string})) [] protocol tcp or udp; port_range a port or ascending range within 1..65535; tag 1 to 255 characters of letters, digits, colon, hyphen, underscore; no duplicate (protocol, port_range, tag) set by the calling stack (not a user setting) [{protocol="tcp", port_range="9100", tag="cah-connector-tor1"}] public exists
resource_key
Key used in resource names in place of region; empty means region. Set by the calling stack from its resource_key.
string "" empty, or ^[a-z][a-z0-9-]{1,23}$ set by the calling stack (not a user setting) tor1 public exists

modules/twingate-bootstrap

Setting Type Default Validation Set where Example Sensitivity Status
host_firewall
A default-drop host firewall (nftables, input only). Default false changes nothing: user_data is byte-identical (golden hashes in the tests). true adds a script, a systemd unit and the nftables package to user_data, which replaces the machine.
bool false none; non-nullable (null means false) the calling module true public exists
host_firewall_ssh_sources
IPv4 CIDRs allowed to reach TCP 22 (and the metrics port) when host_firewall is true. Default the three private RFC 1918 blocks.
list(string) ["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"] at least one IPv4 CIDR, no repeats; no leading zeros in octets or prefix; prefix 1 to 32 (/0 is refused on purpose); host bits and overlaps allowed the calling module ["10.0.0.0/8"] public exists
layer
Stack layer, used in names (the module has no description for it).
string required none set by the calling stack (not a user setting) access public exists
region
Region, used in names (the module has no description for it).
string required none set by the calling stack (not a user setting) tor1 public exists
remote_network_id
Twingate remote network (site) shared by every connector of the stack.
string required none set by the calling stack (not a user setting) example-remote-network-id public exists
instance
One lowercase letter or digit that tells this connector from its siblings (a, b).
string required ^[a-z0-9]$ set by the calling stack (not a user setting) a public exists
twingate_network
Bare Twingate network subdomain.
string required ^[a-z0-9-]+$ set by the calling stack (not a user setting) example-tenant private (not a credential, but identifies the owner) exists
admin_ssh_public_key
Public key for the cah-admin user; an optional trailing comment is dropped before cloud-init.
string required ^(ssh-ed25519|ssh-rsa|ecdsa-sha2-nistp(256|384|521)) [A-Za-z0-9+/]+=*( [A-Za-z0-9@._+:=/-]{1,64})?$ set by the calling stack (not a user setting) ssh-ed25519 AAAAEXAMPLEONLYNOTAREALKEY private (not a credential, but identifies the owner) exists
reboot_time
HH:MM (in timezone, the droplet's local time) when unattended-upgrades may reboot this droplet.
string required ^([01][0-9]|2[0-3]):[0-5][0-9]$ set by the calling stack (not a user setting) 04:00 public exists
timezone
IANA time zone of the machines, also the zone reboot_time(s) and the weekly connector update are read in. Replaces a literal in the cloud-init template; the default renders byte-identical user_data.
string "America/New_York" ^(UTC|[A-Za-z_]+(/[A-Za-z0-9_+-]+){1,2})$ (an IANA-shaped name; line breaks, spaces and YAML-special characters are rejected) and the name must be listed in modules/twingate-bootstrap/timezones.txt (generated by scripts/gen_timezones.py; enforced in the bootstrap module, which every stack goes through). Changing it changes user_data, which is force-new and replaces the droplets set by the calling stack (not a user setting) America/New_York public exists
connector_telemetry
Connector telemetry. Default {} adds no line to the connector config: user_data is byte-identical (golden hashes in the tests). status_updates_enabled is provider-side; the other fields render lines in /etc/twingate/connector.conf and change user_data. Labels are shown in the Twingate Admin Console: never put a secret in one.
object({status_updates_enabled = optional(bool), log_level = optional(number), connection_logs = optional(bool, false), metrics_port = optional(number), labels = optional(map(string), {})}) {} log_level in {3, 4, 5, 7} or unset; metrics_port a whole number 1024 to 65535 and not 9100, or unset; labels: at most 10, keys ^[A-Z][A-Z0-9_]{0,31}$, values ^[A-Za-z0-9._-]{1,64}$ (no spaces, quotes or line breaks, because they are written into the connector config); connector_telemetry is non-nullable (null means {}) the calling module { log_level = 5, connection_logs = true, metrics_port = 9999, labels = { DEPLOYED_BY = "opentofu" } } public exists
node_exporter
Opt-in node exporter: installs prometheus-node-exporter from the distro archive and binds it to the machine's private IPv4 address only (a boot-time script picks the first RFC 1918 address and refuses to start the exporter if there is none). Default {} (enabled false): user_data is byte-identical (golden hashes in the tests). enabled = true changes user_data and REPLACES the machine. With host_firewall the port is allowed from host_firewall_ssh_sources. NOT applied and NOT tested on a real machine yet.
object({enabled = optional(bool, false), port = optional(number, 9100)}) {} port a whole number from 1024 to 65535 (checked even while disabled); non-nullable (null means {}); must differ from connector_telemetry.metrics_port (a precondition) the calling module { enabled = true, port = 9100 } public exists
log_shipping
Opt-in log shipping (Wazuh agent and auditd, enrolled at first boot with a secret pulled from Doppler). Default {} (enabled false): user_data is byte-identical (golden hashes in the tests). enabled = true changes user_data and REPLACES the machine. Required when enabled: manager_address, agent_name (<site>-connector-<instance>, ending in this machine's instance) and apt_key_fingerprint (40 uppercase hex digits, no default on purpose). No secret is an input here. NOT applied and NOT tested on a real machine; the 1 GB droplet's memory headroom is UNMEASURED.
object({enabled = optional(bool, false), manager_address = optional(string, ""), manager_port = optional(number, 1514), enroll_port = optional(number, 1515), agent_name = optional(string, ""), agent_group = optional(string, ""), apt_key_fingerprint = optional(string, ""), agent_version = optional(string, "")}) {} ports whole numbers 1-65535 (checked even while disabled); manager_address, agent_name (^[a-z][a-z0-9-]{0,55}-connector-[a-z0-9]$) and apt_key_fingerprint (^[0-9A-F]{40}$) required when enabled; agent_group and agent_version format checks; agent_name must end in -connector-<instance> (a precondition) the calling module { enabled = true, manager_address = "siem.example.com", agent_name = "site-tor1-connector-a", apt_key_fingerprint = "<40 uppercase hex characters>" } private (not a credential, but identifies the owner) exists
log_shipping_bootstrap_token
The one secret-shaped value that reaches user_data when log_shipping is enabled: a short-lived, read-only Doppler token for the hostboot config. Written to a root-only file that the first-boot script deletes before using it. Required when log_shipping.enabled; never rendered otherwise.
string "" empty or 16-512 characters of letters, digits, dot, underscore, hyphen; required when log_shipping.enabled (a precondition) the calling module "" bootstrap credential (a short-lived bearer token rendered into user_data for first boot) exists
metadata_lock
Lock the metadata endpoint (169.254.169.254) to root and the do-agent user. Default false changes nothing: user_data is byte-identical (golden hashes in the tests). true adds an nftables table, a script, a systemd unit and the nftables package to user_data, which replaces the machine.
bool false none; non-nullable (null means false) the calling module true public exists
resource_key
Key used in resource names in place of region; empty means region. Set by the calling stack from its resource_key.
string "" empty, or ^[a-z][a-z0-9-]{1,23}$ set by the calling stack (not a user setting) tor1 public exists

modules/twingate-connector

Setting Type Default Validation Set where Example Sensitivity Status
host_firewall
A default-drop host firewall (nftables, input only). Default false changes nothing: user_data is byte-identical (golden hashes in the tests). true adds a script, a systemd unit and the nftables package to user_data, which replaces the droplet.
bool false none; non-nullable (null means false) the calling stack true public exists
host_firewall_ssh_sources
IPv4 CIDRs allowed to reach TCP 22 (and the metrics port) when host_firewall is true. Default the three private RFC 1918 blocks.
list(string) ["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"] none here; the bootstrap module requires IPv4 CIDRs without leading zeros, prefix 1 to 32 (/0 refused), no repeats the calling stack ["10.0.0.0/8"] public exists
layer
Stack layer, used in names (the module has no description for it).
string required none set by the calling stack (not a user setting) access public exists
region
Region, used in names (the module has no description for it).
string required none set by the calling stack (not a user setting) tor1 public exists
vpc_id
VPC the droplet joins (the module has no description for it).
string required none set by the calling stack (not a user setting) 00000000-0000-4000-8000-000000000000 public exists
tags
Droplet tags, including the baseline firewall tag. Set at creation.
list(string) required none set by the calling stack (not a user setting) ["cah-baseline-tor1", "cah-connector-tor1"] public exists
size
Droplet size slug. The stack reads it from cost.json.
string required none in the module (cost_check.py checks the price table) set by the calling stack (not a user setting) s-1vcpu-1gb public exists
remote_network_id
Twingate remote network (site) shared by every connector of the stack.
string required none set by the calling stack (not a user setting) example-remote-network-id public exists
ssh_key_fingerprint
Fingerprint of the stack's admin SSH key.
string required none set by the calling stack (not a user setting) 00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00 public exists
instance
One lowercase letter or digit that tells this connector from its siblings (a, b).
string required none in this module (the bootstrap module checks it) set by the calling stack (not a user setting) a public exists
twingate_network
Bare Twingate network subdomain.
string required none in this module (the bootstrap module checks it) set by the calling stack (not a user setting) example-tenant private (not a credential, but identifies the owner) exists
admin_ssh_public_key
Public key for the cah-admin user.
string required none in this module (the bootstrap module checks it) set by the calling stack (not a user setting) ssh-ed25519 AAAAEXAMPLEONLYNOTAREALKEY private (not a credential, but identifies the owner) exists
reboot_time
HH:MM (America/New_York) when unattended-upgrades may reboot.
string required none in this module (the bootstrap module checks it) set by the calling stack (not a user setting) 04:00 public exists
timezone
IANA time zone of the machines, also the zone reboot_time(s) and the weekly connector update are read in. Replaces a literal in the cloud-init template; the default renders byte-identical user_data.
string "America/New_York" ^(UTC|[A-Za-z_]+(/[A-Za-z0-9_+-]+){1,2})$ (an IANA-shaped name; line breaks, spaces and YAML-special characters are rejected) and the name must be listed in modules/twingate-bootstrap/timezones.txt (generated by scripts/gen_timezones.py; enforced in the bootstrap module, which every stack goes through). Changing it changes user_data, which is force-new and replaces the droplets set by the calling stack (not a user setting) America/New_York public exists
connector_telemetry
Connector telemetry, passed to the bootstrap module (which validates it). Default {} changes nothing. log_level, connection_logs, metrics_port and labels change user_data and replace the droplet; status_updates_enabled does not. Labels are shown in the Twingate Admin Console: never put a secret in one.
object({status_updates_enabled = optional(bool), log_level = optional(number), connection_logs = optional(bool, false), metrics_port = optional(number), labels = optional(map(string), {})}) {} none (validated in modules/twingate-bootstrap); connector_telemetry is non-nullable (null means {}) the calling stack { log_level = 5, connection_logs = true, metrics_port = 9999, labels = { DEPLOYED_BY = "opentofu" } } public exists
node_exporter
Opt-in node exporter bound to the private address, passed to the bootstrap module, which validates it. Default {} changes nothing. enabled = true changes user_data and replaces the droplet.
object({enabled = optional(bool, false), port = optional(number, 9100)}) {} none (validated in modules/twingate-bootstrap); node_exporter is non-nullable (null means {}) the calling stack { enabled = true, port = 9100 } public exists
log_shipping
Opt-in Wazuh agent and auditd with a first-boot enrollment, passed to the bootstrap module, which validates it. Default {} changes nothing. enabled = true changes user_data and replaces the droplet.
object({enabled = optional(bool, false), manager_address = optional(string, ""), manager_port = optional(number, 1514), enroll_port = optional(number, 1515), agent_name = optional(string, ""), agent_group = optional(string, ""), apt_key_fingerprint = optional(string, ""), agent_version = optional(string, "")}) {} none (validated in modules/twingate-bootstrap); non-nullable (null means {}) the calling stack { enabled = true, manager_address = "siem.example.com", agent_name = "site-tor1-connector-a", apt_key_fingerprint = "<40 uppercase hex characters>" } private (not a credential, but identifies the owner) exists
log_shipping_bootstrap_token
Short-lived, read-only Doppler token for the hostboot config, passed to the bootstrap module. Required when log_shipping.enabled; never rendered otherwise.
string "" none (validated in modules/twingate-bootstrap) the calling stack "" bootstrap credential (a short-lived bearer token rendered into user_data for first boot) exists
metadata_lock
Lock the metadata endpoint (169.254.169.254) to root and the do-agent user. Default false changes nothing: user_data is byte-identical (golden hashes in the tests). true adds an nftables table, a script, a systemd unit and the nftables package to user_data, which replaces the droplet.
bool false none; non-nullable (null means false) the calling stack true public exists
resource_key
Key used in resource names in place of region; empty means region. Set by the calling stack from its resource_key.
string "" empty, or ^[a-z][a-z0-9-]{1,23}$ set by the calling stack (not a user setting) tor1 public exists

modules/aws/vpc

Setting Type Default Validation Set where Example Sensitivity Status
layer
Stack layer, used in names (for example foundation).
string required none set by the calling stack (not a user setting) access public exists
region
AWS region name (for example us-east-1). Used in names; the stack's provider block decides where the VPC is really created.
string required none set by the calling stack (not a user setting) us-east-1 public exists
ip_range
Private IPv4 CIDR for the VPC. The one public subnet uses the whole range.
string required see the validation block in variables.tf set by the calling stack (not a user setting) 10.99.0.0/24 private (not a credential, but identifies the owner) exists
availability_zone
Availability zone of the subnet (for example us-east-1a). Null lets AWS choose. AWS extension: a subnet lives in exactly one zone.
string null none set by the calling stack (not a user setting) us-east-1a public exists

modules/aws/firewall

Setting Type Default Validation Set where Example Sensitivity Status
layer
Stack layer, used in names (for example access).
string required none set by the calling stack (not a user setting) access public exists
region
AWS region name (for example us-east-1). Used in names.
string required none set by the calling stack (not a user setting) us-east-1 public exists
purpose
Role in the group name (for example connector).
string required none set by the calling stack (not a user setting) connector public exists
vpc_id
VPC the security group belongs to. From the vpc module's id.
string required none set by the calling stack (not a user setting) vpc-00000000000000000 public exists
extra_outbound_rules
Outbound rules to any IPv4 address, appended to the five defaults. port_range is a port (443), a range (30000-31000) or "all" (ports 0-65535 of that one protocol).
list(object({protocol=string,port_range=string})) [] see the validation block in variables.tf set by the calling stack (not a user setting) [{ protocol = "tcp", port_range = "30000-31000" }] public exists
inbound_from_member_rules
Inbound rules that admit only members of this security group itself (a rule whose source is the group). Empty means no inbound rule at all.
list(object({protocol=string,port_range=string})) [] see the validation block in variables.tf set by the calling stack (not a user setting) [{ protocol = "tcp", port_range = "22" }] public exists
inbound_from_other_groups
Inbound rules that admit only members of the given OTHER security group (one rule per entry, source = that group, never an address, IPv6 range or prefix list). Additive to inbound_from_member_rules. Empty means none. A source equal to this module's own group is refused (use inbound_from_member_rules).
list(object({protocol=string,port_range=string,security_group_id=string})) [] see the validation blocks in variables.tf (tcp or udp, ports 1 to 65535, sg-... id, no repeats); a source equal to the module's own group is refused by a precondition on the rule set by the calling stack (not a user setting) [{ protocol = "tcp", port_range = "9100", security_group_id = "sg-0123456789abcdef0" }] public exists
outbound_to_member_rules
Outbound rules that reach only members of this security group itself (a rule whose destination is the group, no address). Empty means none.
list(object({protocol=string,port_range=string})) [] see the validation block in variables.tf set by the calling stack (not a user setting) [{ protocol = "tcp", port_range = "22" }] public exists

modules/aws/twingate-connector

Setting Type Default Validation Set where Example Sensitivity Status
host_firewall
A default-drop host firewall (nftables, input only). Default false changes nothing: user_data is byte-identical (golden hashes in the tests). true adds a script, a systemd unit and the nftables package to user_data, which replaces the instance.
bool false none; non-nullable (null means false) the calling stack true public exists
host_firewall_ssh_sources
IPv4 CIDRs allowed to reach TCP 22 (and the metrics port) when host_firewall is true. Default the three private RFC 1918 blocks.
list(string) ["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"] none here; the bootstrap module requires IPv4 CIDRs without leading zeros, prefix 1 to 32 (/0 refused), no repeats the calling stack ["10.0.0.0/8"] public exists
layer
layer (the module has no description for it).
string required none set by the calling stack (not a user setting) access public exists
region
region (the module has no description for it).
string required none set by the calling stack (not a user setting) us-east-1 public exists
subnet_id
Public subnet the instance launches into. From the vpc module's subnet_id.
string required none set by the calling stack (not a user setting) subnet-00000000000000000 public exists
security_group_id
The one security group attached to the instance's network interface at launch, so there is no unprotected moment. From the firewall module's id (its single rule target). A single string, not a list: security group rules are additive, so any second group a caller added could open inbound access to the public address.
string required see the validation block in variables.tf set by the calling stack (not a user setting) sg-00000000000000000 public exists
instance_type
EC2 instance type. The stack reads it from cost.json (local.<x>.type) so the price and the plan can never disagree.
string required see the validation block in variables.tf set by the calling stack (not a user setting) t4g.micro public exists
architecture
CPU architecture of the Ubuntu image: arm64 (Graviton types such as t4g) or amd64 (types such as t3). Must match instance_type.
string "arm64" see the validation block in variables.tf set by the calling stack (not a user setting) arm64 public exists
root_volume_size
Root disk size in GiB (gp3). This number is an input to the cost gate: the AMI's own default is not priced, so the size is always set here. 8 GiB is the smallest the Ubuntu image accepts.
number 8 see the validation block in variables.tf set by the calling stack (not a user setting) 8 public exists
remote_network_id
Twingate remote network (site) shared by every connector of the stack.
string required none set by the calling stack (not a user setting) example-remote-network-id public exists
instance
One lowercase letter or digit that tells this connector from its siblings (a, b).
string required none in this module (the bootstrap module checks it) set by the calling stack (not a user setting) a public exists
twingate_network
Bare Twingate network subdomain (acme for acme.twingate.com). Sensitive because the tenant slug identifies the tenant.
string required none in this module (the bootstrap module checks it) set by the calling stack (not a user setting) example-tenant private (not a credential, but identifies the owner) exists
admin_ssh_public_key
Public key (type and key, no comment) for the cah-admin user. Installed by cloud-init; no AWS key pair is used.
string required none in this module (the bootstrap module checks it) set by the calling stack (not a user setting) ssh-ed25519 AAAAEXAMPLEONLYNOTAREALKEY private (not a credential, but identifies the owner) exists
reboot_time
HH:MM (in timezone, the instance's local time) when unattended-upgrades may reboot this instance. Give each connector a different time so they never reboot together.
string required none in this module (the bootstrap module checks it) set by the calling stack (not a user setting) 04:00 public exists
timezone
IANA time zone of the machines, also the zone reboot_time(s) and the weekly connector update are read in. Replaces a literal in the cloud-init template; the default renders byte-identical user_data.
string "America/New_York" ^(UTC|[A-Za-z_]+(/[A-Za-z0-9_+-]+){1,2})$ (an IANA-shaped name; line breaks, spaces and YAML-special characters are rejected) and the name must be listed in modules/twingate-bootstrap/timezones.txt (generated by scripts/gen_timezones.py; enforced in the bootstrap module, which every stack goes through). Changing it changes user_data, which is force-new and replaces the droplets set by the calling stack (not a user setting) America/New_York public exists
connector_telemetry
Connector telemetry, passed to the bootstrap module (which validates it). Default {} changes nothing. log_level, connection_logs, metrics_port and labels change user_data and replace the instance; status_updates_enabled does not. Labels are shown in the Twingate Admin Console: never put a secret in one.
object({status_updates_enabled = optional(bool), log_level = optional(number), connection_logs = optional(bool, false), metrics_port = optional(number), labels = optional(map(string), {})}) {} none (validated in modules/twingate-bootstrap); connector_telemetry is non-nullable (null means {}) the calling stack { log_level = 5, connection_logs = true, metrics_port = 9999, labels = { DEPLOYED_BY = "opentofu" } } public exists
metadata_lock
Lock the metadata endpoint (169.254.169.254) to root and the do-agent user. Default false changes nothing: user_data is byte-identical (golden hashes in the tests). true adds an nftables table, a script, a systemd unit and the nftables package to user_data, which replaces the instance.
bool false none; non-nullable (null means false) the calling stack true public exists
node_exporter
Opt-in node exporter bound to the private address, passed to the bootstrap module, which validates it. Default {} changes nothing (user_data is byte-identical, golden hashes in the tests). enabled = true changes user_data and replaces the instance.
object({enabled = optional(bool, false), port = optional(number, 9100)}) {} none (validated in modules/twingate-bootstrap); node_exporter is non-nullable (null means {}) the calling stack { enabled = true, port = 9100 } public exists

Values read from cost.json, approved.json and the price table

These are not OpenTofu variables. The cost gate (scripts/cost_check.py) reads them, and stack 10 reads its droplet sizes from cost.json, so there is one source of truth for money. To change a size you edit cost.json and docs/stages/approved.json together; that is the lesson.

Setting File Pointer Current value Validation Example Status
connector-a size stacks/10-access/cost.json /resources/0/size "s-1vcpu-1gb" price must exist in docs/cost-prices.json; both connectors are kept equal by a test s-1vcpu-1gb exists
connector-b size stacks/10-access/cost.json /resources/1/size "s-1vcpu-1gb" price must exist in docs/cost-prices.json s-1vcpu-1gb exists
connector-a backups stacks/10-access/cost.json /resources/0/backups false false or true; true adds 20% of the droplet price false exists
connector-b backups stacks/10-access/cost.json /resources/1/backups false false or true false exists
stage 0 resources stacks/00-foundation/cost.json /resources [] list of costed resources; empty because stage 0 costs nothing [] exists
approved monthly total (DigitalOcean) docs/stages/approved.json /providers/digitalocean/approved_monthly_total 12.0 the computed total may not exceed it; the sum across providers may not exceed the ceiling 12.0 exists
monthly ceiling (current phase) docs/stages/approved.json /phases/stage-1/ceiling 30.0 dollars; one ceiling per phase in phases, the one named by current_phase is enforced; a later phase may raise it, lowering needs lowers: true, and hard_max (required, with hard_max_reason) caps every phase 30.0 exists
droplet price table entry docs/cost-prices.json /droplets/s-1vcpu-1gb 6 dollars per month for a size; a size absent from the table fails the gate 6 exists
AWS connector-a instance type stacks/aws/10-access/cost.json /resources/0/type "t4g.micro" price must exist in docs/cost-prices/aws.json; read by the stack as local.connector_a.type; both connectors are kept equal by a test t4g.micro exists
AWS connector-b instance type stacks/aws/10-access/cost.json /resources/1/type "t4g.micro" price must exist in docs/cost-prices/aws.json; read by the stack as local.connector_b.type; both connectors are kept equal by a test t4g.micro exists
approved monthly total (AWS) docs/stages/approved.json /providers/aws/approved_monthly_total 21.0 the computed AWS total may not exceed it; counted against the ceiling only while the AWS entry is active 21.0 exists

Settings that are hard-coded today

These are literals in the code, not variables. They are listed so the table is complete and so you know a change is a code edit, not a value.

Setting Where Current value Validation Plan Status
connector extra outbound rules stacks/10-access/main.tf tcp 30000-31000 and udp 1-65535 none kept hard-coded on purpose (low value to expose) exists
connector SSH rule between connectors stacks/10-access/main.tf tcp 22, tag to tag firewall module validation kept hard-coded on purpose exists
Twingate remote network location stacks/10-access/main.tf OTHER Twingate provider enum not planned exists
project environment stacks/00-foundation/main.tf Production DigitalOcean enum not planned exists

Secrets and private values: names only

This table lists names and where they live. It never holds a value. See Doppler and secrets for how they reach a run.

Name Class Lives in Used by Delivered as Expiry Rotation Status
DIGITALOCEAN_TOKEN secret Doppler config prd stacks/00-foundation, stacks/10-access DIGITALOCEAN_TOKEN 90 days docs/spacelift-secrets.md exists
TWINGATE_API_TOKEN secret Doppler config prd stacks/10-access, stacks/20-telemetry TWINGATE_API_TOKEN 90 days docs/spacelift-secrets.md exists
TWINGATE_NETWORK private Doppler config prd stacks/10-access, stacks/20-telemetry TWINGATE_NETWORK and TF_VAR_twingate_network none not a credential exists
ADMIN_SSH_PUBLIC_KEY private Doppler config prd stacks/10-access TF_VAR_admin_ssh_public_key none replace the key and re-apply exists
ADMIN_SSH_KEY_NAME private Doppler config prd stacks/10-access TF_VAR_admin_ssh_key_name none not a credential exists
ADMIN_USER_IDS private Doppler config prd stacks/10-access TF_VAR_admin_user_ids none not a credential exists
DOPPLER_TOKEN secret Spacelift context cah-doppler (write-only) the Spacelift hook script DOPPLER_TOKEN 90 days docs/spacelift-secrets.md exists
DOPPLER_PROJECT private Spacelift context cah-doppler the Spacelift hook script DOPPLER_PROJECT none not a credential exists
DOPPLER_CONFIG private Spacelift context cah-doppler the Spacelift hook script DOPPLER_CONFIG none not a credential exists
Per-host Twingate service keys secret Doppler hostboot config (one per site) each host, pulled at first boot first-boot pull, never a variable 365 days by default issue a new key, revoke the old one planned
Doppler hostboot bootstrap token secret Doppler service token for the config cah-<resource_key>-hostboot, created out of band; handed to the stack as TF_VAR_log_shipping_bootstrap_token stacks/10-access (user_data of each connector with log_shipping.enabled) sensitive stack variable log_shipping_bootstrap_token; the first-boot script deletes the token file UNVERIFIED: must expire on its own within a day; revoke after the first boot if Doppler's minimum is longer create a new token for each rebuild; revoke the old one planned
Wazuh enrollment password secret Doppler hostboot config (one per site) each connector with log_shipping.enabled, pulled at first boot as WAZUH_ENROLLMENT_PASSWORD first-boot pull, never a variable rotated after each enrollment window rotate on the manager planned
Grafana admin password secret Doppler hostboot config (one per site) optional NOC host first-boot pull, never a variable none rotate in Doppler and on the host planned

Settings that live somewhere else

Nothing here is checked by a machine except that a pointer file exists. Each has an owner and a place to look.

Setting Owner Where Pointer Status
Spacelift stack environment (foundation outputs mapped to TF_VAR_*) maintainer Spacelift UI, stack dependencies of cah-10-access docs/stages/01-access.md exists
Doppler project and config prd maintainer Doppler console docs/spacelift-secrets.md exists
Twingate console (remote network, connectors, users) maintainer Twingate admin console docs/wiki/Twingate.md exists
DigitalOcean account (sizes available to the account, SSH key registration) maintainer DigitalOcean console and API docs/cost.md exists
Home Prometheus scrape interval home monitoring repository that repository's target file specified in the stage 2 design, not built here planned
Home firewall rules for the logging connector maintainer the home firewall specified in the stage 2 design, not built here planned

Next: Variables, tfvars and secrets, step 5 of the build track on the Reading order page, which says which of these places a value belongs in and why.

Home | Reading order

Core path: read in this order
  1. Start here if you are new
  2. What is the cloud
  3. Networking basics
  4. Identity, secrets, zero trust
  5. Infrastructure as code
  6. Git and pull requests
  7. Containers and Docker (optional)
  8. How money works
  9. Enterprise habits
  10. Architecture overview
  11. Why ZTNA and not a VPN
  12. Twingate
  13. DigitalOcean network and firewalls
  14. Droplet hardening
  15. OpenTofu, stacks and modules
  16. Spacelift
  17. Doppler and secrets
  18. CI, tests and the cost gate
  19. Threat model
  20. Lessons learned
  21. Roadmap
Track: build it yourself
Track: understand the design

The rest of this track is on the core path; see Reading order.

Track: security and threat modelling

The Threat model is core step 19.

Track: detection and monitoring
Enterprise context
Track: other clouds (unverified drafts)
Reference and operator guides

Clone this wiki locally