As part of my infrastructure hardening and automation efforts, I built a CIS-hardened Debian 12 AMI using Packer and the ansible-lockdown/DEBIAN12-CIS Ansible role.
For reference or reuse, Iβve made the AMI publicly available:
AMI ID:
ami-0ded45c1c47569084
Region:us-east-1
This post documents how I did it from scratch.
π οΈ Tools Used
- Packer
- Ansible
- Ansible Lockdown Role
- AWS EC2
- Debian 12 (Official AMI)
π Project Structure
cis-debian-ami/
βββ packer.pkr.hcl
βββ ansible/
β βββ playbook.yml
β βββ roles/
β βββ DEBIAN12-CIS/ # added via git submodule
To add the Ansible role:
git submodule add https://github.com/ansible-lockdown/DEBIAN12-CIS.git ansible/roles/DEBIAN12-CIS
π§± Packer Template
packer.pkr.hcl
:
packer {
required_plugins {
amazon = {
version = ">= 1.0.0"
source = "github.com/hashicorp/amazon"
}
ansible = {
version = "~> 1"
source = "github.com/hashicorp/ansible"
}
}
}
variable "region" {
type = string
default = "us-east-1"
}
source "amazon-ebs" "debian" {
ami_name = "cis-hardened-debian12-amd64-{{timestamp}}"
instance_type = "t3.micro"
region = var.region
ssh_username = "admin"
source_ami_filter {
filters = {
name = "debian-12-amd64-*"
root-device-type = "ebs"
virtualization-type = "hvm"
}
owners = ["136693071363"]
most_recent = true
}
}
build {
name = "cis-debian12-ami"
sources = ["source.amazon-ebs.debian"]
provisioner "ansible" {
playbook_file = "ansible/playbook.yml"
}
}
π§© Ansible Playbook
ansible/playbook.yml
:
- name: Harden Debian 12 with CIS Benchmark
hosts: all
become: yes
vars:
setup_audit: true
run_audit: true
audit_run_heavy_tests: true
deb12cis_rule_5_2_4: false # Skip: user 'admin' is locked (uses SSH keys)
deb12cis_rule_5_4_1_1: false # Skip: password expiration policy
deb12cis_rule_5_4_2_4: false # Skip: root password must be set (root is disabled)
deb12cis_sshd:
allow_users: "admin"
allow_groups: "admin"
client_alive_interval: 900
deb12cis_ufw_allow_out_ports:
- 53
- 80
- 443
roles:
- DEBIAN12-CIS
π IAM Policy
The Packer build requires extensive EC2 permissions. I created a dedicated IAM policy (PackerEC2BuildPolicy
) with these actions:
{
"Effect": "Allow",
"Action": [
"ec2:Describe*",
"ec2:RunInstances",
"ec2:StopInstances",
"ec2:StartInstances",
"ec2:TerminateInstances",
"ec2:CreateTags",
"ec2:CreateImage",
"ec2:DeregisterImage",
"ec2:CreateSnapshot",
"ec2:DeleteVolume",
"ec2:CreateVolume",
"ec2:AttachVolume",
"ec2:Modify*",
"ec2:CreateKeyPair",
"ec2:DeleteKeyPair",
"ec2:ImportKeyPair",
"ec2:CreateSecurityGroup",
"ec2:DeleteSecurityGroup",
"ec2:AuthorizeSecurityGroupIngress",
"ec2:RevokeSecurityGroupIngress"
],
"Resource": "*"
}
You can define this in the AWS Console, CLI, or Terraform.
π§ͺ Build the AMI
packer init .
packer build packer.pkr.hcl
After provisioning, Packer automatically deletes temporary security groups, key pairs, and the instance.
π Why Some CIS Rules Were Disabled
To ensure the build works with AWS cloud-init and SSH key authentication, I disabled these rules:
deb12cis_rule_5_2_4
: Requires user account not be locked β not compatible with key-based SSH.deb12cis_rule_5_4_1_1
: Password expiration β unnecessary in AMIs.deb12cis_rule_5_4_2_4
: Requires root to have a password β Debian disables root login by default.
These donβt reduce the hardened posture when SSH is key-only and root login is disabled.
πͺ Result
This build applies the majority of Level 1 CIS controls for Debian 12, with a few carefully disabled for compatibility with cloud-init and SSH key-based access (e.g., rules requiring local passwords). Level 2 controls were not applied.
To launch an EC2 instance from this AMI:
aws ec2 run-instances \
--image-id ami-0ded45c1c47569084 \
--instance-type t3.micro \
--key-name your-keypair-name \
--region us-east-1
Replace your-keypair-name
with an EC2 key pair youβve already created in that region. After the instance starts, you can SSH into it using:
ssh -i your-key.pem admin@<public-ip>