Cloud Provider Configuration

Updated

September 12, 2026

Cloud Provider Configuration

Cloud consoles are mutable. The boring default is: NixOS images from nixos-generators (or official AMIs), cloud-init only for SSH keys at first boot, then the flake owns the instance.

Mental model

Piece Owner
AMI / qcow / gce image nixos-generators on 26.05, or official NixOS AMIs
Instance size, disk, VPC OpenTofu withPlugins
OS config nixosConfigurations in the same repo
First-boot SSH cloud-init / metadata once

Do not bake AWS keys into the image. Do not grow user_data into a second configuration.nix. After first boot, Colmena / nixos-rebuild --flake takes over.

The NixOS firewall and the cloud security group must tell the same story. 0.0.0.0/0 on 22 “for debug” is the story of a scanner.

Worked examples

Case 1: Official AMI lookup (OpenTofu)

Save as ami.tf:

# ami.tf
data "aws_ami" "nixos" {
  owners      = ["427812963091"]
  most_recent = true
  filter {
    name   = "name"
    values = ["nixos/26.05*"]
  }
  filter {
    name   = "architecture"
    values = ["x86_64"]
  }
}

Owner ID is the NixOS AMI account as documented on nixos.org/download. Confirm it when you copy. most_recent plus a name glob is a moving target — pin a specific AMI id once you have tested it:

# ami-pin.tf
variable "nixos_ami" {
  type    = string
  default = "ami-xxxxxxxxxxxxxxxxx"
}

Case 2: Image you built

nix build .#amazon
ls -l result
# upload / AMI import per current AWS docs for the VHD

The generator produces the artifact. This chapter only consumes the AMI id in tofu. Do not hand-edit an AMI from 24.11 after the flake moved to 26.05.

Case 3: user-data is SSH keys, not packages

Save as userdata.tf:

# userdata.tf
resource "aws_instance" "app_01" {
  ami           = var.nixos_ami
  instance_type = "t3.medium"
  user_data     = <<-EOT
    #cloud-config
    users:
      - name: deskadmin
        sudo: ALL=(ALL) NOPASSWD:ALL
        ssh_authorized_keys:
          - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI… deskadmin@ops-01
  EOT
  tags = {
    Name    = "desk-app-01"
    Flake   = "git.desk.internal/platform/desk"
    HostKey = "app-01"
  }
}

After first boot:

ssh deskadmin@203.0.113.11
sudo nixos-rebuild switch --flake git+ssh://git.desk.internal/platform/desk#app-01

Do not keep adding packages to user_data. They vanish on the next rebuild anyway if the flake does not declare them.

Case 4: Security group matches the firewall

Save as sg.tf:

# sg.tf
resource "aws_security_group" "gw" {
  name = "desk-gw"
  ingress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
  ingress {
    from_port   = 51820
    to_port     = 51820
    protocol    = "udp"
    cidr_blocks = ["0.0.0.0/0"]
  }
  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["203.0.113.0/24"] # office VPN, not the world
  }
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

On the host, networking.firewall.allowedTCPPorts = [ 443 ]; and allowedUDPPorts = [ 51820 ];. If the SG is wider than NixOS, the SG is the bug. If NixOS is wider than the SG, the module is the bug.

Case 5: Tags are how you find the box

When DNS is wrong:

aws ec2 describe-instances --filters Name=tag:HostKey,Values=app-01 \
  --query 'Reservations[].Instances[].InstanceId'

Tags are not secrets. Do not put the restic password in a tag.

The trap

The trap is an AMI from 24.11 still in tofu after the flake moved to 26.05. The instance boots old software, then the first rebuild is a surprise kernel. Pin AMI name nixos/26.05* or a specific AMI id you tested — and bump it on purpose.

The boring rule

  • Images: 26.05 generator or official 26.05 AMI, then pin the id.
  • tofu owns cloud objects. Flake owns the OS.
  • user-data: SSH keys only. Then Colmena.
  • SG + NixOS firewall agree. No 0.0.0.0/0 on 22.
  • No cloud keys in the image.

Try this

  1. aws ec2 describe-images --owners 427812963091 --filters Name=name,Values=nixos/26.05* (if you have AWS) and record an AMI id.
  2. Compare that id to what tofu would pick with most_recent.
  3. List ports in the SG vs networking.firewall.allowedTCPPorts for one host — they should match.
  4. Boot from user-data keys, then rebuild from the flake; confirm a package you did not put in user-data is present only because the flake says so.