{"playbook":"Infrastructure","url":"https://agentwranglers.se/playbook/infrastructure","updated":"2026-10-21","license":"https://creativecommons.org/licenses/by/4.0/","credit":"Required by the licence: credit Agent Wranglers with a link to the rule or playbook you used.","how":"Each rule's How text is on its own page: add .md to its url.","rules":[{"chapter":"Infrastructure as code","name":"Define every resource in code","level":"Recommended","rule":"Create and change every cloud resource, firewall rule and DNS record through code in version control. Never make a lasting change in a web console.","check":"The cloud audit log for production, such as AWS CloudTrail, shows no write calls in the last 30 days from anyone but the deploy role. A plan on the main branch shows no changes.","url":"https://agentwranglers.se/playbook/infrastructure/infrastructure-as-code/define-infrastructure-in-code","related":["/playbook/engineering/version-control-and-review/everything-in-version-control","/playbook/infrastructure/infrastructure-as-code/import-existing-resources"],"sources":[]},{"chapter":"Infrastructure as code","name":"Keep state remote with locking and encryption","level":"Required","risk":"data","rule":"Keep the state file in a remote backend that locks, encrypts and keeps old versions. Only the deploy roles may read it. Never keep state in git or on a laptop.","check":"`git ls-files '*.tfstate*'` prints nothing. Start two plans on the same state at once, and the second stops with a lock error. The bucket reports versioning `Enabled` and public access blocked. Its policy grants read access to the deploy roles only.","url":"https://agentwranglers.se/playbook/infrastructure/infrastructure-as-code/remote-state","related":["/playbook/adversarial-testing/data-and-secrets/secrets-out-of-code","/playbook/infrastructure/configuration-and-secrets/secrets-out-of-state","/playbook/stack/delivery-and-infrastructure/opentofu"],"sources":[]},{"chapter":"Infrastructure as code","name":"Review the plan and apply only from CI","level":"Recommended","rule":"Post the plan in every pull request. Apply only from CI after merge, from the plan file made for that commit. No person applies from a laptop.","check":"The workflow file runs `apply` only on a saved plan file, in a job limited to the main branch and a protected environment. No human role has write rights on the state bucket.","url":"https://agentwranglers.se/playbook/infrastructure/infrastructure-as-code/plan-in-review","related":["/playbook/infrastructure/identity-and-access/separate-plan-and-apply-roles","/playbook/engineering/version-control-and-review/protect-the-main-branch"],"sources":[]},{"chapter":"Infrastructure as code","name":"Pin the tool and provider versions","level":"Recommended","rule":"Pin the version of the tool and of every provider, and commit the provider lock file with hashes for every platform you run on.","check":"`git ls-files .terraform.lock.hcl` prints the file. `init -lockfile=readonly` passes in CI. The `terraform` block sets `required_version`, and every provider block names a version.","url":"https://agentwranglers.se/playbook/infrastructure/infrastructure-as-code/pinned-provider-versions","related":["/playbook/engineering/running-in-production/reproducible-builds","/playbook/engineering/version-control-and-review/automated-dependency-updates"],"sources":[]},{"chapter":"Infrastructure as code","name":"Split state by environment and layer","level":"Recommended","rule":"Give each environment and each layer its own state, such as network, data and app. One mistake then touches only one of them.","check":"Every backend key names exactly one environment. `state list` in any app state shows no database or network resources. Each plan finishes in under 2 minutes.","url":"https://agentwranglers.se/playbook/infrastructure/infrastructure-as-code/split-state","related":["/playbook/infrastructure/environments/account-per-environment"],"sources":[]},{"chapter":"Infrastructure as code","name":"Reuse versioned modules","level":"Recommended","rule":"Put each repeated pattern in a module with checked inputs, and pin every use of it to a version. Never copy the same resources between environments.","check":"Every git `source` line holds `?ref=` with a version tag, as the grep below confirms. Every registry module block has a `version` argument.","url":"https://agentwranglers.se/playbook/infrastructure/infrastructure-as-code/versioned-modules","related":["/playbook/engineering/version-control-and-review/automated-dependency-updates"],"sources":[]},{"chapter":"Infrastructure as code","name":"Detect drift every day","level":"Recommended","rule":"Run a plan against every state at least once a day. Open a ticket when it finds a change nobody made in code.","check":"The drift job ran in the last 24 hours for every state. Change a tag by hand on a staging resource, and a ticket opens within 24 hours.","url":"https://agentwranglers.se/playbook/infrastructure/infrastructure-as-code/drift-detection","related":["/playbook/infrastructure/infrastructure-as-code/import-existing-resources"],"sources":[]},{"chapter":"Infrastructure as code","name":"Protect resources that hold data","level":"Required","risk":"data","rule":"Turn on deletion protection for every database, bucket and disk that holds data. Stop any plan that deletes or replaces one until a second person approves.","check":"Change a database's name in a branch. The CI plan step fails and names the resource. In the cloud, a delete call on the production database is refused.","url":"https://agentwranglers.se/playbook/infrastructure/infrastructure-as-code/protect-stateful-resources","related":["/playbook/operations/disaster-recovery/off-site-backups"],"sources":[]},{"chapter":"Infrastructure as code","name":"Bring hand-made resources under code","level":"Recommended","rule":"Import every resource that was made by hand into code before you change it. Then tag everything the code manages, so the rest stands out.","check":"After the import, the plan shows no changes. A resource search in production, such as AWS Resource Explorer, finds no taggable resource without the `managed-by` tag.","url":"https://agentwranglers.se/playbook/infrastructure/infrastructure-as-code/import-existing-resources","related":["/playbook/infrastructure/infrastructure-as-code/define-infrastructure-in-code","/playbook/infrastructure/infrastructure-as-code/drift-detection"],"sources":[]},{"chapter":"Environments","name":"Give each environment its own account","level":"Recommended","rule":"Run production in its own cloud account, project or subscription. Development, staging and previews each live in another one.","check":"The production account holds no resource tagged with another environment. The staging deploy role tries to assume the production deploy role and gets AccessDenied.","url":"https://agentwranglers.se/playbook/infrastructure/environments/account-per-environment","related":["/playbook/infrastructure/infrastructure-as-code/split-state","/playbook/payments/checkout-and-card-data/test-and-live-keys"],"sources":[]},{"chapter":"Environments","name":"Build every environment from the same code","level":"Recommended","rule":"Build all environments from the same modules. Let them differ only in values such as sizes, counts and names, kept in one file per environment.","check":"A diff between the staging and production folders shows only variables and backend settings. Both environments report the same module versions and engine versions.","url":"https://agentwranglers.se/playbook/infrastructure/environments/same-code-every-environment","related":["/playbook/testing/test-data-and-environments/production-like-staging"],"sources":[]},{"chapter":"Environments","name":"Change staging before production","level":"Recommended","rule":"Apply every infrastructure change to staging first. Apply the same commit to production only after the staging apply and its smoke tests pass.","check":"For every production apply in the last 90 days, the CI history shows a successful staging apply of the same commit before it.","url":"https://agentwranglers.se/playbook/infrastructure/environments/promote-infrastructure-changes","related":["/playbook/engineering/build-test-and-deploy/staged-rollouts"],"sources":[]},{"chapter":"Environments","name":"Delete preview environments on time","level":"Recommended","rule":"Give each pull request its own short-lived environment from the same code. Delete it when the pull request closes, and after 72 hours at most.","check":"The nightly job lists no preview older than 72 hours. Close a test pull request, and its environment is gone within 1 hour.","url":"https://agentwranglers.se/playbook/infrastructure/environments/preview-environments","related":["/playbook/email/email-privacy/test-mail-capture"],"sources":[]},{"chapter":"Environments","name":"Start every account from one baseline","level":"Recommended","rule":"Create every new account from code with the same baseline. That means an audit log, a budget, blocked public storage, default encryption and the allowed regions.","check":"For every account in the organization, a script confirms the audit trail is on, a budget exists and account-level public access is blocked. It also confirms default disk encryption is on and a call in a region outside the list ends in AccessDenied.","url":"https://agentwranglers.se/playbook/infrastructure/environments/account-baseline","related":["/playbook/infrastructure/cost-and-ownership/budgets-for-every-account","/playbook/infrastructure/policy-as-code/cloud-guardrails","/playbook/infrastructure/environments/threat-detection"],"sources":[]},{"chapter":"Environments","name":"Turn on threat detection in every account","level":"Recommended","rule":"Turn on the cloud's managed threat detection in every account and region you use. Send high and critical findings to the on-call team within 15 minutes.","check":"Every account and region in use reports the detector as enabled. Create sample findings in a sandbox account, and the on-call alert arrives within 15 minutes.","url":"https://agentwranglers.se/playbook/infrastructure/environments/threat-detection","related":["/playbook/infrastructure/environments/account-baseline","/playbook/operations/alerts-and-on-call/page-or-ticket"],"sources":[]},{"chapter":"Identity and access","name":"Sign people in through single sign-on","level":"Required","risk":"takeover","rule":"Give people cloud access only through the company identity provider, with two-factor login. Create no separate cloud user with a password for a person.","check":"The cloud lists no user with a console password, except the documented break-glass accounts. Disable a test person in the identity provider, and their next cloud call fails within 1 hour.","url":"https://agentwranglers.se/playbook/infrastructure/identity-and-access/single-sign-on-for-people","related":["/playbook/payments/payment-events-and-reconciliation/provider-dashboard-access","/playbook/adversarial-testing/access-and-accounts/two-factor-login","/playbook/compliance/audit-logs-and-access/offboarding-within-a-day"],"sources":[{"label":"NIST SP 800-63B-4 Authentication and Authenticator Management","url":"https://csrc.nist.gov/pubs/sp/800/63/b/4/final"}]},{"chapter":"Identity and access","name":"Keep no long-lived access keys","level":"Required","risk":"takeover","rule":"Use short-lived credentials for every person, pipeline and service. Delete every static access key, and alert when a new one appears.","check":"The script below lists no active key, apart from documented exceptions younger than that limit. Create a test key in a sandbox account, and the alert arrives within 15 minutes.","url":"https://agentwranglers.se/playbook/infrastructure/identity-and-access/no-long-lived-keys","related":["/playbook/adversarial-testing/data-and-secrets/least-privilege-keys","/playbook/infrastructure/configuration-and-secrets/oidc-for-ci","/playbook/infrastructure/configuration-and-secrets/workload-identity"],"sources":[{"label":"OWASP Secrets Management Cheat Sheet","url":"https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html"}]},{"chapter":"Identity and access","name":"Give every role the least it needs","level":"Required","risk":"takeover","rule":"Write every role's permissions in code, limited to named actions on named resources. Never grant every action on every resource, except to the break-glass role.","check":"A code scan, such as Checkov, finds no statement that allows `*` actions or a whole service, such as `s3:*`, on `*` resources. The break-glass role is the only exception. The access analyzer lists no unused permission older than 90 days.","url":"https://agentwranglers.se/playbook/infrastructure/identity-and-access/least-privilege-roles","related":["/playbook/adversarial-testing/data-and-secrets/least-privilege-keys"],"sources":[{"label":"NIST SP 800-53 Rev. 5 Security and Privacy Controls","url":"https://csrc.nist.gov/pubs/sp/800/53/r5/upd1/final"}]},{"chapter":"Identity and access","name":"Separate the roles for plan and apply","level":"Recommended","rule":"Give pull requests a read-only role that can only plan. Give the write role only to the main branch in a protected environment. Give the app its own runtime role.","check":"A pull request job that tries to assume the apply role gets AccessDenied. The plan role's policy holds only read actions.","url":"https://agentwranglers.se/playbook/infrastructure/identity-and-access/separate-plan-and-apply-roles","related":["/playbook/infrastructure/infrastructure-as-code/plan-in-review","/playbook/infrastructure/configuration-and-secrets/oidc-for-ci","/playbook/engineering/version-control-and-review/protect-the-main-branch"],"sources":[]},{"chapter":"Identity and access","name":"Lock the root account away","level":"Required","risk":"takeover","rule":"Protect every cloud root or owner account with a hardware key or passkey, give it no access keys, and alert on every sign-in.","check":"In the credential report, the root user of the management account has MFA on and no active access keys. Member accounts show no root password. A test root sign-in in a sandbox account triggers the alert within 15 minutes.","url":"https://agentwranglers.se/playbook/infrastructure/identity-and-access/lock-root-account","related":["/playbook/infrastructure/identity-and-access/break-glass-access"],"sources":[{"label":"NIST SP 800-63B-4 Authentication and Authenticator Management","url":"https://csrc.nist.gov/pubs/sp/800/63/b/4/final"}]},{"chapter":"Identity and access","name":"Keep tested break-glass accounts","level":"Recommended","rule":"Keep two emergency admin accounts that work when single sign-on is down. Store their factors offline, alert on every use, and test them twice a year.","check":"The last test sign-in is less than 6 months old and raised the alert. Both accounts have hardware keys registered.","url":"https://agentwranglers.se/playbook/infrastructure/identity-and-access/break-glass-access","related":["/playbook/infrastructure/identity-and-access/lock-root-account","/playbook/compliance/audit-logs-and-access/just-in-time-admin"],"sources":[]},{"chapter":"Identity and access","name":"Grant admin access for hours only","level":"Recommended","rule":"Give admin rights in production only on request, with a reason and an approver, for 8 hours at most.","check":"A query of the role assignments in production finds no person with a standing admin role, except the break-glass accounts. Every admin session in the audit log has a matching approved request.","url":"https://agentwranglers.se/playbook/infrastructure/identity-and-access/just-in-time-access","related":["/playbook/compliance/audit-logs-and-access/just-in-time-admin"],"sources":[]},{"chapter":"Configuration and secrets","name":"Create the secret's place in code and its value in the store","level":"Recommended","rule":"Create each secret's entry, owner and access policy in code. Put its value in the store by rotation or by hand, never through a code variable.","check":"The grep below finds no `secret_string`, password or key argument set to a quoted literal. Every secret in the store has an owner tag and exactly one reading role.","url":"https://agentwranglers.se/playbook/infrastructure/configuration-and-secrets/secret-containers-in-code","related":["/playbook/engineering/running-in-production/secrets-store"],"sources":[{"label":"OWASP Secrets Management Cheat Sheet","url":"https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html"}]},{"chapter":"Configuration and secrets","name":"Keep secrets out of state","level":"Required","risk":"takeover","rule":"Pass secrets to resources in a way that never writes them into the state or plan files.","check":"Pull the state and scan it with a secret scanner such as TruffleHog or gitleaks. It finds nothing. A search of the staging state for the staging database password finds no match.","url":"https://agentwranglers.se/playbook/infrastructure/configuration-and-secrets/secrets-out-of-state","related":["/playbook/engineering/running-in-production/secrets-store","/playbook/infrastructure/infrastructure-as-code/remote-state"],"sources":[]},{"chapter":"Configuration and secrets","name":"Let CI sign in with OIDC","level":"Recommended","rule":"Let pipelines sign in to the cloud with a short-lived token from the CI provider's OIDC issuer. Store no cloud key in CI secrets.","check":"The CI secret list holds no cloud key. The trust policy has a `sub` condition with no `*`. Run the workflow from another branch, and the cloud refuses the token.","url":"https://agentwranglers.se/playbook/infrastructure/configuration-and-secrets/oidc-for-ci","related":["/playbook/engineering/running-in-production/secrets-store","/playbook/infrastructure/identity-and-access/no-long-lived-keys","/playbook/infrastructure/identity-and-access/separate-plan-and-apply-roles","/playbook/stack/delivery-and-infrastructure/github-actions-ci"],"sources":[]},{"chapter":"Configuration and secrets","name":"Give services the identity of their machine","level":"Recommended","rule":"Let each running service get cloud credentials from the platform's identity, such as an instance role, a Kubernetes service account or a managed identity. Put no cloud key in its settings.","check":"The running service's environment holds no `AWS_SECRET_ACCESS_KEY`, and no `GOOGLE_APPLICATION_CREDENTIALS` that points to a key file. Its calls appear in the audit log under its role.","url":"https://agentwranglers.se/playbook/infrastructure/configuration-and-secrets/workload-identity","related":["/playbook/infrastructure/identity-and-access/no-long-lived-keys","/playbook/adversarial-testing/data-and-secrets/secrets-out-of-code"],"sources":[]},{"chapter":"Configuration and secrets","name":"Rotate secrets automatically","level":"Recommended","rule":"Let the secrets store rotate each secret on a schedule. The app picks up the new value without a deploy.","check":"The query below lists no secret without rotation or with a last rotation older than that limit. Documented secrets rotated by hand within the limit are the only exception. Trigger a rotation on staging, and the app keeps serving without a restart.","url":"https://agentwranglers.se/playbook/infrastructure/configuration-and-secrets/automatic-secret-rotation","related":["/playbook/engineering/running-in-production/secrets-store","/playbook/adversarial-testing/data-and-secrets/least-privilege-keys","/playbook/adversarial-testing/supply-chain-and-setup/compromised-package-response"],"sources":[{"label":"OWASP Secrets Management Cheat Sheet","url":"https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html"}]},{"chapter":"Configuration and secrets","name":"Keep encryption keys in a key service","level":"Recommended","rule":"Encrypt stored data with keys held in a key management service that never hands out the key itself. Rotate each key automatically, and keep the people who manage keys apart from the roles that use them.","check":"The script below lists no customer-managed key without automatic rotation. No key policy grants every key action to a role other than the key admin role. Schedule the deletion of a test key in a sandbox account, and the alert arrives within 15 minutes.","url":"https://agentwranglers.se/playbook/infrastructure/configuration-and-secrets/key-management","related":["/playbook/data/personal-data/retired-data-stores","/playbook/data/personal-data/field-encryption","/playbook/adversarial-testing/data-and-secrets/encrypt-data"],"sources":[{"label":"NIST SP 800-57 Part 1 Rev. 5 Recommendation for Key Management","url":"https://csrc.nist.gov/pubs/sp/800/57/pt1/r5/final"},{"label":"AWS Well-Architected SEC08-BP01 Implement secure key management","url":"https://docs.aws.amazon.com/wellarchitected/latest/security-pillar/sec_protect_data_rest_key_mgmt.html"}]},{"chapter":"Networking and DNS","name":"Keep databases in private subnets","level":"Required","risk":"data","rule":"Put every database, cache and queue in a private subnet with no public address. Only the app's own security group may reach it.","check":"The query below lists no database with a public address. A connection to the database port from outside the network times out.","url":"https://agentwranglers.se/playbook/infrastructure/networking-and-dns/private-data-stores","related":["/playbook/infrastructure/networking-and-dns/no-public-admin-ports","/playbook/adversarial-testing/data-and-secrets/private-storage-buckets"],"sources":[]},{"chapter":"Networking and DNS","name":"Deny all traffic unless a rule allows it","level":"Required","risk":"takeover","rule":"Start every security group and firewall from deny. Allow only named ports from named sources, and open a port to the whole internet only on the load balancer.","check":"The query below finds no rule open to the internet on any port but 80 or 443. Rules open on 80 or 443 belong only to the load balancer's security group. A policy scan fails a pull request that adds one.","url":"https://agentwranglers.se/playbook/infrastructure/networking-and-dns/deny-by-default-firewall","related":["/playbook/website/hosting-and-network/ipv6","/playbook/operations/containers/published-ports","/playbook/adversarial-testing/access-and-accounts/deny-by-default"],"sources":[{"label":"NIST SP 800-41 Rev. 1 Guidelines on Firewalls","url":"https://csrc.nist.gov/pubs/sp/800/41/r1/final"}]},{"chapter":"Networking and DNS","name":"Close admin ports to the internet","level":"Required","risk":"takeover","rule":"Never open SSH, RDP, database or admin ports to the internet. Reach machines through an identity-aware tunnel or a private network that needs single sign-on.","check":"A port scan from outside of every public address finds only 80 and 443 open, plus 25 on mail servers.","url":"https://agentwranglers.se/playbook/infrastructure/networking-and-dns/no-public-admin-ports","related":["/playbook/adversarial-testing/data-and-secrets/close-debug-doors","/playbook/infrastructure/networking-and-dns/private-data-stores","/playbook/infrastructure/servers/host-firewall","/playbook/infrastructure/servers/ssh-keys-only"],"sources":[]},{"chapter":"Networking and DNS","name":"Limit where servers can send traffic","level":"Recommended","rule":"Let servers send traffic only to the hosts they need. Databases send none to the internet at all.","check":"From an app server, a request to a host on the list succeeds and a request to `https://example.org` fails. A database server cannot reach any outside address.","url":"https://agentwranglers.se/playbook/infrastructure/networking-and-dns/egress-control","related":["/playbook/operations/containers/container-networks","/playbook/adversarial-testing/requests-and-files/block-server-side-request-forgery"],"sources":[{"label":"NIST SP 800-41 Rev. 1 Guidelines on Firewalls","url":"https://csrc.nist.gov/pubs/sp/800/41/r1/final"}]},{"chapter":"Networking and DNS","name":"Force encryption on every managed service","level":"Required","risk":"data","rule":"Configure every database, cache and bucket to refuse plain connections. Make every client check the server's certificate.","check":"A database connection without TLS is refused. A client given a wrong host name fails the certificate check. A bucket request over plain HTTP gets 403.","url":"https://agentwranglers.se/playbook/infrastructure/networking-and-dns/enforce-tls-on-services","related":["/playbook/adversarial-testing/data-and-secrets/database-scram-auth","/playbook/adversarial-testing/data-and-secrets/encrypt-data","/playbook/stack/delivery-and-infrastructure/lets-encrypt"],"sources":[{"label":"RFC 9325 Recommendations for Secure Use of TLS","url":"https://www.rfc-editor.org/rfc/rfc9325"}]},{"chapter":"Networking and DNS","name":"Require tokens for the metadata service","level":"Required","risk":"takeover","rule":"Make every cloud server's metadata service answer only requests that carry a session token. Allow the token to travel one network hop, or two when containers on the host need it.","check":"The query below lists no instance with tokens optional. No instance has a hop limit above 2. From the server, a GET to `169.254.169.254` without a token gets 401.","url":"https://agentwranglers.se/playbook/infrastructure/networking-and-dns/metadata-service-tokens","related":["/playbook/adversarial-testing/requests-and-files/block-server-side-request-forgery"],"sources":[{"label":"OWASP Server-Side Request Forgery Prevention Cheat Sheet","url":"https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html"}]},{"chapter":"Networking and DNS","name":"Keep DNS records in code","level":"Recommended","rule":"Manage every DNS record of every domain in code, changed by pull request. Never edit a record in the provider's web panel.","check":"A daily job runs the tool's preview against the live zone. It reports no difference.","url":"https://agentwranglers.se/playbook/infrastructure/networking-and-dns/dns-as-code","related":["/playbook/website/html-basics/for-sale-dns","/playbook/website/ready-for-ai-agents/dns-aid","/playbook/email/sending-domain-and-authentication/spf","/playbook/website/hosting-and-network/caa-records","/playbook/website/hosting-and-network/dnssec","/playbook/adversarial-testing/supply-chain-and-setup/dangling-dns","/playbook/stack/delivery-and-infrastructure/managed-dns-and-cdn"],"sources":[]},{"chapter":"Networking and DNS","name":"Shorten the TTL before you move a record","level":"Recommended","rule":"Before you point a name at a new server, shorten its TTL and wait one full old TTL. Restore the longer TTL after the move.","check":"Before the move window, `dig` on a public resolver shows a TTL of 300 or less for the name.","url":"https://agentwranglers.se/playbook/infrastructure/networking-and-dns/lower-ttl-before-changes","related":["/playbook/website/redirects-and-canonicals/site-moves"],"sources":[{"label":"RFC 1035 Domain Names, Implementation and Specification","url":"https://www.rfc-editor.org/rfc/rfc1035"},{"label":"RFC 2181 section 8 Time to Live (TTL)","url":"https://www.rfc-editor.org/rfc/rfc2181#section-8"}]},{"chapter":"Networking and DNS","name":"Authenticate calls between your own services","level":"Recommended","rule":"Make every service prove who it is on every call to another service, even inside a private network. Refuse a call without a valid identity, and allow each caller only the services it needs.","check":"From a server inside the network, a call to an internal service with no certificate or token fails the handshake or gets 401. A call with a valid identity that is missing from the caller list gets 403.","url":"https://agentwranglers.se/playbook/infrastructure/networking-and-dns/service-to-service-auth","related":["/playbook/api-design/api-security/token-auth"],"sources":[{"label":"NIST SP 800-207 Zero Trust Architecture","url":"https://csrc.nist.gov/pubs/sp/800/207/final"},{"label":"NIST SP 800-204A Building Secure Microservices-based Applications Using Service-Mesh Architecture","url":"https://csrc.nist.gov/pubs/sp/800/204/a/final"}]},{"chapter":"Policy as code","name":"Scan infrastructure code on every change","level":"Recommended","rule":"Run an infrastructure scanner on every pull request, and fail the build on any finding rated high or critical.","check":"Add a bucket with public read access in a test branch. The CI scan fails and names the resource.","url":"https://agentwranglers.se/playbook/infrastructure/policy-as-code/scan-infrastructure-code","related":["/playbook/infrastructure/policy-as-code/expiring-policy-exceptions","/playbook/engineering/version-control-and-review/continuous-integration"],"sources":[{"label":"OWASP Infrastructure as Code Security Cheat Sheet","url":"https://cheatsheetseries.owasp.org/cheatsheets/Infrastructure_as_Code_Security_Cheat_Sheet.html"}]},{"chapter":"Policy as code","name":"Test your own rules against the plan","level":"Recommended","rule":"Write the house rules that no scanner knows as policies, and run them against the plan in CI. Examples are the allowed regions, the required tags and the largest server size.","check":"A plan that creates a server in a region outside the list fails with that policy's message. `conftest verify` passes the policies' own tests.","url":"https://agentwranglers.se/playbook/infrastructure/policy-as-code/policy-on-the-plan","related":["/playbook/engineering/rules-as-code/check-every-rule-in-code","/playbook/infrastructure/cost-and-ownership/required-tags"],"sources":[]},{"chapter":"Policy as code","name":"Block dangerous actions for the whole organization","level":"Recommended","rule":"Set organization-wide policies that refuse the most dangerous actions in every account, even for admins.","check":"In a sandbox account, an admin tries to stop the audit trail and to start a server in a region outside the list. Both calls end in AccessDenied.","url":"https://agentwranglers.se/playbook/infrastructure/policy-as-code/cloud-guardrails","related":["/playbook/ai-assisted-development/rules-the-agent-cannot-break/blocking-hooks","/playbook/infrastructure/environments/account-baseline"],"sources":[]},{"chapter":"Policy as code","name":"Let every exception expire","level":"Recommended","rule":"Give every skipped check a reason, an owner and an end date in the code. Fail the build once the date has passed.","check":"Set a skip's date to yesterday in a test branch. The build fails and names the file and line.","url":"https://agentwranglers.se/playbook/infrastructure/policy-as-code/expiring-policy-exceptions","related":["/playbook/infrastructure/policy-as-code/scan-infrastructure-code","/playbook/infrastructure/servers/hardening-checklist"],"sources":[]},{"chapter":"Policy as code","name":"Test infrastructure modules","level":"Recommended","rule":"Give every shared module tests that check its plan, and run them in CI on every change to the module.","check":"Remove the encryption setting from the module in a test branch. A module test fails.","url":"https://agentwranglers.se/playbook/infrastructure/policy-as-code/test-infrastructure-modules","related":["/playbook/testing/test-strategy/tests-with-the-change"],"sources":[]},{"chapter":"Servers","name":"Replace servers instead of patching them","level":"Recommended","rule":"Build a new machine image for every change, start new servers from it and stop the old ones. Never change a running server by hand.","check":"No running server is older than 30 days. Every server runs the image ID of the latest build.","url":"https://agentwranglers.se/playbook/infrastructure/servers/immutable-servers","related":["/playbook/infrastructure/servers/configuration-management","/playbook/engineering/running-in-production/reproducible-builds","/playbook/infrastructure/servers/automatic-security-updates"],"sources":[]},{"chapter":"Servers","name":"Configure long-lived servers from code","level":"Recommended","rule":"When a server lives for months, set up its packages, files and services from code that can run again without changing anything.","check":"Run the code twice in a row. The second run reports 0 changed. The scheduled check run reports no differences.","url":"https://agentwranglers.se/playbook/infrastructure/servers/configuration-management","related":["/playbook/infrastructure/servers/immutable-servers"],"sources":[]},{"chapter":"Servers","name":"Rebuild any server from code","level":"Recommended","rule":"Keep everything needed to rebuild each server in code, so one command builds a fresh one. Prove it on a new machine in the quarterly [restore drill](https://agentwranglers.se/playbook/operations/disaster-recovery/restore-drills).","check":"The last rebuild drill is under 3 months old. It built a working server from code and backups within the recovery time objective, without logging in to fix anything.","url":"https://agentwranglers.se/playbook/infrastructure/servers/rebuild-from-scratch","related":["/playbook/operations/disaster-recovery/restore-drills","/playbook/operations/disaster-recovery/restore-runbook"],"sources":[]},{"chapter":"Servers","name":"Install security updates by themselves","level":"Required","risk":"takeover","rule":"Let every server install security updates by itself every day, and reboot in a set window when the kernel needs it.","check":"`apt list --upgradable` lists no package from a security pocket, or `dnf updateinfo list --security` lists none, outside the 24 hours after a release. No server has had `/var/run/reboot-required` for more than 7 days.","url":"https://agentwranglers.se/playbook/infrastructure/servers/automatic-security-updates","related":["/playbook/adversarial-testing/supply-chain-and-setup/patch-and-hide-versions","/playbook/infrastructure/servers/immutable-servers","/playbook/stack/how-to-choose/still-maintained","/playbook/stack/delivery-and-infrastructure/ubuntu-lts-or-debian"],"sources":[{"label":"NIST SP 800-40 Rev. 4 Enterprise Patch Management","url":"https://csrc.nist.gov/pubs/sp/800/40/r4/final"}]},{"chapter":"Servers","name":"Turn on a firewall on every host","level":"Recommended","rule":"Run a firewall on every server that refuses all incoming traffic except the ports it serves. Allow SSH only from named admin addresses.","check":"`ufw status verbose` or `nft list ruleset` shows the incoming default as deny. A port scan from outside finds only the allowed ports.","url":"https://agentwranglers.se/playbook/infrastructure/servers/host-firewall","related":["/playbook/adversarial-testing/ai-agents-and-models/local-server-origin","/playbook/infrastructure/networking-and-dns/no-public-admin-ports","/playbook/infrastructure/servers/minimal-services"],"sources":[{"label":"NIST SP 800-41 Rev. 1 Guidelines on Firewalls","url":"https://csrc.nist.gov/pubs/sp/800/41/r1/final"}]},{"chapter":"Servers","name":"Allow SSH with keys only","level":"Required","risk":"takeover","rule":"Turn off password logins and direct root logins over SSH. Give each person their own key, and remove it the day they leave.","check":"`sshd -T` prints `passwordauthentication no`, `kbdinteractiveauthentication no` and `permitrootlogin no`. A login that offers only a password is refused. Run `sshd -t` before every reload, so a broken file never locks you out.","url":"https://agentwranglers.se/playbook/infrastructure/servers/ssh-keys-only","related":["/playbook/infrastructure/networking-and-dns/no-public-admin-ports","/playbook/compliance/audit-logs-and-access/offboarding-within-a-day"],"sources":[{"label":"NIST IR 7966 Security of SSH Access Management","url":"https://csrc.nist.gov/pubs/ir/7966/final"}]},{"chapter":"Servers","name":"Keep server clocks in sync","level":"Recommended","rule":"Run a time sync client on every server with at least two time sources. Keep every server on UTC, and alert when a clock is more than 1 second off.","check":"On every server, `timedatectl show -p NTPSynchronized --value` prints `yes` and `timedatectl show -p Timezone --value` prints `Etc/UTC` or `UTC`. `chronyc tracking` shows a system time offset under 1 second.","url":"https://agentwranglers.se/playbook/infrastructure/servers/time-sync","related":["/playbook/operations/logs/structured-logs"],"sources":[{"label":"RFC 5905 Network Time Protocol Version 4","url":"https://www.rfc-editor.org/rfc/rfc5905"},{"label":"Ubuntu Server guide to serving NTP with Chrony","url":"https://ubuntu.com/server/docs/how-to/networking/serve-ntp-with-chrony/"}]},{"chapter":"Servers","name":"Remove services the server does not need","level":"Recommended","rule":"Install only the packages and services the server's job needs. Remove the rest, and disable what cannot be removed.","check":"The script below prints nothing on any server. It compares the listening ports with the expected list kept in code.","url":"https://agentwranglers.se/playbook/infrastructure/servers/minimal-services","related":["/playbook/infrastructure/servers/host-firewall","/playbook/operations/containers/multi-stage-builds"],"sources":[{"label":"NIST SP 800-123 Guide to General Server Security","url":"https://csrc.nist.gov/pubs/sp/800/123/final"}]},{"chapter":"Servers","name":"Check server images against a hardening checklist","level":"Recommended","rule":"Scan every server image against a published hardening checklist, such as a CIS Benchmark or a DISA STIG profile. Fail the image build on any failed check without a written exception.","check":"Every image build runs the scan and keeps its report. The report shows 0 failed checks outside the exception list. Turn on SSH password logins in a test image, and the build fails.","url":"https://agentwranglers.se/playbook/infrastructure/servers/hardening-checklist","related":["/playbook/infrastructure/policy-as-code/expiring-policy-exceptions"],"sources":[{"label":"NIST SP 800-70 Rev. 5 National Checklist Program for IT Products","url":"https://csrc.nist.gov/pubs/sp/800/70/r5/final"},{"label":"Ubuntu Security Guide CIS compliance","url":"https://ubuntu.com/security/certifications/docs/usg/cis/compliance"},{"label":"Ubuntu Security Guide CIS audit","url":"https://ubuntu.com/security/certifications/docs/usg/cis/audit"}]},{"chapter":"Cost and ownership","name":"Tag every resource with its owner","level":"Recommended","rule":"Tag every resource with its owner, service, environment and the tool that manages it. Refuse any plan that creates a resource without them.","check":"A resource search finds no taggable resource missing any of the `owner`, `service`, `environment` and `managed-by` tags. The cost report groups at least 95 percent of last month's spend by `service`.","url":"https://agentwranglers.se/playbook/infrastructure/cost-and-ownership/required-tags","related":["/playbook/operations/capacity-and-cost/cost-monitoring","/playbook/infrastructure/policy-as-code/policy-on-the-plan"],"sources":[]},{"chapter":"Cost and ownership","name":"Give every account a budget with alerts","level":"Required","risk":"money","rule":"Create a monthly budget in code for every account. Alert the owning team when the forecast passes 80 percent and when spending passes 100 percent.","check":"Every account in the organization has a budget with both alerts. A test alert reaches the team channel.","url":"https://agentwranglers.se/playbook/infrastructure/cost-and-ownership/budgets-for-every-account","related":["/playbook/operations/capacity-and-cost/cost-monitoring","/playbook/infrastructure/environments/account-baseline"],"sources":[]},{"chapter":"Cost and ownership","name":"Delete resources that sit idle","level":"Recommended","rule":"Run a weekly job that lists idle resources and deletes them after their owner has had 7 days to object.","check":"The weekly report lists no resource that was already on last week's report and had no objection.","url":"https://agentwranglers.se/playbook/infrastructure/cost-and-ownership/delete-idle-resources","related":["/playbook/adversarial-testing/supply-chain-and-setup/dangling-dns"],"sources":[]},{"chapter":"Cost and ownership","name":"Size servers from what they use","level":"Recommended","rule":"Every quarter, compare each server's size with its measured use over 14 days. Step down one size when its 95th percentile CPU stays under 40 percent and memory under 50 percent.","check":"The quarterly report lists every server with its 14-day 95th percentile CPU and memory. None is below both limits without a written reason.","url":"https://agentwranglers.se/playbook/infrastructure/cost-and-ownership/rightsize-from-measurements","related":["/playbook/operations/metrics-and-traces/resource-metrics","/playbook/operations/capacity-and-cost/headroom"],"sources":[]},{"chapter":"Cost and ownership","name":"Show the cost of each change in review","level":"Optional","rule":"Show the monthly cost change of every infrastructure pull request in the review. Ask the budget owner to approve any change above a set amount.","check":"A pull request that adds a large database shows its monthly price in a comment. It cannot merge without the budget owner's approval.","url":"https://agentwranglers.se/playbook/infrastructure/cost-and-ownership/cost-in-review","related":["/playbook/operations/capacity-and-cost/cost-monitoring"],"sources":[]},{"chapter":"Cost and ownership","name":"Buy commitment discounts for steady use","level":"Optional","rule":"After a service has run 90 days, cover its steady base with a commitment discount. Review the cover every month and buy in small steps.","check":"The last commitment review is under 1 month old. The total hourly commitment is below the lowest hourly compute spend of the last 90 days.","url":"https://agentwranglers.se/playbook/infrastructure/cost-and-ownership/commitment-discounts","related":["/playbook/operations/capacity-and-cost/cost-monitoring"],"sources":[{"label":"AWS Well-Architected COST07 Select the best pricing model","url":"https://docs.aws.amazon.com/wellarchitected/latest/cost-optimization-pillar/select-the-best-pricing-model.html"}]}]}