One-Click Infrastructure Rotation#

The RotationManager performs blue-green deployment rotation of redirector instances. The current (blue) instance keeps serving traffic while a replacement (green) instance is provisioned, configured, health-checked, and verified. If anything fails, the blue instance stays in place and the green is destroyed.

Rotation Steps#

1. Pre-flight checks    DNS propagation, cert validity, upstream health
         |
2. Provision green      IaC apply (Terraform or Pulumi) for the replacement VPS
         |
3. Configure green      SCP config + profile, start docker compose
         |
4. Health gate          Poll /health on the green instance
         |
5. Traffic shift        Verify DNS resolves to green IP
         |
6. Beacon verification  HTTPS request through the full domain path
         |
7. Encrypt state        age-encrypt the Terraform state (skipped for Pulumi)
         |
8. Destroy blue         IaC destroy the old instance

If any step from 3 onward fails, the manager rolls back: DNS stays on blue, the green instance is destroyed, and the RotationResult records rollback_performed: true.

Pre-flight Checks#

Before touching any infrastructure, three checks run:

CheckWhat it verifies
DNSThe domain resolves to A records. Optionally verifies it resolves to the current blue IP. Retries up to 30 times at 10s intervals.
CertificateTLS handshake succeeds and the cert has at least 7 days until expiry.
UpstreamThe C2 teamserver is reachable (any HTTP response, even 404, proves the host is up).

CLI Usage#

infraguard rotate \
  --domain cdn.example.com \
  --upstream https://10.0.0.5:8443 \
  --provider do \
  --engine pulumi \
  --c2-profile malleable.profile \
  --ssh-key ~/.ssh/id_ed25519.pub \
  --strategy blue-green \
  --state-key age1... \
  --operator-ip 1.2.3.4/32

Options#

FlagDescription
--domainDomain to rotate
--upstreamC2 teamserver URL
--providerCloud provider (do, aws, azure, hetzner, cloudflare)
--engineIaC engine: terraform (default) or pulumi
--c2-profilePath to C2 profile file
--ssh-keyOperator SSH public key
--strategyOnly blue-green is currently supported
--state-keyage public key for encrypting Terraform state (ignored with Pulumi)
--state-identityage identity file for decrypting blue state (ignored with Pulumi)
--regionCloud region override
--instance-sizeInstance size override
--skip-preflightSkip DNS/cert/upstream checks (not recommended)
--no-destroy-blueKeep the blue instance after rotation

Provider Support#

ProviderSSH UserNotes
DigitalOceanrootSSH key fingerprint computed from public key
AWSubuntuSSH public key passed as tfvar
AzureoperatorSSH public key passed as tfvar
HetznerrootSSH public key passed as tfvar
Cloudflare Workersn/aNo VPS, no SSH, no health check. Config push only.

Security#

  • Secrets never appear on the IaC CLI. With Terraform, they’re written to a tfvars file with 0o600 permissions. With Pulumi, they’re set as config values.
  • Terraform state is age-encrypted after apply and the plaintext is deleted. Pulumi state uses a local file backend without encryption by default.
  • SSH/SCP uses the operator’s key pair with strict timeout options.

RotationResult#

The rotate() method returns a RotationResult dataclass:

@dataclass
class RotationResult:
    success: bool
    strategy: str
    domain: str
    green_ip: str | None
    blue_ip: str | None
    preflight: PreFlightResult | None
    rollback_performed: bool
    error: str | None
    green_work_dir: Path | None
    elapsed_seconds: float