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 instanceIf 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:
| Check | What it verifies |
|---|---|
| DNS | The domain resolves to A records. Optionally verifies it resolves to the current blue IP. Retries up to 30 times at 10s intervals. |
| Certificate | TLS handshake succeeds and the cert has at least 7 days until expiry. |
| Upstream | The 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/32Options#
| Flag | Description |
|---|---|
--domain | Domain to rotate |
--upstream | C2 teamserver URL |
--provider | Cloud provider (do, aws, azure, hetzner, cloudflare) |
--engine | IaC engine: terraform (default) or pulumi |
--c2-profile | Path to C2 profile file |
--ssh-key | Operator SSH public key |
--strategy | Only blue-green is currently supported |
--state-key | age public key for encrypting Terraform state (ignored with Pulumi) |
--state-identity | age identity file for decrypting blue state (ignored with Pulumi) |
--region | Cloud region override |
--instance-size | Instance size override |
--skip-preflight | Skip DNS/cert/upstream checks (not recommended) |
--no-destroy-blue | Keep the blue instance after rotation |
Provider Support#
| Provider | SSH User | Notes |
|---|---|---|
| DigitalOcean | root | SSH key fingerprint computed from public key |
| AWS | ubuntu | SSH public key passed as tfvar |
| Azure | operator | SSH public key passed as tfvar |
| Hetzner | root | SSH public key passed as tfvar |
| Cloudflare Workers | n/a | No 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
0o600permissions. 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