Skip to main content

EC2 Instance Older Than Specific Days

Overview

This check identifies running EC2 instances that have been active longer than a configurable threshold (default: 180 days). Long-running instances often accumulate security vulnerabilities due to outdated operating systems, unpatched software, and configuration drift from your organization's security baseline.

Risk

If this check fails, you have EC2 instances that have been running for an extended period without being refreshed:

Potential consequences:

  • Unpatched operating systems and software with known vulnerabilities (CVEs)
  • Privilege escalation opportunities for attackers
  • Divergence from current security baselines and hardened AMI configurations
  • Increased risk of malware or crypto-mining infections
  • Complicated incident response due to outdated forensic baselines
  • Higher costs from running instances that may no longer be optimally sized

Severity: Medium

Remediation Steps

Prerequisites

  • Access to the AWS Console with permissions to view and manage EC2 instances, or
  • AWS CLI installed and configured with appropriate credentials
Required IAM permissions

Your IAM user or role needs the following permissions:

{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ec2:DescribeInstances",
"ec2:StopInstances",
"ec2:TerminateInstances",
"ec2:RunInstances",
"ec2:CreateImage"
],
"Resource": "*"
}
]
}

For production, scope the Resource to specific instance ARNs.

AWS Console Method

You have several options depending on your situation:

Option A: Replace with a fresh instance (recommended)

This is the most secure approach - launch a new instance from an updated AMI:

  1. Open the EC2 Console
  2. Find the old instance flagged in the Prowler finding
  3. Note the instance configuration (instance type, security groups, IAM role, storage)
  4. Click Launch instances in the top right
  5. Select an updated AMI (ideally your organization's hardened, patched image)
  6. Configure the new instance to match the old one
  7. Complete the launch wizard
  8. Migrate your workload to the new instance
  9. Once validated, terminate the old instance (see Option C)

Option B: Stop the instance (if it's no longer needed running)

Stopping the instance will make it pass the check (stopped instances are ignored):

  1. Open the EC2 Console
  2. Find and select the instance flagged in the Prowler finding
  3. Click Instance state > Stop instance
  4. Click Stop to confirm

Warning: Stopping an instance does not address the underlying security risk. It only passes the check. If you start the instance again, it will still have outdated software.

Option C: Terminate the instance (if no longer needed)

  1. Open the EC2 Console
  2. Find and select the instance flagged in the Prowler finding
  3. Click Instance state > Terminate instance
  4. Click Terminate to confirm

Warning: Termination is permanent. Any data on instance store volumes will be lost. EBS volumes may be deleted depending on the "Delete on Termination" setting.

Option D: Patch and reboot the existing instance

If replacing the instance is not feasible, update the software in place:

  1. Connect to the instance via SSH or Session Manager
  2. Update all packages:
    • Amazon Linux/RHEL: sudo yum update -y && sudo reboot
    • Ubuntu/Debian: sudo apt update && sudo apt upgrade -y && sudo reboot
    • Windows: Run Windows Update and restart
  3. After rebooting, create a new AMI as a fresh baseline

Note: This approach resets the security posture but does not reset the launch time. The check will still flag the instance. Consider this a short-term mitigation while planning instance replacement.

AWS CLI method

Find instances older than 180 days:

# List all running instances with their launch times
aws ec2 describe-instances \
--filters "Name=instance-state-name,Values=running" \
--query 'Reservations[*].Instances[*].{ID:InstanceId,Name:Tags[?Key==`Name`].Value|[0],LaunchTime:LaunchTime,Type:InstanceType}' \
--output table \
--region us-east-1

Stop a specific instance:

aws ec2 stop-instances \
--instance-ids i-1234567890abcdef0 \
--region us-east-1

Terminate a specific instance:

aws ec2 terminate-instances \
--instance-ids i-1234567890abcdef0 \
--region us-east-1

Script to find all instances older than N days:

#!/bin/bash
REGION="us-east-1"
DAYS_THRESHOLD=180
CUTOFF_DATE=$(date -d "-${DAYS_THRESHOLD} days" +%Y-%m-%dT%H:%M:%S 2>/dev/null || date -v-${DAYS_THRESHOLD}d +%Y-%m-%dT%H:%M:%S)

echo "Finding EC2 instances launched before: $CUTOFF_DATE"
echo "---"

aws ec2 describe-instances \
--filters "Name=instance-state-name,Values=running" \
--query "Reservations[*].Instances[?LaunchTime<='${CUTOFF_DATE}'].{ID:InstanceId,Name:Tags[?Key==\`Name\`].Value|[0],LaunchTime:LaunchTime}" \
--output table \
--region "$REGION"
CloudFormation

CloudFormation cannot directly address instance age, but you can implement lifecycle management practices:

Use Auto Scaling Groups with instance refresh:

Auto Scaling Groups allow you to automatically replace instances on a schedule, ensuring they are always running from fresh, updated AMIs.

AWSTemplateFormatVersion: '2010-09-09'
Description: EC2 Auto Scaling Group with scheduled instance refresh

Parameters:
LatestAmiId:
Type: AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>
Default: /aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64
Description: Latest Amazon Linux 2023 AMI from SSM Parameter Store

VpcId:
Type: AWS::EC2::VPC::Id
Description: VPC to deploy into

SubnetIds:
Type: List<AWS::EC2::Subnet::Id>
Description: Subnets for the Auto Scaling Group

Resources:
LaunchTemplate:
Type: AWS::EC2::LaunchTemplate
Properties:
LaunchTemplateName: !Sub ${AWS::StackName}-template
LaunchTemplateData:
ImageId: !Ref LatestAmiId
InstanceType: t3.micro
TagSpecifications:
- ResourceType: instance
Tags:
- Key: Name
Value: !Sub ${AWS::StackName}-instance
- Key: ManagedBy
Value: CloudFormation

AutoScalingGroup:
Type: AWS::AutoScaling::AutoScalingGroup
Properties:
AutoScalingGroupName: !Sub ${AWS::StackName}-asg
LaunchTemplate:
LaunchTemplateId: !Ref LaunchTemplate
Version: !GetAtt LaunchTemplate.LatestVersionNumber
MinSize: '1'
MaxSize: '3'
DesiredCapacity: '1'
VPCZoneIdentifier: !Ref SubnetIds
HealthCheckType: EC2
HealthCheckGracePeriod: 300
Tags:
- Key: Name
Value: !Sub ${AWS::StackName}-asg
PropagateAtLaunch: false

# Scheduled action to refresh instances monthly
InstanceRefreshSchedule:
Type: AWS::AutoScaling::ScheduledAction
Properties:
AutoScalingGroupName: !Ref AutoScalingGroup
Recurrence: "0 2 1 * *" # 2 AM on the 1st of each month
MinSize: 1
MaxSize: 3
DesiredCapacity: 1

Outputs:
AutoScalingGroupName:
Value: !Ref AutoScalingGroup
Description: Name of the Auto Scaling Group

To trigger an instance refresh manually:

After updating the AMI parameter, use the AWS CLI to start an instance refresh:

aws autoscaling start-instance-refresh \
--auto-scaling-group-name <your-asg-name> \
--preferences MinHealthyPercentage=50 \
--region us-east-1
Terraform

Use Terraform with Auto Scaling Groups and instance refresh to manage instance lifecycle:

variable "vpc_id" {
description = "VPC ID to deploy into"
type = string
}

variable "subnet_ids" {
description = "Subnet IDs for the Auto Scaling Group"
type = list(string)
}

variable "instance_type" {
description = "EC2 instance type"
type = string
default = "t3.micro"
}

# Get latest Amazon Linux 2023 AMI
data "aws_ami" "amazon_linux" {
most_recent = true
owners = ["amazon"]

filter {
name = "name"
values = ["al2023-ami-*-kernel-*-x86_64"]
}

filter {
name = "virtualization-type"
values = ["hvm"]
}
}

resource "aws_launch_template" "main" {
name_prefix = "lifecycle-managed-"
image_id = data.aws_ami.amazon_linux.id
instance_type = var.instance_type

tag_specifications {
resource_type = "instance"
tags = {
Name = "lifecycle-managed-instance"
ManagedBy = "Terraform"
}
}

lifecycle {
create_before_destroy = true
}
}

resource "aws_autoscaling_group" "main" {
name = "lifecycle-managed-asg"
desired_capacity = 1
max_size = 3
min_size = 1
vpc_zone_identifier = var.subnet_ids

launch_template {
id = aws_launch_template.main.id
version = "$Latest"
}

# Enable instance refresh on launch template changes
instance_refresh {
strategy = "Rolling"
preferences {
min_healthy_percentage = 50
instance_warmup = 300
}
triggers = ["launch_template"]
}

tag {
key = "Name"
value = "lifecycle-managed-asg"
propagate_at_launch = false
}

lifecycle {
create_before_destroy = true
}
}

# Optional: CloudWatch Event to trigger periodic refresh
resource "aws_cloudwatch_event_rule" "monthly_refresh" {
name = "monthly-instance-refresh"
description = "Trigger instance refresh monthly"
schedule_expression = "cron(0 2 1 * ? *)" # 2 AM on 1st of each month
}

resource "aws_cloudwatch_event_target" "refresh_target" {
rule = aws_cloudwatch_event_rule.monthly_refresh.name
target_id = "StartInstanceRefresh"
arn = "arn:aws:ssm:us-east-1::automation-definition/AWS-StartAutomationExecution"
role_arn = aws_iam_role.eventbridge_role.arn

input = jsonencode({
AutoScalingGroupName = [aws_autoscaling_group.main.name]
})
}

resource "aws_iam_role" "eventbridge_role" {
name = "eventbridge-asg-refresh-role"

assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Principal = {
Service = "events.amazonaws.com"
}
Action = "sts:AssumeRole"
}
]
})
}

resource "aws_iam_role_policy" "eventbridge_policy" {
name = "eventbridge-asg-refresh-policy"
role = aws_iam_role.eventbridge_role.id

policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"autoscaling:StartInstanceRefresh"
]
Resource = aws_autoscaling_group.main.arn
}
]
})
}

output "asg_name" {
value = aws_autoscaling_group.main.name
description = "Name of the Auto Scaling Group"
}

To manually trigger instance refresh:

terraform apply  # If AMI has changed, this triggers refresh automatically

Or use AWS CLI:

aws autoscaling start-instance-refresh \
--auto-scaling-group-name lifecycle-managed-asg \
--region us-east-1

Verification

After remediation, verify the fix was successful:

  1. If you replaced the instance: In the EC2 console, confirm the new instance shows a recent launch time
  2. If you stopped the instance: Confirm the instance state shows "Stopped"
  3. If you terminated the instance: Confirm the instance no longer appears (or shows "Terminated")
  4. Re-run the Prowler check to confirm:
    prowler aws --check ec2_instance_older_than_specific_days -r us-east-1
CLI verification

Check instance launch time:

aws ec2 describe-instances \
--instance-ids i-1234567890abcdef0 \
--query 'Reservations[*].Instances[*].{ID:InstanceId,State:State.Name,LaunchTime:LaunchTime}' \
--output table \
--region us-east-1

List all running instances with their ages:

aws ec2 describe-instances \
--filters "Name=instance-state-name,Values=running" \
--query 'Reservations[*].Instances[*].{ID:InstanceId,Name:Tags[?Key==`Name`].Value|[0],LaunchTime:LaunchTime}' \
--output table \
--region us-east-1

Additional Resources

Notes

  • Stopping is not the same as fixing: While stopping an instance will make it pass this check, the underlying security issue (outdated software) remains. If you start the instance again, it will still be vulnerable.

  • Prefer immutable infrastructure: The best long-term solution is to treat instances as disposable. Use Auto Scaling Groups with instance refresh, or container orchestration (ECS, EKS) to regularly replace workloads with fresh, patched images.

  • Use golden AMIs: Maintain hardened, regularly-updated AMI images (golden AMIs) that include all security patches. AWS EC2 Image Builder can automate this process.

  • Implement patch management: For instances that cannot be replaced frequently, use AWS Systems Manager Patch Manager to keep them updated automatically.

  • Check before terminating: Before terminating any instance, verify:

    • All important data is backed up or stored on persistent EBS volumes
    • Any Elastic IPs are documented (they will be released)
    • DNS records pointing to the instance are updated
    • Associated resources (load balancers, target groups) are updated
  • Configurable threshold: The default threshold is 180 days, but this can be configured in Prowler using the max_ec2_instance_age_in_days parameter. Adjust based on your organization's security requirements.

  • Consider tagging for exceptions: If certain long-running instances are intentional (e.g., database servers with complex state), document the exception and consider implementing compensating controls like enhanced monitoring and aggressive patching.