Codifying Trust: Creating and Securing Server OS Images with AWS AMIs


In modern cloud infrastructure—especially in environments that span multiple classification domains—custom Amazon Machine Images (AMIs) become a foundation for secure, reproducible, and compliant server deployments.

Whether you’re building hardened Linux images, enforcing baseline security controls, or deploying across environments with varying security postures (e.g., IL2, IL4, IL6), scripting AMI creation and management ensures consistency and control.


What You’ll Learn

  • How to automate AMI creation using EC2 Image Builder & CLI
  • How to secure OS images with configuration scripts and IAM policies
  • How to promote images across domains (e.g., dev → staging → production)
  • How to integrate classification tagging, lifecycle controls, and encrypted snapshots

1. Automating Custom AMI Creation

Option A: Using aws ec2 create-image

Launch and configure your EC2 instance manually or via script:

aws ec2 run-instances \
  --image-id ami-0abcdef1234567890 \
  --instance-type t3.medium \
  --key-name secure-key \
  --security-group-ids sg-01234 \
  --subnet-id subnet-01234

Once configured (e.g., install agents, update packages), create an AMI:

aws ec2 create-image \
  --instance-id i-0123456789abcdef0 \
  --name "hardened-linux-base-2025-05-09" \
  --no-reboot

✅ Add "--no-reboot" only if you understand the risks—some in-memory config may be lost.


Option B: Using EC2 Image Builder for Codified AMI Pipelines

Define components in YAML and automate builds:

name: HardenedImagePipeline
version: 1.0.0
components:
  - name: baseline-linux-hardening
    description: "Applies STIG rules and disables root SSH"
    phases:
      - name: build
        steps:
          - action: ExecuteBash
            inputs:
              commands:
                - apt-get update && apt-get upgrade -y
                - systemctl disable ssh
                - useradd secureops && mkdir /home/secureops

Register and execute using the AWS CLI:

aws imagebuilder create-image-pipeline \
  --name HardenedPipeline \
  --infrastructure-configuration arn:aws:imagebuilder:... \
  --image-recipe arn:aws:imagebuilder:...

🛠️ You can trigger this pipeline from Git commits, CloudWatch events, or weekly schedules.


2. Hardening Images with Scripts

Inject security via provisioning scripts.

Example: Harden Ubuntu with a bootstrap script

#!/bin/bash
# Disable root login
passwd -l root

# Disable SSH password auth
sed -i 's/^#PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config

# Install audit tools
apt install -y auditd aide fail2ban

# Remove unused packages
apt autoremove -y

Pass this script using --user-data in EC2 or Image Builder.


3. Tagging and Classifying AMIs for Domain Control

Use tags to track image purpose, domain, classification level, and approval state.

aws ec2 create-tags \
  --resources ami-0abc1234567890 \
  --tags Key=Domain,Value=IL4 Key=Classification,Value=SECRET Key=Approved,Value=True

Enforce access using IAM policies:

{
  "Effect": "Deny",
  "Action": "ec2:RunInstances",
  "Resource": "*",
  "Condition": {
    "StringEquals": {
      "ec2:ResourceTag/Classification": "TOP-SECRET"
    }
  }
}

4. Promoting Images Across Classification Tiers

Use AMI copy and encryption to transfer across domains while enforcing controls.

Copy AMI to another region or enclave:

aws ec2 copy-image \
  --source-image-id ami-0abc1234567890 \
  --source-region us-west-2 \
  --region us-gov-west-1 \
  --name "secure-app-prod"

Encrypt with KMS key per domain:

aws ec2 copy-image \
  --source-image-id ami-0abc1234567890 \
  --kms-key-id arn:aws:kms:gov-west-1:1234567890:key/xyz \
  --encrypted

✅ Encrypted AMIs are required for classified or enclave-compliant workloads.


5. Image Cleanup and Lifecycle Automation

Create lifecycle scripts or Lambda functions to:

  • Deregister old AMIs
  • Delete orphaned snapshots
  • Enforce image rotation (e.g., retain last 5)

Python: Clean up AMIs older than 60 days

import boto3
from datetime import datetime, timedelta

ec2 = boto3.client('ec2')
cutoff = datetime.utcnow() - timedelta(days=60)

images = ec2.describe_images(Owners=['self'])['Images']
for image in images:
    created = datetime.strptime(image['CreationDate'], "%Y-%m-%dT%H:%M:%S.%fZ")
    if created < cutoff:
        ec2.deregister_image(ImageId=image['ImageId'])

Bonus: Versioned Infrastructure with Packer

Use HashiCorp Packer to version and script image creation across providers.

{
  "builders": [{
    "type": "amazon-ebs",
    "ami_name": "secure-linux-{{timestamp}}",
    "source_ami": "ami-0abc1234567890",
    "instance_type": "t3.micro",
    "ssh_username": "ubuntu",
    "region": "us-east-1"
  }],
  "provisioners": [{
    "type": "shell",
    "script": "scripts/harden.sh"
  }]
}

Run:

packer build secure-linux.json

Summary Table

TaskToolExample
Create AMIAWS CLIaws ec2 create-image
Automate BuildEC2 Image BuilderYAML pipelines
Harden SystemBash scriptsDisable root, install audit tools
Tag/Trackcreate-tagsClassification labels
Promotecopy-image + encryptionRegion to region/domain
CleanupPython + Boto3Deregister old AMIs

Final Thoughts

Creating secure, reproducible server images is essential for any multi-domain or compliance-sensitive deployment. But doing it by hand isn’t scalable. By embracing scripting and infrastructure-as-code, you ensure your images remain consistent, hardened, and ready for production across environments—without sacrificing control or visibility.