When you provision a fleet of Amazon EC2 Ubuntu 22.04 or 24.04 LTS instances inside an AWS VPC, the AWS Shared Responsibility Model draws a strict line: AWS secures the underlying physical data centers and hypervisor, while you are responsible for securing the guest operating system.

Out of the box, default cloud Linux images prioritize convenience over zero-trust security:

  • OpenSSH often permits weak authentication defaults or generous retry limits (MaxAuthTries).
  • Kernel network parameters (sysctl) may allow IP spoofing, ICMP redirects, or unhardened packet forwarding.
  • Host-level firewalls (UFW) sit inactive, relying purely on external security groups without defense-in-depth at the OS layer.
  • Worse, when engineers manually SSH into an EC2 instance to debug an issue, configuration drift silently weakens your security posture across the fleet.

In my previous articles, I covered eliminating container CVEs in a shift-left FastAPI DevSecOps pipeline and cutting Amazon EKS & VPC infrastructure costs by 38.1% with Terraform and an automated Python FinOps gate.

In this third guide (ansible-linux-cis-hardening), I built an automated, idempotent Ansible CIS Ubuntu Level 1 Hardening Role paired with a zero-dependency Python Compliance & Drift Scanner inside a 4-stage GitHub Actions pipeline to lock down AWS EC2 Ubuntu fleets automatically.

Architecture: How Fleet Hardening & CI/CD Drift Verification Work

In a real AWS production environment, EC2 instances scale dynamically and change private IP addresses over time. Rather than maintaining static IP lists or manually SSH-ing into hosts, this platform combines AWS EC2 Tag-Based Targeting with a 4-Gate CI/CD Verification Pipeline:

┌────────────────────────────────────────────────────────────────────────────────────┐ │ GitHub Actions 4-Gate CI/CD Pipeline │ │ │ │ [Gate 1: Lint] ──► [Gate 2: Dry-Run] ──► [Gate 3: Idempotency] ──► [Gate 4: Audit] │ │ ansible-lint Apply CIS Role Re-run Playbook: Python 100% │ │ & YAML Syntax on Ubuntu Host Verify changed=0 Compliance │ │ Container Audit │ └────────────────────────────────────────────┬───────────────────────────────────────┘ │ Verified Playbook & Role ▼ ┌────────────────────────────────────────────────────────────────────────────────────┐ │ AWS Cloud — EC2 Ubuntu Fleet │ │ │ │ ┌──────────────────────┐ ┌──────────────────────┐ ┌──────────────────────┐ │ │ │ EC2 Web Fleet │ │ EC2 Worker Nodes │ │ EC2 Bastion/Ops │ │ │ │ Tag: Role=web-prod │ │ Tag: Role=eks-node │ │ Tag: Env=prod │ │ │ ├──────────────────────┤ ├──────────────────────┤ ├──────────────────────┤ │ │ │ • SSH Key-Only Auth │ │ • Kernel sysctl Lock │ │ • UFW Default │ │ │ │ • Root Login Blocked │ │ • ASLR & SYN Cookies │ │ Deny Incoming │ │ │ │ • /etc/shadow 0600 │ │ • IP Forward = 0 │ │ • Audit Scanner │ │ │ └──────────────────────┘ └──────────────────────┘ └──────────────────────┘ │ └────────────────────────────────────────────────────────────────────────────────────┘ 

Layer 1: Enforcing 18+ CIS Level 1 Controls with an Idempotent Ansible Role

The core automation lives inside roles/cis_ubuntu_hardening/, broken into modular, idempotent task files covering four critical security domains across Ubuntu 22.04 and 24.04 EC2 instances:

1. OpenSSH Daemon Hardening (tasks/ssh.yml)

Brute-force credential stuffing and root SSH logins are the #1 attack vector against cloud Linux hosts. Our Ansible role enforces strict CIS Level 1 OpenSSH configurations inside /etc/ssh/sshd_config and validates the config syntax (sshd -t -f %s) before restarting the service:

- name: "CIS 5.2.x | Enforce Hardened OpenSSH Daemon Configuration" ansible.builtin.lineinfile: path: /etc/ssh/sshd_config regexp: "{{ item.regexp }}" line: "{{ item.line }}" state: present validate: "/usr/sbin/sshd -t -f %s" loop: - { regexp: '^#?PermitRootLogin', line: 'PermitRootLogin no' } - { regexp: '^#?PasswordAuthentication', line: 'PasswordAuthentication no' } - { regexp: '^#?PermitEmptyPasswords', line: 'PermitEmptyPasswords no' } - { regexp: '^#?MaxAuthTries', line: 'MaxAuthTries 3' } - { regexp: '^#?ClientAliveInterval', line: 'ClientAliveInterval 300' } - { regexp: '^#?ClientAliveCountMax', line: 'ClientAliveCountMax 2' } - { regexp: '^#?X11Forwarding', line: 'X11Forwarding no' } notify: Restart sshd 

2. Kernel & Network Stack Hardening (tasks/sysctl.yml)

Even inside an AWS VPC with Security Groups, host-level network stacks must be hardened against man-in-the-middle ICMP redirects, source-routed packets, and SYN flood denial-of-service attacks.

We enforce persistent kernel parameters across /etc/sysctl.d/99-cis-hardening.conf:

  • net.ipv4.tcp_syncookies = 1 — Protects the EC2 instance against TCP SYN flood attacks.
  • net.ipv4.ip_forward = 0 — Prevents non-router EC2 instances from forwarding unauthorized packets between interfaces.
  • net.ipv4.conf.all.accept_redirects = 0 & send_redirects = 0 — Blocks malicious ICMP routing table manipulation.
  • net.ipv4.conf.all.rp_filter = 1 — Enforces Reverse Path Filtering to drop spoofed source IPs.
  • kernel.randomize_va_space = 2 — Enforces full Address Space Layout Randomization (ASLR) to mitigate buffer-overflow exploits.

3. Host Firewall Defense-in-Depth (tasks/firewall.yml)

Relying solely on AWS Security Groups means a single overly permissive Terraform rule exposes every port on the instance.

We configure UFW (Uncomplicated Firewall) directly on the EC2 host to enforce a zero-trust baseline:

  • Default Incoming Policy: deny
  • Default Outgoing Policy: allow
  • SSH Rate-Limiting: Explicitly rate-limits 22/tcp (ufw limit 22/tcp) to throttle automated connection bursts.

4. Filesystem & Identity Lockdown (tasks/filesystem.yml)

To prevent local privilege escalation if a container or application process is compromised, the role locks down critical identity files and disables legacy kernel filesystem modules:

  • Enforces 0600 (root:root) permissions on /etc/shadow and /etc/gshadow so password hashes can never be read by non-root users.
  • Enforces 0644 (root:root) on /etc/passwd and /etc/group.
  • Disables unused legacy filesystems (cramfs, freevxfs, jffs2, hfs, hfsplus, udf) via /etc/modprobe.d/cis-filesystems.conf and restricts core dumps.

Layer 2: Targeting Dynamic AWS EC2 Fleets at Scale

In a 500-instance AWS environment, EC2 instances are frequently replaced by Auto Scaling Groups, changing their IP addresses.

Rather than hardcoding static IPs in inventory/hosts.ini, Ansible pairs with the AWS EC2 Dynamic Inventory plugin (amazon.aws.aws_ec2) to query the AWS API in real time by resource tags.

Whether we want to roll out CIS hardening across the entire cloud fleet or target a specific subset of 100 production web servers in rolling waves (serial), we can scope execution dynamically:

# Apply CIS Level 1 hardening across ALL tagged EC2 Ubuntu instances ansible-playbook -i inventory/aws_ec2.yml site.yml # Target ONLY the web-server fleet using --limit ansible-playbook -i inventory/aws_ec2.yml site.yml --limit tag_Role_web_prod # Target a canary slice of the first 100 servers (0 to 99) before full rollout ansible-playbook -i inventory/aws_ec2.yml site.yml --limit 'tag_Env_prod' 

Layer 3: Automated Python Compliance & Drift Scanner (cis_compliance_audit.py)

Applying a security playbook once is only half the battle—how do you prove to security auditors (SOC 2, ISO 27001, or internal Cloud SecOps) that zero configuration drift has occurred?

I built a zero-dependency Python 3 audit tool (scripts/cis_compliance_audit.py) that inspects live Linux system state across all 18 CIS Level 1 controls, outputs a structured JSON compliance artifact, and fails the CI/CD pipeline (exit code 1) if compliance drops below 100%:

CIS Control ID Security Check Audited Expected State Severity Status
CIS-5.2.8 SSH PermitRootLogin disabled no CRITICAL PASS
CIS-5.2.12 SSH PasswordAuthentication disabled no CRITICAL PASS
CIS-5.2.7 SSH MaxAuthTries threshold <= 3 HIGH PASS
CIS-3.1.1 IPv4 Packet Forwarding (ip_forward) 0 HIGH PASS
CIS-3.3.8 TCP SYN Cookies (tcp_syncookies) 1 HIGH PASS
CIS-3.3.2 ICMP Redirect Acceptance (accept_redirects) 0 MEDIUM PASS
CIS-1.5.2 Kernel ASLR (randomize_va_space) 2 HIGH PASS
CIS-6.1.2 /etc/passwd permissions & owner 0644 root:root CRITICAL PASS
CIS-6.1.3 /etc/shadow permissions & owner 0600 root:root CRITICAL PASS
CIS-1.1.1 Unused Filesystems (cramfs, udf) install /bin/true MEDIUM PASS

Layer 4: The 4-Gate GitHub Actions Verification Pipeline

Every commit and Pull Request in .github/workflows/cis-hardening-ci.yml must pass four automated verification gates before any playbook change is allowed into main:

Gate 1 — Static Linting & Syntax Check: Runs yamllint, ansible-lint, and ansible-playbook --syntax-check to catch malformed YAML or deprecated Ansible modules.

Gate 2 — Live Ubuntu Container Convergence: Spins up a clean Ubuntu container, executes site.yml to apply all 18+ CIS hardening controls, and verifies zero failed tasks.

Gate 3 — Strict Idempotency Test (changed=0): Runs ansible-playbook site.yml a second consecutive time against the already-hardened host. If any task reports changed > 0, the pipeline fails—guaranteeing the role is 100% idempotent and safe to run on production EC2 cron schedules without disrupting running services.

Gate 4 — 100% CIS Compliance Gate: Executes python3 scripts/cis_compliance_audit.py against the hardened environment and enforces a 100.0% pass rate before unlocking the PR merge button.

Try It Yourself (Open-Source Repository)

You can clone the full Ansible role, Python CIS compliance scanner, and 4-gate GitHub Actions pipeline to test locally in Docker or adapt for your own AWS EC2 Ubuntu fleets:

Part 3 Repository (Ansible Linux CIS Hardening & Drift Scanner): github.com/mohammedarmaan936-sketch/ansible-linux-cis-hardening

Part 2 Repository (AWS EKS, Terraform, FinOps Gate & Argo CD): github.com/mohammedarmaan936-sketch/cloudpulse-eks-gitops-infra

Part 1 Repository (FastAPI Microservice & Hardened Docker CI/CD): github.com/mohammedarmaan936-sketch/cloudpulse-devsecops-service