CloudTrail closed the previous lesson with a concrete and honest limitation: it detects events, not states. It knows somebody called DeleteBucketEncryption this morning. It does not know that mercadofresco-registros-web has gone five months without encryption because it was never set up. It does not know there is a security group with 0.0.0.0/0 on port 22 from a March test that nobody undid. It cannot say how many resources in the account breach MercadoFresco's mandatory tagging. And it certainly cannot fix anything.

This is the fifth and last question module 4 left open: nothing warns you if somebody turns off a bucket's encryption or opens a security group to the world.

AWS Config is the answer. Its idea is different from everything we have seen in this module: instead of recording what happens, it continuously photographs how each resource is configured, keeps the history of those photographs, and evaluates each one against a set of rules. When a configuration drifts from what it should be, Config flags it as non-compliant, warns you, and —if you let it— fixes it by itself.

It is the difference between a security camera that films whoever comes in and an inventory that, every night, checks that all the doors are locked.

Cost warning. AWS Config is, together with CloudWatch log ingestion, the service in this module that most easily runs away with you. It charges for every configuration item recorded and for every rule evaluation. An account with an auto scaling group rotating instances all day can generate tens of thousands of items a month. There is a whole section with the calculation and the five ways to keep it under control.

Compliance warning. This material is educational. The conformance packs mentioned here (CIS, PCI DSS) are aids, not certifications: enabling a conformance pack does not make you compliant with PCI DSS. Any use of Config for real regulatory compliance must be validated by a compliance professional.

Contents

  1. What a resource's configuration is
  2. The configuration item and the timeline
  3. Config versus CloudTrail
  4. The full flow: recorder, rules and remediation
  5. Enabling the configuration recorder
  6. The delivery channel: S3 and SNS
  7. Choosing which resource types are recorded
  8. Rules: the heart of the service
  9. The managed rules MercadoFresco enables
  10. Change trigger versus periodic trigger
  11. Mandatory tagging with required-tags
  12. Custom rules with Lambda
  13. Custom rules with Guard
  14. Automatic remediation with Systems Manager
  15. The two real cases: bucket encryption and an open SG
  16. The warning about testing in manual mode first
  17. Conformance packs
  18. MercadoFresco's compliance dashboard
  19. Advanced queries for taking inventory
  20. Multi-account aggregators
  21. Security Hub and GuardDuty: where each one fits
  22. Real cost and how not to blow it up
  23. Cleanup

What a resource's configuration is

The configuration of a resource is the set of properties that define it at a given moment. Not its data, not its traffic: its settings.

Resource Its configuration includes…
mercadofresco-catalogo-fotos (S3) Encryption, versioning, policy, public access block, lifecycle, tags
sg-mercadofresco-basedatos Inbound and outbound rules, VPC, description, tags
mercadofresco-pedidos (RDS) Class, Multi-AZ, encryption, backup retention, engine version, window
rol-mercadofresco-tienda (IAM) Attached policies, trust policy, permissions boundary
mercadofresco-tienda-01 (EC2) Type, AMI, subnet, SG, instance role, volumes, tags
alb-mercadofresco-tienda Listeners, certificates, scheme, target groups

Config can record the configuration of more than 250 resource types across dozens of services, and keep the full history of their changes.

The configuration item and the timeline

A configuration item (CI) is a complete photograph of a resource at a specific instant. Config creates one every time it detects a change.

{
  "configurationItemVersion": "1.3",
  "configurationItemCaptureTime": "2026-07-28T09:14:33.412Z",
  "configurationItemStatus": "OK",
  "configurationStateId": "1785142473412",
  "awsAccountId": "111122223333",
  "resourceType": "AWS::S3::Bucket",
  "resourceId": "mercadofresco-registros-web",
  "resourceName": "mercadofresco-registros-web",
  "ARN": "arn:aws:s3:::mercadofresco-registros-web",
  "awsRegion": "eu-west-1",
  "resourceCreationTime": "2026-03-02T11:20:01.000Z",

  "tags": {
    "Proyecto": "mercadofresco",
    "Entorno": "produccion",
    "Componente": "registros",
    "Propietario": "marta",
    "CentroCoste": "plataforma"
  },

  "relationships": [
    { "resourceType": "AWS::KMS::Key",
      "resourceId": "8f2c1a9b-4d3e-4f6a-9c1b-2e5d7a8f3c04",
      "relationshipName": "Is encrypted with" }
  ],

  "configuration": {
    "name": "mercadofresco-registros-web",
    "bucketVersioningConfiguration": { "status": "Enabled" },
    "publicAccessBlockConfiguration": {
      "blockPublicAcls": true, "ignorePublicAcls": true,
      "blockPublicPolicy": true, "restrictPublicBuckets": true
    },
    "serverSideEncryptionConfiguration": {
      "rules": [{
        "applyServerSideEncryptionByDefault": {
          "sseAlgorithm": "aws:kms",
          "kmsMasterKeyID": "arn:aws:kms:eu-west-1:111122223333:alias/mercadofresco-datos"
        }
      }]
    }
  },

  "supplementaryConfiguration": {
    "BucketPolicy": { "policyText": "{...}" },
    "IsRequesterPaysEnabled": false
  }
}

Three fields deserve special attention:

  • relationships: Config does not store isolated resources, it stores the graph. It knows this bucket is encrypted with that KMS key, that that instance sits in that subnet, that that SG is attached to that ALB. When you want to know what breaks if you delete a key, this is what answers.
  • supplementaryConfiguration: properties that do not come in the main Describe call and that Config collects separately. The bucket policy, for example.
  • configurationItemStatus: OK, ResourceDiscovered, ResourceDeleted. Yes: Config also records the deletion, so you can see how a resource that no longer exists was configured.

The configuration timeline is the sequence of every CI of a resource. It is the most powerful view in the Config console and it answers the question operations asks every single week: "this worked on Friday; what has changed?"

timeline
    title Timeline of sg-mercadofresco-basedatos
    2026-03-02 : Created with rule 5432 from sg-mercadofresco-tienda : COMPLIANT
    2026-05-14 : Rule 22 added from 0.0.0.0-0 : NON COMPLIANT
    2026-05-14 : Automatic remediation revokes the rule : COMPLIANT
    2026-07-09 : Rule 5432 added from sg-mercadofresco-admin : COMPLIANT

And the query:

aws configservice get-resource-config-history \
  --resource-type AWS::EC2::SecurityGroup \
  --resource-id sg-0a1b2c3d4e5f6a7b8 \
  --limit 10 \
  --query 'configurationItems[].[configurationItemCaptureTime,configurationItemStatus]' \
  --output table \
  --profile mercadofresco-dev --region eu-west-1

Config versus CloudTrail

They are complementary and they get confused constantly. The table:

CloudTrail (05-03) AWS Config
Records The API call The resource state
Question Who did what, when, from where? How is it configured now? Is it compliant?
Unit Event Configuration item
Sees the previous state No Yes: before and after
Detects existing drift No Yes
Evaluates against rules No Yes
Fixes No Yes (remediation)
Sees changes made outside the API No Yes, in the periodic evaluation
Cost Almost zero Per CI and per evaluation
First copy free Yes No

The example that nails it. Somebody adds an open SSH rule to sg-mercadofresco-basedatos:

  • CloudTrail: an AuthorizeSecurityGroupIngress event, with the identity, the time and the IP of whoever did it. It does not say how the security group ended up, only what was requested.
  • Config: a new CI with the complete configuration of the group after the change, the comparison with the previous CI (the diff), the restricted-ssh rule evaluation flagging it as NON_COMPLIANT, and —if it is configured— the remediation that revokes the rule.

They are used together, and in fact Config depends on CloudTrail: it uses its events to know that something has changed and to trigger the recording of a new CI.

The full flow: recorder, rules and remediation

flowchart TD
    R["AWS resource<br/>bucket, SG, RDS, role..."] -->|"change"| CT["CloudTrail<br/>detects the call"]
    CT --> G["CONFIGURATION RECORDER<br/>creates a configuration item"]
    G --> H["History in S3<br/>mercadofresco-config-historial"]
    G --> E["RULES engine"]
    E -->|"COMPLIANT"| OK["Nothing to do"]
    E -->|"NON_COMPLIANT"| N["Flagged as non-compliant"]
    N --> SNS["SNS notification<br/>alertas-mercadofresco"]
    N --> EB["EventBridge event<br/>07-03"]
    N --> REM["REMEDIATION<br/>SSM Automation document"]
    REM -->|"fixes"| R
    G --> SH["Security Hub<br/>findings aggregation"]

Five pieces and their order:

  1. Configuration recorder: the process that creates the CIs. There can only be one per account and region.
  2. Delivery channel: where the CIs are deposited (an S3 bucket) and where changes are notified (an SNS topic). Also one per account and region.
  3. Rules: the conditions that get evaluated.
  4. Remediation: the corrective action, run by Systems Manager Automation.
  5. Aggregator: the consolidated view across several accounts and regions (09-04).

Enabling the configuration recorder

Step 1: the history bucket.

aws s3api create-bucket \
  --bucket mercadofresco-config-historial \
  --region eu-west-1 \
  --create-bucket-configuration LocationConstraint=eu-west-1 \
  --profile mercadofresco-dev

aws s3api put-public-access-block \
  --bucket mercadofresco-config-historial \
  --public-access-block-configuration \
    "BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true" \
  --profile mercadofresco-dev

aws s3api put-bucket-encryption \
  --bucket mercadofresco-config-historial \
  --server-side-encryption-configuration '{
    "Rules":[{"ApplyServerSideEncryptionByDefault":{"SSEAlgorithm":"AES256"},
              "BucketKeyEnabled":true}]}' \
  --profile mercadofresco-dev

With its policy:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ConfigComprobarAcl",
      "Effect": "Allow",
      "Principal": { "Service": "config.amazonaws.com" },
      "Action": ["s3:GetBucketAcl", "s3:ListBucket"],
      "Resource": "arn:aws:s3:::mercadofresco-config-historial",
      "Condition": {
        "StringEquals": { "AWS:SourceAccount": "111122223333" }
      }
    },
    {
      "Sid": "ConfigEscribir",
      "Effect": "Allow",
      "Principal": { "Service": "config.amazonaws.com" },
      "Action": "s3:PutObject",
      "Resource": "arn:aws:s3:::mercadofresco-config-historial/AWSLogs/111122223333/Config/*",
      "Condition": {
        "StringEquals": {
          "s3:x-amz-acl": "bucket-owner-full-control",
          "AWS:SourceAccount": "111122223333"
        }
      }
    }
  ]
}

Step 2: the service role. Config needs permission to read the configuration of everything:

aws iam create-role \
  --role-name rol-aws-config-mercadofresco \
  --assume-role-policy-document '{
    "Version": "2012-10-17",
    "Statement": [{
      "Effect": "Allow",
      "Principal": { "Service": "config.amazonaws.com" },
      "Action": "sts:AssumeRole",
      "Condition": {
        "StringEquals": { "AWS:SourceAccount": "111122223333" }
      }
    }]
  }' \
  --profile mercadofresco-dev

aws iam attach-role-policy \
  --role-name rol-aws-config-mercadofresco \
  --policy-arn arn:aws:iam::aws:policy/service-role/AWS_ConfigRole \
  --profile mercadofresco-dev

The AWS_ConfigRole managed policy grants read-only access across dozens of services. Config does not need to write anything: remediation is run by Systems Manager with another, separate role, which is where the dangerous permissions will live. That separation is deliberate and good.

Step 3: the recorder.

aws configservice put-configuration-recorder \
  --configuration-recorder '{
    "name": "grabador-mercadofresco",
    "roleARN": "arn:aws:iam::111122223333:role/rol-aws-config-mercadofresco",
    "recordingGroup": {
      "allSupported": false,
      "includeGlobalResourceTypes": false,
      "resourceTypes": [
        "AWS::S3::Bucket",
        "AWS::EC2::SecurityGroup",
        "AWS::EC2::Instance",
        "AWS::EC2::Volume",
        "AWS::EC2::NetworkAcl",
        "AWS::EC2::VPC",
        "AWS::EC2::Subnet",
        "AWS::RDS::DBInstance",
        "AWS::RDS::DBSnapshot",
        "AWS::Lambda::Function",
        "AWS::IAM::Role",
        "AWS::IAM::Policy",
        "AWS::IAM::User",
        "AWS::KMS::Key",
        "AWS::SecretsManager::Secret",
        "AWS::ElasticLoadBalancingV2::LoadBalancer",
        "AWS::CloudFront::Distribution",
        "AWS::CloudTrail::Trail",
        "AWS::Logs::LogGroup",
        "AWS::WAFv2::WebACL"
      ]
    },
    "recordingMode": {
      "recordingFrequency": "CONTINUOUS",
      "recordingModeOverrides": [
        {
          "description": "ASG instances rotate a lot: daily is enough",
          "resourceTypes": ["AWS::EC2::Instance", "AWS::EC2::Volume"],
          "recordingFrequency": "DAILY"
        }
      ]
    }
  }' \
  --profile mercadofresco-dev --region eu-west-1

Two decisions that are worth real money here:

  • allSupported: false with an explicit list. Recording the 250+ supported types in a live account generates an enormous volume of CIs, and many of them add nothing.
  • recordingModeOverrides with DAILY for instances and volumes. This is the most important optimisation in the whole lesson. The asg-mercadofresco-tienda ASG launches and terminates instances at every Friday peak; each launch generates CIs for the instance, its volumes and its network interfaces. In continuous mode that is thousands of CIs a month for an ephemeral resource whose configuration is always the same —it comes from lt-mercadofresco-tienda—. In daily mode, one.

Step 4: the delivery channel.

aws configservice put-delivery-channel \
  --delivery-channel '{
    "name": "canal-mercadofresco",
    "s3BucketName": "mercadofresco-config-historial",
    "s3KeyPrefix": "config",
    "snsTopicARN": "arn:aws:sns:eu-west-1:111122223333:alertas-mercadofresco",
    "configSnapshotDeliveryProperties": {
      "deliveryFrequency": "TwentyFour_Hours"
    }
  }' \
  --profile mercadofresco-dev --region eu-west-1

Step 5: start it. Just as with CloudTrail, creating is not starting:

aws configservice start-configuration-recorder \
  --configuration-recorder-name grabador-mercadofresco \
  --profile mercadofresco-dev --region eu-west-1

# Check
aws configservice describe-configuration-recorder-status \
  --query 'ConfigurationRecordersStatus[].[name,recording,lastStatus]' \
  --output table \
  --profile mercadofresco-dev --region eu-west-1

And a warning about the SNS topic: if you connect all of Config's notifications to alertas-mercadofresco, you will get a message for every configuration change in the account. With the ASG rotating, that is pure noise and it will make people stop reading the alerts. The right way is to send only the non-compliance notifications over SNS, and that is done with EventBridge (07-03) filtering by event type, or with a separate topic for the noise. MercadoFresco uses the topic here only because the channel demands it, and filters in EventBridge what reaches people.

Choosing which resource types are recorded

MercadoFresco's decision table, which is the pattern to copy:

Resource type Record? Frequency Reason
AWS::S3::Bucket Yes Continuous Encryption, public access: critical
AWS::EC2::SecurityGroup Yes Continuous The front door
AWS::RDS::DBInstance Yes Continuous Encryption, Multi-AZ, backups
AWS::IAM::Role / Policy / User Yes Continuous It is global: in one region only
AWS::KMS::Key Yes Continuous Rotation, policy
AWS::CloudTrail::Trail Yes Continuous So that nobody turns it off
AWS::Logs::LogGroup Yes Continuous Retention (the 05-01 problem)
AWS::EC2::Instance Yes Daily They rotate constantly
AWS::EC2::Volume Yes Daily Same
AWS::EC2::NetworkInterface No Pure ASG noise
AWS::SSM::ManagedInstanceInventory No Huge volume, low value
AWS::Config::ResourceCompliance No Config recording Config

The last three rows are the ones that save the most money in real accounts. NetworkInterface and ManagedInstanceInventory are, by a distance, the two types that generate the most CIs by accident.

Global resources: IAM and CloudFront are global. If you enable includeGlobalResourceTypes in several regions, you will pay for the same CI several times. It is enabled in a single region —MercadoFresco uses eu-west-1— and the IAM types are recorded explicitly as we did above.

Rules: the heart of the service

A Config rule evaluates resources and assigns them a state:

State Means
COMPLIANT Compliant
NON_COMPLIANT Not compliant
NOT_APPLICABLE The rule does not apply to that resource
INSUFFICIENT_DATA It could not be evaluated

There are three classes of rule:

Class Who writes it Evaluation cost
AWS managed AWS. There are more than 300 0.001 USD
Custom with Lambda You, in code 0.001 USD + Lambda cost
Custom with Guard You, in a declarative language 0.001 USD

Always start with the managed ones: they cover 90 % of what you need and need no maintenance.

The managed rules MercadoFresco enables

Rule What it checks Why MercadoFresco needs it Trigger
s3-bucket-server-side-encryption-enabled Default encryption on buckets The module 4 question Change
s3-bucket-public-read-prohibited That it cannot be read publicly mercadofresco-copias-basedatos being public would be catastrophic Change
s3-bucket-public-write-prohibited That it cannot be written to Same Change
s3-bucket-versioning-enabled Versioning enabled Recovery from deletions Change
s3-bucket-ssl-requests-only Policy that requires TLS Data in transit Change
rds-storage-encrypted RDS encryption mercadofresco-pedidos with alias/mercadofresco-datos (04-02) Change
rds-instance-public-access-check RDS not reachable from the internet It must sit in the data subnets (03-01) Change
rds-multi-az-support Multi-AZ enabled We built it in 02-04; nobody should remove it Change
db-instance-backup-enabled Automatic backups enabled PITR from 02-04 Change
restricted-ssh Port 22 not open to 0.0.0.0/0 The other module 4 question Change
restricted-common-ports 3389, 5432, 3306, 6379… closed So that nobody exposes PostgreSQL Change
vpc-default-security-group-closed The default SG with no rules The classic oversight from 03-02 Change
vpc-flow-logs-enabled Flow logs enabled on the VPC flowlogs-mercadofresco (03-01) Periodic
iam-user-mfa-enabled MFA on every IAM user 04-01 Periodic (24 h)
iam-root-access-key-check Root with no access keys 04-01 Periodic (24 h)
iam-password-policy Strong password policy 04-01 Periodic
access-keys-rotated Keys rotated every 90 days Exercise 2 of 05-03 Periodic
iam-policy-no-statements-with-admin-access No "Action": "*" Least privilege (04-01) Change
cloudtrail-enabled An active trail exists trail-mercadofresco (05-03) Periodic
cloud-trail-log-file-validation-enabled Integrity validation 05-03 Periodic
cloud-trail-encryption-enabled Trail encrypted with KMS 05-03 Periodic
cw-loggroup-retention-period-check Retention configured The 610 USD mistake from 05-01 Change
encrypted-volumes EBS volumes encrypted 04-02 Change
ec2-imdsv2-check IMDSv2 mandatory on EC2 Protects the instance role Change
elb-tls-https-listeners-only ALB with HTTPS only 03-03 Change
kms-cmk-not-scheduled-for-deletion No key marked for deletion The most destructive event (05-03) Periodic
required-tags Mandatory tagging MercadoFresco's policy Change
lambda-function-public-access-prohibited Lambdas not invocable by just anyone 02-05 Change
secretsmanager-rotation-enabled-check Rotation enabled on secrets 04-03 Periodic

That is 29 rules. Enabling them:

# The one that answers the module 4 question
aws configservice put-config-rule --config-rule '{
  "ConfigRuleName": "mercadofresco-s3-cifrado",
  "Description": "All buckets must have default encryption",
  "Source": {
    "Owner": "AWS",
    "SourceIdentifier": "S3_BUCKET_SERVER_SIDE_ENCRYPTION_ENABLED"
  },
  "Scope": { "ComplianceResourceTypes": ["AWS::S3::Bucket"] }
}' --profile mercadofresco-dev --region eu-west-1

# The other one: SSH open to the world
aws configservice put-config-rule --config-rule '{
  "ConfigRuleName": "mercadofresco-ssh-restringido",
  "Description": "No SG with port 22 open to 0.0.0.0/0",
  "Source": { "Owner": "AWS", "SourceIdentifier": "INCOMING_SSH_DISABLED" },
  "Scope": { "ComplianceResourceTypes": ["AWS::EC2::SecurityGroup"] }
}' --profile mercadofresco-dev --region eu-west-1

# Common ports, with parameters
aws configservice put-config-rule --config-rule '{
  "ConfigRuleName": "mercadofresco-puertos-restringidos",
  "Source": { "Owner": "AWS", "SourceIdentifier": "RESTRICTED_INCOMING_TRAFFIC" },
  "Scope": { "ComplianceResourceTypes": ["AWS::EC2::SecurityGroup"] },
  "InputParameters": "{\"blockedPort1\":\"22\",\"blockedPort2\":\"3389\",\"blockedPort3\":\"5432\",\"blockedPort4\":\"3306\",\"blockedPort5\":\"6379\"}"
}' --profile mercadofresco-dev --region eu-west-1

# Log group retention: the 05-01 mistake
aws configservice put-config-rule --config-rule '{
  "ConfigRuleName": "mercadofresco-retencion-registros",
  "Description": "Every log group must have retention configured",
  "Source": { "Owner": "AWS", "SourceIdentifier": "CW_LOGGROUP_RETENTION_PERIOD_CHECK" },
  "InputParameters": "{\"MinRetentionTime\":\"7\"}"
}' --profile mercadofresco-dev --region eu-west-1

And the query for the overall state:

aws configservice describe-compliance-by-config-rule \
  --query 'ComplianceByConfigRules[?Compliance.ComplianceType==`NON_COMPLIANT`].[ConfigRuleName,Compliance.ComplianceContributorCount.CappedCount]' \
  --output table \
  --profile mercadofresco-dev --region eu-west-1

# And the detail of a specific rule
aws configservice get-compliance-details-by-config-rule \
  --config-rule-name mercadofresco-s3-cifrado \
  --compliance-types NON_COMPLIANT \
  --query 'EvaluationResults[].EvaluationResultIdentifier.EvaluationResultQualifier.ResourceId' \
  --output table \
  --profile mercadofresco-dev --region eu-west-1

Change trigger versus periodic trigger

On configuration change Periodic
When it evaluates On detecting a new CI Every 1, 3, 6, 12 or 24 hours
Detection latency Minutes Up to 24 hours
Cost One evaluation per change One evaluation per period
What for Specific resources Account-level checks
Example restricted-ssh iam-user-mfa-enabled, cloudtrail-enabled

The distinction is not arbitrary. A change-triggered rule needs a resource to attach itself to. Rules such as cloudtrail-enabled or iam-password-policy evaluate a property of the account, not of a specific resource, so they can only be periodic.

And there is an important cost nuance: a change-triggered rule on a resource type that changes a lot —EC2 instances with the ASG rotating— gets evaluated an enormous number of times. A periodic rule every 24 hours is evaluated 30 times a month and that is that. When a check does not need immediate detection, make it periodic.

They can be combined:

{
  "ConfigRuleName": "mercadofresco-volumenes-cifrados",
  "Source": {
    "Owner": "AWS",
    "SourceIdentifier": "ENCRYPTED_VOLUMES",
    "SourceDetails": [
      { "EventSource": "aws.config",
        "MessageType": "ConfigurationItemChangeNotification" },
      { "EventSource": "aws.config",
        "MessageType": "ScheduledNotification",
        "MaximumExecutionFrequency": "TwentyFour_Hours" }
    ]
  }
}

It evaluates on change and once a day, which is the safety net in case some CI was missed.

Mandatory tagging with required-tags

MercadoFresco's tagging is Proyecto, Entorno, Componente, Propietario, CentroCoste. Until now it was a written rule that nobody checked. Config turns it into a rule:

aws configservice put-config-rule --config-rule '{
  "ConfigRuleName": "mercadofresco-etiquetado-obligatorio",
  "Description": "Resources must carry the five MercadoFresco tags",
  "Source": { "Owner": "AWS", "SourceIdentifier": "REQUIRED_TAGS" },
  "InputParameters": "{\"tag1Key\":\"Proyecto\",\"tag1Value\":\"mercadofresco\",\"tag2Key\":\"Entorno\",\"tag2Value\":\"produccion,preproduccion,desarrollo\",\"tag3Key\":\"Componente\",\"tag4Key\":\"Propietario\",\"tag5Key\":\"CentroCoste\"}",
  "Scope": {
    "ComplianceResourceTypes": [
      "AWS::EC2::Instance",
      "AWS::EC2::Volume",
      "AWS::S3::Bucket",
      "AWS::RDS::DBInstance",
      "AWS::Lambda::Function",
      "AWS::ElasticLoadBalancingV2::LoadBalancer"
    ]
  },
  "MaximumExecutionFrequency": "TwentyFour_Hours"
}' --profile mercadofresco-dev --region eu-west-1

Syntax details that are not obvious:

  • tagNKey without tagNValue requires the tag to exist, with any value. That is how Componente, Propietario and CentroCoste are set: their values are free.
  • tagNValue with a comma-separated list requires the value to be one of those. Entorno can only be produccion, preproduccion or desarrollo.
  • A maximum of 6 tags per rule. If you need more, two rules.

And the result the first time it runs, which is the most instructive moment:

Type Total Compliant Non-compliant What is missing
S3 buckets 6 4 2 mercadofresco-registros-web without CentroCoste
EC2 instances 2 2 0
EBS volumes 4 1 3 The ones created by the ASG do not inherit tags
Lambdas 2 2 0
RDS 2 2 0
ALB 1 1 0

The volumes row is the real finding, and it is a problem almost everybody has without knowing it: EBS volumes created automatically by an ASG do not inherit the instance's tags unless it is configured explicitly in the launch template:

{
  "TagSpecifications": [
    {
      "ResourceType": "instance",
      "Tags": [
        { "Key": "Proyecto", "Value": "mercadofresco" },
        { "Key": "Entorno", "Value": "produccion" },
        { "Key": "Componente", "Value": "tienda" },
        { "Key": "Propietario", "Value": "marta" },
        { "Key": "CentroCoste", "Value": "tienda-online" }
      ]
    },
    {
      "ResourceType": "volume",
      "Tags": [
        { "Key": "Proyecto", "Value": "mercadofresco" },
        { "Key": "Entorno", "Value": "produccion" },
        { "Key": "Componente", "Value": "tienda" },
        { "Key": "Propietario", "Value": "marta" },
        { "Key": "CentroCoste", "Value": "tienda-online" }
      ]
    }
  ]
}

Direct and measurable consequence: in module 11, when Sara splits the cost by CentroCoste, the shop's volumes would show up as "unassigned". A Config rule has found a hole in the cost accounting that nobody had spotted in months. It is the best argument there is for enabling required-tags from day one.

Custom rules with Lambda

When no managed rule fits. MercadoFresco's case: no business-data bucket may have a lifecycle rule that deletes objects before 30 days, because Sara needs to be able to recompute the month's reports.

"""Custom Config rule: safe lifecycle on business buckets.

The event the function receives contains the complete configuration
item of the resource being evaluated.
"""
import json
import boto3

config = boto3.client("config")
s3 = boto3.client("s3")

BUSINESS_BUCKETS = (
    "mercadofresco-informes-analitica",
    "mercadofresco-copias-basedatos",
)
MINIMUM_DAYS = 30


def evaluate(item):
    """Returns (status, reason) for a configuration item."""
    name = item["resourceName"]

    if not name.startswith(BUSINESS_BUCKETS):
        return "NOT_APPLICABLE", "Not a business data bucket"

    try:
        lifecycle = s3.get_bucket_lifecycle_configuration(Bucket=name)
    except s3.exceptions.ClientError as e:
        if "NoSuchLifecycleConfiguration" in str(e):
            return "COMPLIANT", "No lifecycle rules: nothing is deleted"
        raise

    for rule in lifecycle.get("Rules", []):
        if rule.get("Status") != "Enabled":
            continue
        expiration = rule.get("Expiration", {})
        days = expiration.get("Days")
        if days is not None and days < MINIMUM_DAYS:
            return ("NON_COMPLIANT",
                    f"Rule '{rule.get('ID')}' expires objects after {days} days "
                    f"(minimum required: {MINIMUM_DAYS})")

    return "COMPLIANT", f"No rule expires before {MINIMUM_DAYS} days"


def handler(event, context):
    invoking = json.loads(event["invokingEvent"])
    item = invoking["configurationItem"]
    token = event["resultToken"]

    # A deleted resource is not evaluated.
    if item["configurationItemStatus"] in ("ResourceDeleted",
                                           "ResourceDeletedNotRecorded"):
        status, reason = "NOT_APPLICABLE", "Resource deleted"
    else:
        status, reason = evaluate(item)

    config.put_evaluations(
        Evaluations=[{
            "ComplianceResourceType": item["resourceType"],
            "ComplianceResourceId": item["resourceId"],
            "ComplianceType": status,
            "Annotation": reason[:256],          # 256-character limit
            "OrderingTimestamp": item["configurationItemCaptureTime"],
        }],
        ResultToken=token,
    )
    return {"status": status, "reason": reason}

Four things you have to get right and that get forgotten:

  • Return NOT_APPLICABLE for what does not apply, not COMPLIANT. If you return COMPLIANT for every bucket in the world, the compliance dashboard lies: it will look as if 40 buckets comply with a rule that applies to only two.
  • Handle ResourceDeleted. Evaluating a resource that no longer exists leaves orphan records flagged as non-compliant forever.
  • Annotation is the text shown in the console next to the non-compliant resource. A good message there saves half an hour of investigation. Maximum 256 characters.
  • ResultToken must be returned exactly as received. It is what ties the answer to the evaluation.

And registering the rule:

aws configservice put-config-rule --config-rule '{
  "ConfigRuleName": "mercadofresco-ciclo-vida-seguro",
  "Description": "Business buckets cannot expire objects before 30 days",
  "Source": {
    "Owner": "CUSTOM_LAMBDA",
    "SourceIdentifier": "arn:aws:lambda:eu-west-1:111122223333:function:mercadofresco-regla-ciclo-vida",
    "SourceDetails": [{
      "EventSource": "aws.config",
      "MessageType": "ConfigurationItemChangeNotification"
    }]
  },
  "Scope": { "ComplianceResourceTypes": ["AWS::S3::Bucket"] }
}' --profile mercadofresco-dev --region eu-west-1

Cost: 0.001 USD per evaluation plus the cost of the Lambda invocation. With a handful of buckets it is negligible; with a custom rule over EC2 instances in an account with a lot of movement, it can come as a surprise.

Custom rules with Guard

AWS CloudFormation Guard is a declarative rule language, much shorter than a Lambda when it comes to checks over the configuration itself. Config rules written in Guard are called Custom Policy rules and they require no function whatsoever: there is no code to maintain, no role to create, and no Lambda cost.

The same idea as before, in Guard, checking that the business buckets are encrypted with MercadoFresco's own key and not with the AWS one:

# Rule: business buckets must use alias/mercadofresco-datos
rule business_buckets_with_own_key when
    resourceType == "AWS::S3::Bucket"
{
    let name = resourceName

    when %name == /^mercadofresco-(informes-analitica|copias-basedatos)/ {
        configuration.serverSideEncryptionConfiguration.rules[*]
            .applyServerSideEncryptionByDefault.sseAlgorithm == "aws:kms"
            <<
              Business data buckets must be encrypted with KMS,
              not with S3-managed AES256.
            >>

        configuration.serverSideEncryptionConfiguration.rules[*]
            .applyServerSideEncryptionByDefault.kmsMasterKeyID
            == /mercadofresco-datos/
            <<
              The alias/mercadofresco-datos key must be used (04-02).
            >>
    }
}

# Rule: no log group without retention
rule log_groups_with_retention when
    resourceType == "AWS::Logs::LogGroup"
{
    configuration.retentionInDays EXISTS
        << Every log group must have retention set (05-01). >>
    configuration.retentionInDays <= 400
        << Retention longer than 400 days must be justified. >>
}

The block between << and >> is the error message, which appears as an annotation in the console.

aws configservice put-config-rule --config-rule '{
  "ConfigRuleName": "mercadofresco-cifrado-buckets-negocio",
  "Source": {
    "Owner": "CUSTOM_POLICY",
    "SourceDetails": [{
      "EventSource": "aws.config",
      "MessageType": "ConfigurationItemChangeNotification"
    }],
    "CustomPolicyDetails": {
      "PolicyRuntime": "guard-2.x.x",
      "PolicyText": "<contents of the .guard file>",
      "EnableDebugLogDelivery": true
    }
  },
  "Scope": { "ComplianceResourceTypes": ["AWS::S3::Bucket"] }
}' --profile mercadofresco-dev --region eu-west-1
Lambda Guard
You write Python/Node code Declarative rules
Can call other APIs Yes No
Extra cost Lambda invocations None
Maintenance Runtime, dependencies, role None
Complexity supported Any Whatever is in the CI
Recommendation Only if you need external logic By default

The practical difference: Guard can only look at what is in the configuration item. If your check needs to call another API —like the Lambda example, which queried get_bucket_lifecycle_configuration—, you need Lambda. For everything else, Guard.

Automatic remediation with Systems Manager

Detecting is good. Fixing by itself is better, and it is what turns Config into an operational tool rather than one more report nobody reads.

Remediation runs through a Systems Manager Automation document: a predefined procedure with steps and parameters. AWS provides dozens of ready-to-use AWS-* documents.

The remediation role. This is where the dangerous permissions live, and that is why it is a role separate from Config's, with the strict minimum:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "CifrarBuckets",
      "Effect": "Allow",
      "Action": [
        "s3:PutEncryptionConfiguration",
        "s3:GetEncryptionConfiguration"
      ],
      "Resource": "arn:aws:s3:::mercadofresco-*"
    },
    {
      "Sid": "CerrarGruposDeSeguridad",
      "Effect": "Allow",
      "Action": [
        "ec2:RevokeSecurityGroupIngress",
        "ec2:DescribeSecurityGroups"
      ],
      "Resource": "*",
      "Condition": {
        "StringEquals": { "aws:ResourceTag/Proyecto": "mercadofresco" }
      }
    },
    {
      "Sid": "PonerRetencionEnRegistros",
      "Effect": "Allow",
      "Action": ["logs:PutRetentionPolicy", "logs:DescribeLogGroups"],
      "Resource": "arn:aws:logs:eu-west-1:111122223333:log-group:*"
    }
  ]
}

Note three least-privilege decisions, all straight out of the 04-01 playbook:

  • s3:PutEncryptionConfiguration only over mercadofresco-*. The role cannot touch other people's buckets.
  • A tag condition on the SG actions: it only acts on resources belonging to the project.
  • There is no ec2:AuthorizeSecurityGroupIngress. The role can remove rules, never add them. If somebody compromised this role, the worst it could do is close things down. That asymmetry is deliberate and it is the kind of detail that separates a safe setup from a dangerous one.

The two real cases: bucket encryption and an open SG

Case 1: re-enabling a bucket's encryption.

aws configservice put-remediation-configurations \
  --remediation-configurations '[{
    "ConfigRuleName": "mercadofresco-s3-cifrado",
    "TargetType": "SSM_DOCUMENT",
    "TargetId": "AWS-EnableS3BucketEncryption",
    "TargetVersion": "1",
    "Automatic": true,
    "MaximumAutomaticAttempts": 3,
    "RetryAttemptSeconds": 300,
    "Parameters": {
      "AutomationAssumeRole": {
        "StaticValue": {
          "Values": ["arn:aws:iam::111122223333:role/rol-remediacion-mercadofresco"]
        }
      },
      "BucketName": {
        "ResourceValue": { "Value": "RESOURCE_ID" }
      },
      "SSEAlgorithm": {
        "StaticValue": { "Values": ["AES256"] }
      }
    }
  }]' \
  --profile mercadofresco-dev --region eu-west-1

The ResourceValue: RESOURCE_ID is the key piece: it tells the remediation that the bucket name is that of the resource that failed the evaluation. Without it, remediation would not know what to act on.

Full timeline of what happens now if somebody disables encryption:

Moment What happens
T+0 s Somebody runs DeleteBucketEncryption
T+2 s CloudTrail records the event (05-03)
T+3 min Config creates a new CI of the bucket
T+3 min The mercadofresco-s3-cifrado rule evaluates: NON_COMPLIANT
T+3 min An EventBridge event is emitted and a notification is sent to SNS
T+4 min The remediation runs AWS-EnableS3BucketEncryption
T+5 min Encryption is restored
T+8 min New evaluation: COMPLIANT
T+8 min Marta gets the notice with the whole cycle in the log

Eight minutes, with no human intervention. And with the full trail: CloudTrail knows who turned it off, Config knows how it ended up, the remediation fixed it, and the timeline tells the whole story. That is the answer to module 4's last question.

Case 2: revoking a security group rule open to the world.

aws configservice put-remediation-configurations \
  --remediation-configurations '[{
    "ConfigRuleName": "mercadofresco-ssh-restringido",
    "TargetType": "SSM_DOCUMENT",
    "TargetId": "AWSConfigRemediation-RemoveUnrestrictedSourceIngressRules",
    "TargetVersion": "1",
    "Automatic": true,
    "MaximumAutomaticAttempts": 5,
    "RetryAttemptSeconds": 60,
    "Parameters": {
      "AutomationAssumeRole": {
        "StaticValue": {
          "Values": ["arn:aws:iam::111122223333:role/rol-remediacion-mercadofresco"]
        }
      },
      "GroupId": { "ResourceValue": { "Value": "RESOURCE_ID" } }
    }
  }]' \
  --profile mercadofresco-dev --region eu-west-1

This document revokes all the inbound rules with source 0.0.0.0/0 or ::/0 in the group, not just the one on 22. It is aggressive, and you need to know it: if your public ALB sits in a security group that legitimately allows 0.0.0.0/0 on 443, this remediation would take it down.

That is why MercadoFresco narrows the scope of the rule, not of the remediation:

{
  "ConfigRuleName": "mercadofresco-ssh-restringido",
  "Source": { "Owner": "AWS", "SourceIdentifier": "INCOMING_SSH_DISABLED" },
  "Scope": {
    "ComplianceResourceTypes": ["AWS::EC2::SecurityGroup"],
    "TagKey": "Componente",
    "TagValue": "basedatos"
  }
}

With TagKey/TagValue, the rule —and therefore the remediation— applies only to the security groups tagged Componente=basedatos. sg-mercadofresco-alb, which must be open to the world on 443, is left out. It is the same minimum-scope reasoning we applied to the WAF exclusions in 04-05.

Case 3, the most profitable: setting retention on the log groups.

aws configservice put-remediation-configurations \
  --remediation-configurations '[{
    "ConfigRuleName": "mercadofresco-retencion-registros",
    "TargetType": "SSM_DOCUMENT",
    "TargetId": "AWSConfigRemediation-SetCloudWatchLogGroupRetention",
    "Automatic": true,
    "MaximumAutomaticAttempts": 3,
    "RetryAttemptSeconds": 300,
    "Parameters": {
      "AutomationAssumeRole": { "StaticValue": { "Values": [
        "arn:aws:iam::111122223333:role/rol-remediacion-mercadofresco"] } },
      "LogGroupName": { "ResourceValue": { "Value": "RESOURCE_ID" } },
      "RetentionInDays": { "StaticValue": { "Values": ["30"] } }
    }
  }]' \
  --profile mercadofresco-dev --region eu-west-1

This is the one that would have prevented the 610 USD incident from exercise 3 of 05-01. Any new log group —created by a Lambda, by the agent, by a service— automatically receives 30 days of retention within a matter of minutes. It is the remediation with the best cost-benefit ratio in the whole module.

The warning about testing in manual mode first

Never enable Automatic: true on a remediation you have not tested. A remediation is code with permissions that modifies your production without anyone approving it. If it is wrong, it does so very fast and everywhere.

MercadoFresco's procedure, and it is non-negotiable:

Phase 1 — Detection only, 1 week. The rule, with no remediation. You watch which resources it flags. This is where the false positives surface: resources that break the letter of the rule for good reasons.

Phase 2 — Manual remediation, 2 weeks. Automatic: false. The remediation exists but you have to launch it by hand from the console, resource by resource:

aws configservice start-remediation-execution \
  --config-rule-name mercadofresco-s3-cifrado \
  --resource-keys resourceType=AWS::S3::Bucket,resourceId=mercadofresco-registros-web \
  --profile mercadofresco-dev --region eu-west-1

You run it on a low-criticality resource, check that it does exactly what is expected and nothing else, and review the execution log in Systems Manager.

Phase 3 — Automatic, with narrowed scope. Automatic: true, but first only over the resources tagged Entorno=desarrollo. One week. Then production.

Phase 4 — With limits. MaximumAutomaticAttempts and RetryAttemptSeconds are an important brake: if the remediation fails 3 times, Config stops trying instead of going into a loop. A remediation loop can generate thousands of API calls and a considerable bill.

And three kinds of remediation MercadoFresco never automates:

Remediation Why not
Deleting non-compliant resources Irreversible. A badly written rule deletes production
Stopping EC2 instances It cuts the service. The rule may be wrong
Modifying IAM policies It can leave everyone without access, the remediation included

The mental rule: automate what adds security and is reversible (enabling encryption, setting retention, removing public access). Never automate what takes away availability or is irreversible.

Conformance packs

A conformance pack is a template that deploys a set of rules and remediations in one go, as a single unit, and that can be applied across a whole organisation.

AWS publishes templates aligned with well-known frameworks:

Pack Approx. rules What for
Operational Best Practices for CIS AWS Foundations Benchmark v1.4 ~50 The most common starting standard
Operational Best Practices for PCI DSS 3.2.1 ~90 Merchants that process cards
Operational Best Practices for NIST 800-53 ~200 US public sector
Operational Best Practices for GDPR ~60 Help with the GDPR
Operational Best Practices for Amazon S3 ~15 S3 only
Security Best Practices ~120 AWS generalist
# paquete-mercadofresco.yaml
Parameters:
  MinimumLogRetention:
    Type: String
    Default: '7'

Resources:
  S3EncryptionEnabled:
    Type: AWS::Config::ConfigRule
    Properties:
      ConfigRuleName: paq-mercadofresco-s3-cifrado
      Source:
        Owner: AWS
        SourceIdentifier: S3_BUCKET_SERVER_SIDE_ENCRYPTION_ENABLED
      Scope:
        ComplianceResourceTypes: [ 'AWS::S3::Bucket' ]

  S3NoPublicRead:
    Type: AWS::Config::ConfigRule
    Properties:
      ConfigRuleName: paq-mercadofresco-s3-sin-lectura-publica
      Source:
        Owner: AWS
        SourceIdentifier: S3_BUCKET_PUBLIC_READ_PROHIBITED
      Scope:
        ComplianceResourceTypes: [ 'AWS::S3::Bucket' ]

  RdsEncrypted:
    Type: AWS::Config::ConfigRule
    Properties:
      ConfigRuleName: paq-mercadofresco-rds-cifrado
      Source:
        Owner: AWS
        SourceIdentifier: RDS_STORAGE_ENCRYPTED

  LogRetention:
    Type: AWS::Config::ConfigRule
    Properties:
      ConfigRuleName: paq-mercadofresco-retencion-registros
      InputParameters:
        MinRetentionTime: !Ref MinimumLogRetention
      Source:
        Owner: AWS
        SourceIdentifier: CW_LOGGROUP_RETENTION_PERIOD_CHECK
aws configservice put-conformance-pack \
  --conformance-pack-name paquete-mercadofresco-base \
  --template-body file://paquete-mercadofresco.yaml \
  --delivery-s3-bucket mercadofresco-config-historial \
  --delivery-s3-key-prefix conformance \
  --profile mercadofresco-dev --region eu-west-1

aws configservice describe-conformance-pack-compliance \
  --conformance-pack-name paquete-mercadofresco-base \
  --profile mercadofresco-dev --region eu-west-1

The warning worth repeating. Deploying the PCI DSS pack does not make you compliant with PCI DSS. It is an aid: it automatically checks a subset of technical controls. Real compliance includes processes, training, contracts, assessments and a formal audit by a qualified assessor. Presenting a green Config dashboard as proof of compliance is a serious mistake. These decisions are validated by a compliance professional, not by a course.

Cost: a pack costs nothing in itself; what costs money is the rules it contains. A PCI DSS pack with 90 rules evaluating hundreds of resources is the fastest way to multiply your Config bill by ten. Read it before deploying it.

MercadoFresco's compliance dashboard

The first result, three days after enabling everything. This is the genuinely valuable moment, because it brings to light what had been sitting there for months:

Rule State Non-compliant resources Comment
mercadofresco-s3-cifrado NON-COMPLIANT 1 mercadofresco-registros-web unencrypted since March
mercadofresco-ssh-restringido NON-COMPLIANT 1 A test SG with 22 open since May
mercadofresco-etiquetado-obligatorio NON-COMPLIANT 5 3 EBS volumes + 2 buckets
mercadofresco-retencion-registros NON-COMPLIANT 4 4 groups set to "never expire"
mercadofresco-puertos-restringidos COMPLIANT 0 The 03-02 work holds up
mercadofresco-rds-cifrado COMPLIANT 0 04-02
mercadofresco-rds-multi-az COMPLIANT 0 02-04
mercadofresco-iam-mfa NON-COMPLIANT 1 A service user without MFA
mercadofresco-cloudtrail-activo COMPLIANT 0 05-03
mercadofresco-claves-rotadas NON-COMPLIANT 1 A 412-day-old key
mercadofresco-vpc-flow-logs COMPLIANT 0 03-01
mercadofresco-sg-defecto-cerrado NON-COMPLIANT 1 The VPC default SG, with rules

Six non-compliant rules, 14 resources. And not one of those fourteen had ever fired a CloudWatch alarm or turned up in a CloudTrail investigation, because none of them was an event: they were states. They had been sitting there for months.

The three findings Marta was not expecting:

  1. mercadofresco-registros-web unencrypted since March. It was created by hand in 03-03 for the ALB logs, before the 04-02 encryption policy existed, and nobody went back to it.
  2. The SG with 22 open since a test in May. The rule was added "just for a moment" to debug something and stayed for three months.
  3. The default security group of vpc-mercadofresco with rules. It is the 03-02 classic: nobody uses it, but there it is, permissive, waiting for somebody to launch an instance without specifying a group.

The remediation plan, ordered by risk:

Priority Resource Action How
1 SG with 22 open Revoke the rule Automatic remediation
2 Default SG Empty its rules Manual, with review
3 mercadofresco-registros-web Enable encryption Automatic remediation
4 4 log groups Set 30-day retention Automatic remediation
5 412-day-old key Rotate it and disable the old one Manual, coordinated
6 User without MFA Replace with a role (04-01) Manual, design change
7 Volume tags Fix lt-mercadofresco-tienda Manual, template change

Notice the pattern: the first four are automated because they add security and are reversible. The last three call for human judgement, and that is why they are not automated.

Advanced queries for taking inventory

Config includes a SQL query engine over the current inventory of every recorded resource. It is one of its most underrated features.

# All buckets without encryption
aws configservice select-resource-config \
  --expression "
    SELECT resourceId, resourceName, awsRegion
    WHERE resourceType = 'AWS::S3::Bucket'
      AND supplementaryConfiguration.ServerSideEncryptionConfiguration NOT LIKE '%aws:kms%'
  " \
  --profile mercadofresco-dev --region eu-west-1

# EC2 instances by type, for the module 11 cost analysis
aws configservice select-resource-config \
  --expression "
    SELECT configuration.instanceType, COUNT(*)
    WHERE resourceType = 'AWS::EC2::Instance'
    GROUP BY configuration.instanceType
  " \
  --profile mercadofresco-dev --region eu-west-1

# Resources WITHOUT the CentroCoste tag: the hole in the accounting
aws configservice select-resource-config \
  --expression "
    SELECT resourceType, resourceId, resourceName, tags
    WHERE tags.key != 'CentroCoste'
  " \
  --profile mercadofresco-dev --region eu-west-1

# Unencrypted EBS volumes
aws configservice select-resource-config \
  --expression "
    SELECT resourceId, configuration.size, configuration.volumeType
    WHERE resourceType = 'AWS::EC2::Volume'
      AND configuration.encrypted = false
  " \
  --profile mercadofresco-dev --region eu-west-1

# Security groups with rules open to the world
aws configservice select-resource-config \
  --expression "
    SELECT resourceId, resourceName, configuration.ipPermissions
    WHERE resourceType = 'AWS::EC2::SecurityGroup'
      AND configuration.ipPermissions.ipRanges = '0.0.0.0/0'
  " \
  --profile mercadofresco-dev --region eu-west-1

This engine answers in seconds questions that would otherwise require walking through dozens of paginated Describe calls. And unlike Athena over CloudTrail (05-03), it costs nothing per query: it is part of the service.

A query Marta runs every month and that has already paid for the cost of Config several times over:

# EBS volumes not attached to any instance: money down the drain
aws configservice select-resource-config \
  --expression "
    SELECT resourceId, configuration.size, configuration.createTime, tags
    WHERE resourceType = 'AWS::EC2::Volume'
      AND configuration.state.value = 'available'
  " \
  --profile mercadofresco-dev --region eu-west-1

An available volume is a volume that exists, that is paid for every month, and that is attached to nothing. They are usually left over from terminated instances. It is exactly one of the findings Trusted Advisor also reports, which is the next lesson.

Multi-account aggregators

An aggregator consolidates Config data from several accounts and regions into a single view.

aws configservice put-configuration-aggregator \
  --configuration-aggregator-name agregador-mercadofresco \
  --account-aggregation-sources '[{
    "AccountIds": ["111122223333"],
    "AllAwsRegions": true
  }]' \
  --profile mercadofresco-dev --region eu-west-1

With AWS Organizations you use --organization-aggregation-source and the aggregator picks up every account in the organisation automatically, including any created in the future. That is the right configuration for any company with several accounts, and it is lesson 09-04.

MercadoFresco has one account today, so the aggregator only adds the multi-region view —useful for spotting forgotten resources in regions where you should have nothing at all, the case of exercise 3 of 05-03—.

Security Hub and GuardDuty: where each one fits

It is easy to confuse these three tools. The table that separates them:

AWS Config Amazon GuardDuty AWS Security Hub
What it does Evaluates configurations Detects threats Aggregates findings
Data source Configuration items CloudTrail, DNS, flow logs, S3, EKS Config, GuardDuty, Inspector, Macie…
Detects An unencrypted bucket A malicious IP talking to your instance Both, on one dashboard
Based on Your rules Learning and threat intelligence Standards (CIS, PCI, AWS FSBP)
Fixes Yes (remediation) No Via custom actions
Cost Per CI and evaluation Per GB analysed and events Per check and finding
Question Is it configured correctly? Is somebody inside? What is my overall posture?

Concrete examples that clarify the division of labour:

  • Config: "mercadofresco-registros-web has no encryption." That is a configuration problem.
  • GuardDuty: "An instance of asg-mercadofresco-tienda is resolving domains associated with cryptocurrency mining" or "instance role credentials used from an IP outside AWS". That is an intrusion problem, and no configuration rule would detect it.
  • Security Hub: "Your account meets 82 % of the CIS Benchmark; these 14 findings are critical", with the Config and GuardDuty findings in the same prioritised list.

Practical recommendation: if you are going to enable all three, enable them in this order —Config first, because it is the foundation and because Security Hub consumes its rules; GuardDuty next, because it detects what Config cannot see; Security Hub last, as the aggregation layer—. And bear in mind that all three cost money and all three generate findings somebody has to read. Enabling all three and never looking at them is worse than enabling only Config and reviewing it monthly.

Real cost and how not to blow it up

Item Price in eu-west-1
Configuration item recorded 0.003 USD each
Rule evaluation (first 100,000/month) 0.001 USD each
Evaluations 100,001-500,000 0.0008 USD
Advanced queries Free
Conformance packs Only the rules they contain
Remediation execution (SSM Automation) Free in the standard tier
S3 storage 0.023 USD/GB/month
Aggregator Free

Calculation for MercadoFresco:

Item Amount/month Cost
CIs of stable resources (S3, RDS, IAM, KMS, SG, ALB…) ~450 1.35 USD
CIs of instances and volumes (daily mode) ~180 0.54 USD
Change-triggered rule evaluations (29 rules) ~4,200 4.20 USD
Periodic evaluations (10 rules × 30 days) 300 0.30 USD
S3 storage ~0.3 GB 0.01 USD
Total ~6.40 USD/month

And now the same calculation without the optimisations, which is what happens to most accounts:

Item Amount/month Cost
allSupported: true with instances in continuous mode ~28,000 CIs 84.00 USD
Evaluations over all those resources ~140,000 132.00 USD
Total ~216.00 USD/month

Thirty-four times more expensive, and with exactly the same ability to detect the problems that matter. The difference lies entirely in three configuration decisions.

The five ways to blow up your Config bill, by real-world frequency:

  1. allSupported: true in an account with auto scaling. Every instance launch generates CIs for the instance, its volumes, its network interfaces and its associations. Thousands a month.
  2. Not using recordingFrequency: DAILY for ephemeral resources. The most profitable optimisation and the least well known.
  3. Recording global resources (IAM) in several regions. You pay for the same CI as many times as you have regions whose recorder has includeGlobalResourceTypes.
  4. Deploying a 200-rule conformance pack without reading it. Each rule is evaluated against every applicable resource, on every change.
  5. Custom Lambda rules over very volatile resource types. You pay for the evaluation and the invocation.

The five defences:

  1. An explicit resourceTypes list, never allSupported.
  2. recordingFrequency: DAILY for AWS::EC2::Instance, AWS::EC2::Volume and anything ephemeral.
  3. Global resources in a single region.
  4. Periodic rules every 24 h instead of change-triggered whenever you do not need instant detection.
  5. An AWS Budgets budget filtered by service = Config (module 11), with a notice at 80 %.

Cleanup

# 1. Remove the remediations BEFORE the rules
aws configservice delete-remediation-configuration \
  --config-rule-name mercadofresco-s3-cifrado \
  --profile mercadofresco-dev --region eu-west-1

# 2. Delete the rules
for R in mercadofresco-s3-cifrado mercadofresco-ssh-restringido \
         mercadofresco-puertos-restringidos mercadofresco-retencion-registros \
         mercadofresco-etiquetado-obligatorio mercadofresco-ciclo-vida-seguro \
         mercadofresco-cifrado-buckets-negocio; do
  aws configservice delete-config-rule --config-rule-name "$R" \
    --profile mercadofresco-dev --region eu-west-1
done

# 3. The conformance pack
aws configservice delete-conformance-pack \
  --conformance-pack-name paquete-mercadofresco-base \
  --profile mercadofresco-dev --region eu-west-1

# 4. STOP THE RECORDER: this is what cuts the spend
aws configservice stop-configuration-recorder \
  --configuration-recorder-name grabador-mercadofresco \
  --profile mercadofresco-dev --region eu-west-1

# 5. Channel and recorder
aws configservice delete-delivery-channel --delivery-channel-name canal-mercadofresco \
  --profile mercadofresco-dev --region eu-west-1
aws configservice delete-configuration-recorder \
  --configuration-recorder-name grabador-mercadofresco \
  --profile mercadofresco-dev --region eu-west-1

# 6. The history bucket (check first whether you are required to keep it)
aws s3 rm s3://mercadofresco-config-historial --recursive --profile mercadofresco-dev
aws s3api delete-bucket --bucket mercadofresco-config-historial --profile mercadofresco-dev

Step 4 is the important one. Deleting rules does not stop the spend: CIs keep being recorded for as long as the recorder is running. Stopping the recorder is what cuts the billing. And if at some point you see a Config charge you were not expecting, always start with describe-configuration-recorder-status in every region: a forgotten recorder in a region you do not use is a classic.

Common Mistakes and Tips

1. Enabling allSupported: true without thinking. It is the number one cost mistake of the service. An explicit list, always.

2. Not using recordingFrequency: DAILY for instances and volumes. With an active ASG, it is the difference between 6 and 216 USD a month.

3. Recording global resources in several regions. You pay for the same CI several times. IAM and CloudFront in a single region.

4. Enabling Automatic: true on a remediation without testing it. Detection for a week, manual remediation for two weeks, automatic in development for a week, and then production. No shortcuts.

5. Automating destructive remediations. Deleting resources, stopping instances or modifying IAM automatically can turn a badly written rule into an outage. Automate only what adds security and is reversible.

6. Confusing Config with CloudTrail. "Who changed it?" → CloudTrail. "How is it and is it compliant?" → Config. They are used together.

7. Returning COMPLIANT instead of NOT_APPLICABLE in a custom rule. The compliance dashboard ends up inflated and stops being useful.

8. Wiring every Config notification to SNS. One message per configuration change is guaranteed noise, and noise makes people stop reading the important alerts. Filter with EventBridge (07-03) and send only the non-compliances to people.

9. Believing a conformance pack certifies you. It does not. It checks technical controls; real compliance includes processes, contracts and a formal audit. Have it validated by a compliance professional.

10. Applying an SG remediation without narrowing the scope of the rule. RemoveUnrestrictedSourceIngressRules over sg-mercadofresco-alb would take the shop down. Narrow it with TagKey/TagValue.

11. Deleting rules believing that stops the spend. What bills you is the recorder's CIs. stop-configuration-recorder.

12. Forgetting start-configuration-recorder. As with CloudTrail, creating is not starting.

13. Writing a rule with Lambda when Guard was enough. If the check only looks at the configuration item, Guard is shorter, cheaper and there is no runtime to maintain.

Final tip: enable Config with few, well-chosen rules rather than with a complete conformance pack. Ten rules somebody reviews every month are worth infinitely more than two hundred that produce a permanently red dashboard everybody gets used to. The goal is not to have many rules: it is to get to zero non-compliances and for any new one to be a real signal.

Exercises

Exercise 1: designing the Config setup on a budget

MercadoFresco wants to enable Config with a maximum budget of 12 USD a month. Real inventory of the account:

Resource type Amount Estimated changes/month
AWS::S3::Bucket 7 4
AWS::EC2::SecurityGroup 6 8
AWS::EC2::Instance 2-4 (ASG) 620 (rotation)
AWS::EC2::Volume 4-8 640
AWS::EC2::NetworkInterface 4-8 1,280
AWS::RDS::DBInstance 2 3
AWS::IAM::Role 9 6
AWS::IAM::Policy 12 5
AWS::Lambda::Function 2 20 (deployments)
AWS::Logs::LogGroup 9 12
AWS::KMS::Key 1 1

Requirements: you have to detect within 15 minutes that a bucket loses its encryption or that an SG is opened to the world; you have to check the mandatory tagging; you have to keep an eye on log group retention.

Design: the resourceTypes list with its recordingFrequency, the set of rules with their trigger type, and calculate the total cost. Justify every exclusion. If you go over budget, explain what you would cut and what detection capability you would lose.

Exercise 2: the custom rule and its remediation

MercadoFresco has an internal policy: every RDS snapshot older than 90 days must be encrypted and must not be public, and those older than 365 days must be deleted unless they carry the Retencion=legal tag.

  • a) Explain why this check cannot be done with a single managed rule, and which managed rules cover part of the problem.
  • b) Write the custom rule (Lambda or Guard, justifying your choice) that evaluates the snapshots.
  • c) Design the remediation. Decide what gets automated and what does not, and justify it with the criterion from the lesson.
  • d) Calculate the added cost if MercadoFresco has 120 snapshots and the rule is periodic every 24 hours, compared with change-triggered.

Exercise 3: the combined investigation

One Monday, the mercadofresco-s3-cifrado rule flags mercadofresco-informes-analitica as NON-COMPLIANT. Automatic remediation is still in manual mode. Data:

  • The timeline shows the non-compliant CI was created on Sunday at 04:17.
  • The previous CI, from Thursday, was compliant.
  • The mercadofresco-etiquetado-obligatorio rule flags the same bucket as non-compliant since Sunday at 04:17: it is missing the Propietario tag.
  • There are three new buckets in the account, created on Sunday between 04:10 and 04:25, none of them with encryption or tags.
  • CloudTrail shows 46 write calls that Sunday between 04:05 and 04:30, all with userIdentity.type = "AssumedRole" and sessionIssuer.userName = "rol-despliegue-infra".

Explain: what has most probably happened; how you would confirm it by combining Config and CloudTrail (with specific commands and queries); why Config detected something CloudTrail on its own would never have flagged; what remediation you would apply and in what order; and what underlying fix you would propose so it does not happen again, indicating which module of the course covers it.

Solutions

Solution 1

Inventory analysis. The problem is concentrated in three rows: instances (620), volumes (640) and network interfaces (1,280). That is 2,540 of the 2,679 monthly changes: 95 %. All of them generated by the ASG rotation and all of them with identical configuration, because they come from lt-mercadofresco-tienda.

Proposed recorder configuration:

Type Record? Frequency Justification
AWS::S3::Bucket Yes Continuous Requirement: detection within 15 min
AWS::EC2::SecurityGroup Yes Continuous Requirement: detection within 15 min
AWS::RDS::DBInstance Yes Continuous Few changes, high value
AWS::IAM::Role / Policy Yes Continuous Few changes, high value
AWS::KMS::Key Yes Continuous 1 change/month
AWS::Logs::LogGroup Yes Continuous Retention requirement
AWS::Lambda::Function Yes Continuous 20 changes/month, acceptable
AWS::EC2::Instance Yes DAILY 620 → ~30 CIs/month
AWS::EC2::Volume Yes DAILY 640 → ~30 CIs/month
AWS::EC2::NetworkInterface NO 1,280 CIs of almost no value

Configuration item calculation:

Type CIs/month Cost
Buckets 4 0.012 USD
SGs 8 0.024 USD
Instances (daily) 30 0.090 USD
Volumes (daily) 30 0.090 USD
RDS 3 0.009 USD
Roles + policies 11 0.033 USD
Lambdas 20 0.060 USD
Log groups 12 0.036 USD
KMS 1 0.003 USD
Initial discovery (one-off) ~55 0.165 USD
Total CIs ~174 ~0.52 USD

Proposed rules:

Rule Trigger Evaluations/month Cost
s3-bucket-server-side-encryption-enabled Change 4 + 7 initial = 11 0.011 USD
s3-bucket-public-read-prohibited Change 11 0.011 USD
restricted-ssh Change 8 + 6 = 14 0.014 USD
restricted-common-ports Change 14 0.014 USD
vpc-default-security-group-closed Change 14 0.014 USD
rds-storage-encrypted Change 3 + 2 = 5 0.005 USD
rds-multi-az-support Change 5 0.005 USD
cw-loggroup-retention-period-check Change 12 + 9 = 21 0.021 USD
encrypted-volumes Change (daily) 30 0.030 USD
required-tags Periodic 24 h 30 × 25 resources = 750 0.750 USD
iam-user-mfa-enabled Periodic 24 h 30 0.030 USD
iam-root-access-key-check Periodic 24 h 30 0.030 USD
cloudtrail-enabled Periodic 24 h 30 0.030 USD
access-keys-rotated Periodic 24 h 30 0.030 USD
kms-cmk-not-scheduled-for-deletion Periodic 24 h 30 0.030 USD
Total evaluations ~1,062 ~1.03 USD

Total cost: 0.52 + 1.03 + ~0.02 of S3 = ~1.57 USD/month. Well under the 12 USD.

With the headroom left over you could: add the complete CIS conformance pack (~50 rules, some 3-4 USD more), switch required-tags to a change trigger for faster detection, or include AWS::EC2::NetworkInterface if it were ever needed for a network investigation.

Justification of the exclusions:

  • NetworkInterface: 1,280 CIs (3.84 USD) for ephemeral interfaces the ASG creates and destroys. Their configuration is always the same and comes from the launch template. If a network investigation were needed, there are the VPC Flow Logs (03-01) and CloudTrail (05-03).
  • Instances and volumes in daily mode: you lose immediate detection of a change on a specific instance. Does it matter? Instances are cattle, not pets: if one degrades, the ASG replaces it. The changes that matter are in the launch template, not in the instance. The trade-off is the right one.

What you would lose by cutting further: if you had to get below 1.57 USD, the next cut would be required-tags (0.75 USD, almost half the total). You would lose the detection of the CentroCoste hole in the volumes, which in module 11 is worth much more than 0.75 USD. It does not get cut.

Solution 2

a) Why a single managed rule is not enough.

There are managed rules that cover parts of it:

Managed rule What it covers What it misses
rds-snapshot-encrypted Encrypted snapshots It does not look at age
rds-snapshots-public-prohibited That they are not public It does not look at age
required-tags The Retencion tag It does not relate it to age

What none of them covers is the combined logic: "older than 90 days and unencrypted", "older than 365 days and without the Retencion=legal tag". Config does not let you compose managed rules with conditions between them. That calls for a rule of your own.

b) The rule. Choice: Lambda, not Guard.

Guard can only look at what is in the configuration item. Here we need to compute the age relative to the current date, and Guard has no date arithmetic against "now". On top of that, we want a periodic trigger that evaluates every snapshot, not only the ones that change —a snapshot does not change: it simply gets older—. That is the decisive argument: the non-compliance appears with the passage of time, not with a configuration change.

"""Config rule: RDS snapshots by age.

PERIODIC trigger every 24 h: a snapshot does not change,
but it ages, and non-compliance appears through the passage of time.
"""
import json
from datetime import datetime, timezone
import boto3

config = boto3.client("config")
rds = boto3.client("rds")

ENCRYPTION_DAYS = 90
DELETION_DAYS = 365


def age_in_days(date):
    return (datetime.now(timezone.utc) - date).days


def evaluate_snapshot(snap, tags):
    identifier = snap["DBSnapshotIdentifier"]
    age = age_in_days(snap["SnapshotCreateTime"])

    if age > DELETION_DAYS and tags.get("Retencion") != "legal":
        return ("NON_COMPLIANT",
                f"{age} days old and without Retencion=legal: must be deleted")

    if age > ENCRYPTION_DAYS and not snap.get("Encrypted", False):
        return ("NON_COMPLIANT",
                f"{age} days old and NOT ENCRYPTED")

    # The public-access check requires another call.
    attributes = rds.describe_db_snapshot_attributes(
        DBSnapshotIdentifier=identifier
    )["DBSnapshotAttributesResult"]["DBSnapshotAttributes"]
    for attribute in attributes:
        if attribute["AttributeName"] == "restore" and "all" in attribute["AttributeValues"]:
            return "NON_COMPLIANT", f"The snapshot is PUBLIC"

    return "COMPLIANT", f"{age} days, encrypted and private"


def handler(event, context):
    token = event["resultToken"]
    evaluations = []
    now = datetime.now(timezone.utc)

    paginator = rds.get_paginator("describe_db_snapshots")
    for page in paginator.paginate(SnapshotType="manual"):
        for snap in page["DBSnapshots"]:
            tags = {t["Key"]: t["Value"] for t in snap.get("TagList", [])}
            status, reason = evaluate_snapshot(snap, tags)
            evaluations.append({
                "ComplianceResourceType": "AWS::RDS::DBSnapshot",
                "ComplianceResourceId": snap["DBSnapshotIdentifier"],
                "ComplianceType": status,
                "Annotation": reason[:256],
                "OrderingTimestamp": now,
            })

    # put_evaluations accepts a maximum of 100 per call.
    for i in range(0, len(evaluations), 100):
        config.put_evaluations(
            Evaluations=evaluations[i:i + 100],
            ResultToken=token,
        )

    return {"evaluated": len(evaluations)}

Four details that make this function work in production and not just in the example:

  • SnapshotType="manual": the automatic ones are managed by RDS with its own retention (02-04) and delete themselves. Evaluating them would generate non-compliances impossible to remediate.
  • put_evaluations in batches of 100: that is the API limit. With 120 snapshots a single call would fail.
  • Annotation with the specific age: whoever looks at the console knows immediately what it is about.
  • The public-access check makes an extra call per snapshot. With 120 snapshots that is 120 calls every 24 hours: acceptable. With 10,000, you would have to parallelise or split it into another rule.

c) The remediation, and here is the criterion.

Non-compliance Automate? Why
Public snapshot YES, immediately It adds security, it is reversible, and a public database backup is a data breach in progress. AWSConfigRemediation-RevokeRDSDBSnapshotPublicAccess
Unencrypted snapshot older than 90 days Not automatic "Encrypting" an existing snapshot is not possible: you have to copy it encrypted and delete the original. It is an operation with cost, duration and risk. A ticket is raised
Snapshot older than 365 days without the tag Never automatic It is a deletion. Irreversible. If the rule or the tag is wrong, the only copy of something important is destroyed

That table is exactly the rule from the lesson: automate what adds security and is reversible; never anything destructive. The public snapshot closes itself in minutes; the deletion is approved by a person.

For the two that are not automated, the right answer is to notify properly: an EventBridge rule (07-03) that captures the change to non-compliant and sends alertas-mercadofresco a message including the evaluation's Annotation, which is where the specific explanation lives ("412 days old and unencrypted"). A notice that only says "there is a non-compliant resource" forces you into the console; one that says which snapshot and why can be dealt with from your phone.

And a quarterly review in Marta's calendar to decide what gets deleted, with the list in front of her.

d) Cost.

Periodic option every 24 hours:

  • 120 snapshots × 30 days = 3,600 evaluations/month = 3.60 USD
  • Lambda invocations: 30/month (one per rule evaluation, not per snapshot) ≈ 0.00 USD
  • RDS API calls: free
  • Total: ~3.60 USD/month

Change-triggered option:

  • Manual snapshots barely change: maybe 10 CIs/month.
  • 10 evaluations = 0.01 USD
  • Total: ~0.01 USD/month

But the change-triggered option DOES NOT WORK, and that is the conclusion of this part. The non-compliance here appears with the passage of time, not with a change: a snapshot created today without encryption is compliant, and 91 days from now it stops being so without anything having changed in it. A change-triggered rule would never re-evaluate it and it would never be detected.

3.60 USD a month is the price of detecting time-dependent non-compliances, and TwentyFour_Hours is already the most spaced-out periodic frequency Config accepts: there is no headroom there. The only real saving lever would be to emit a single aggregated evaluation per rule instead of one per snapshot, but then the console would say "the rule is not met" without saying which of the 120 snapshots is to blame, which is precisely the piece of information you need in order to act. You pay for the detail, and it is worth it.

Solution 3

What has most probably happened.

The data points unambiguously to an automated infrastructure deployment in the small hours of Sunday that created resources without the mandatory properties, and that in addition modified an existing bucket.

The clues, in order:

  1. Sunday at 04:17, an hour with no human activity: it is an automated process.
  2. rol-despliegue-infra with 46 write calls in 25 minutes: it is a pipeline or an infrastructure-as-code run.
  3. Three new buckets with no encryption and no tags, created in the same window.
  4. An existing bucket that went from compliant to non-compliant at exactly the same time.

Point 4 is the most serious and the most revealing: the template did not just create badly configured new resources, it redefined a bucket that already existed, overwriting its encryption and its tags with an incomplete definition. It is the classic pattern of a CloudFormation or Terraform template that declares a resource which already existed and "aligns" it with a poor specification.

Confirmation, step by step.

1. See exactly what changed in the bucket (this is what only Config can give you):

aws configservice get-resource-config-history \
  --resource-type AWS::S3::Bucket \
  --resource-id mercadofresco-informes-analitica \
  --later-time 2026-08-03T00:00:00Z \
  --earlier-time 2026-07-30T00:00:00Z \
  --profile mercadofresco-dev --region eu-west-1

Comparing Thursday's CI with Sunday's, you can see the diff: serverSideEncryptionConfiguration disappeared and the tags were reduced. This is information CloudTrail does not have: CloudTrail records the PutBucketEncryption call or its absence, but not the complete resulting state of the bucket.

2. See who made the specific call (this is what only CloudTrail can give you):

SELECT eventtime, eventname,
       split_part(userIdentity.arn, '/', 3) AS session,
       sourceipaddress, useragent,
       json_extract_scalar(requestParameters, '$.bucketName') AS bucket
FROM auditoria_mercadofresco.cloudtrail_mercadofresco
WHERE anio='2026' AND mes='08' AND dia='02'
  AND userIdentity.sessionContext.sessionIssuer.userName = 'rol-despliegue-infra'
ORDER BY eventtime;

You are looking for: the userAgent (cloudformation.amazonaws.com? Terraform/1.7?), the session name, and whether there is an explicit DeleteBucketEncryption or simply a CreateBucket over an existing bucket.

3. See every affected resource at once, with Config's advanced query:

aws configservice select-resource-config \
  --expression "
    SELECT resourceId, resourceName, resourceCreationTime, tags
    WHERE resourceType = 'AWS::S3::Bucket'
      AND resourceCreationTime > '2026-08-02T00:00:00Z'
  " \
  --profile mercadofresco-dev --region eu-west-1

4. The full compliance dashboard, to see whether there is more damage than you knew about:

aws configservice get-compliance-details-by-config-rule \
  --config-rule-name mercadofresco-etiquetado-obligatorio \
  --compliance-types NON_COMPLIANT \
  --profile mercadofresco-dev --region eu-west-1

Why CloudTrail on its own would not have raised anything.

This is the central question of the exercise, and there are three reasons:

  1. CloudTrail does not evaluate. It records 46 calls from a deployment role on a Sunday. That is completely normal for a scheduled pipeline. There is nothing anomalous in the event itself.
  2. The 05-03 alarm would not have fired. The metric filters we built watched DeleteBucketEncryption and PutBucketPolicy, but this came from a legitimate deployment role and probably with no explicit DeleteBucketEncryption: if the template simply does not declare encryption when creating the bucket, there is no call to record. The absence of a call does not generate an event.
  3. CloudTrail does not know MercadoFresco's policy. It does not know every bucket must be encrypted or that five tags are required. Config does, because we told it so with rules.

Config detects the resulting state; CloudTrail detects the act. Here the act was legitimate and the resulting state was not. That is exactly the gap this lesson comes to fill.

Remediation, in order of risk:

# Action Mode
1 Stop the pipeline or disable its scheduled run Manual, immediate
2 Restore encryption on mercadofresco-informes-analitica Manual remediation, verifying
3 Check whether unencrypted objects were uploaded between Sunday and Monday Manual
4 Encrypt the three new buckets Manual remediation
5 Restore the affected bucket's tags Manual
6 Decide what to do with the three new buckets: should they exist? Manual, with Luis
7 Only then, re-enable the pipeline with the corrected template Manual

Step 3 deserves a comment: the objects written while default encryption was disabled were stored unencrypted, and enabling it now does not encrypt them retroactively. They have to be rewritten:

aws s3 cp s3://mercadofresco-informes-analitica/ s3://mercadofresco-informes-analitica/ \
  --recursive --sse aws:kms \
  --sse-kms-key-id arn:aws:kms:eu-west-1:111122223333:alias/mercadofresco-datos \
  --metadata-directive REPLACE \
  --profile mercadofresco-dev

This detail gets overlooked constantly and it is what turns a configuration incident into a data incident.

The underlying fixes, which are the important part:

Fix Module Why
Fix the template so that it declares encryption and the mandatory tags 09-01 (CloudFormation) / 09-02 (CDK) The root cause. In CDK you can impose default values for the whole organisation
Validate the template before deploying with cfn-guard in the pipeline 08-02 (CodeBuild) Catching the problem before it reaches production is always better than remediating it afterwards
Enable automatic remediation on mercadofresco-s3-cifrado This lesson It would have restored the encryption in 8 minutes, in the small hours of a Sunday, with nobody finding out until Monday
Service control policy denying s3:PutObject without encryption 09-04 (Organizations) Hard prevention: you cannot even write unencrypted
Mandatory review of infrastructure changes 08-04 (CodePipeline) A manual approval step for changes over existing resources

The underlying lesson, and it deserves underlining because it is the strongest argument in favour of this service: the same Guard rule you write for Config can be run in the pipeline with cfn-guard before deployment. Config is the safety net that catches what got through; validation in the pipeline is what stops it getting through in the first place. Both, with the same rule written once. That is the pattern MercadoFresco adopts, and it closes in module 8.

Conclusion

MercadoFresco now has the last piece it was missing. You know what the configuration of a resource is —its settings, not its data— and what a configuration item is: a complete photograph with its relationships, its supplementaryConfiguration and its status, which Config creates on every change and even records when the resource is deleted. You know the timeline, which answers the question operations asks every week: "this worked on Friday; what has changed?".

You are clear on the essential difference from the previous lesson: CloudTrail records the call, Config records the state. CloudTrail says who called AuthorizeSecurityGroupIngress; Config says how the security group ended up, whether it is compliant, and fixes it. And you know Config depends on CloudTrail to find out about changes: they do not compete, they support each other.

You have enabled the grabador-mercadofresco recorder with its read-only role, its channel to mercadofresco-config-historial and its SNS topic, with the two decisions that are worth money: allSupported: false with an explicit list, and recordingFrequency: DAILY for instances and volumes, which is the most profitable and least well-known optimisation in the service. And you know that creating is not starting: start-configuration-recorder.

You know the 29 managed rules MercadoFresco enables, each one tied to something we built in an earlier module —s3-bucket-server-side-encryption-enabled for 04-02, restricted-ssh for 03-02, cloudtrail-enabled for 05-03, cw-loggroup-retention-period-check for the 610 USD incident from 05-01— and the difference between a change trigger (minutes, but evaluated on every change) and a periodic one (up to 24 hours, but fixed cost and the only option for account-level checks). required-tags has found you the hole nobody could see: the EBS volumes created by the ASG do not inherit tags, and in module 11 they would have shown up as unassigned cost.

You know how to write your own rules: with Lambda when you need to call other APIs or do date arithmetic —returning NOT_APPLICABLE instead of COMPLIANT, handling ResourceDeleted and writing a useful Annotation— and with Guard, declarative, no runtime, no extra cost and no maintenance, which is what you should use by default when looking at the configuration item is enough.

And you have set up automatic remediation with Systems Manager documents, which is what turns Config into an operational tool: AWS-EnableS3BucketEncryption restores a bucket's encryption in eight minutes with no human intervention, RemoveUnrestrictedSourceIngressRules closes an open security group, and SetCloudWatchLogGroupRetention sets 30 days on any new log group. With the remediation role kept separate from Config's, with permissions narrowed by prefix and by tag, and without AuthorizeSecurityGroupIngress: the role can close, never open. And with the non-negotiable four-phase procedure —detection, manual remediation, automatic in development, automatic in production— and the rule that sums it all up: automate what adds security and is reversible; never anything destructive or anything that takes availability away.

You know the conformance packs aligned with CIS, PCI DSS, NIST and the GDPR, with the warning that always bears repeating: deploying the PCI DSS pack does not make you compliant with PCI DSS. And the compliance dashboard has given you the result that justifies the whole service: six non-compliant rules and fourteen resources, among them a bucket unencrypted since March, a port 22 open since May and the default security group with rules. Fourteen problems that had been sitting there for months and that would never have fired a CloudWatch alarm or turned up in a CloudTrail investigation, because they were not events: they were states.

And you have the advanced queries, free of charge, that inventory the account in seconds: unencrypted buckets, unattached volumes billed every month for doing nothing at all, resources without CentroCoste. With the aggregator for the multi-account view coming in 09-04, and a clear division of labour with GuardDuty —which detects intrusions, not configurations— and Security Hub, which aggregates everybody's findings.

All for around 6.40 USD a month, against the 216 USD that exactly the same detection capability would cost without the three correct configuration decisions. That factor of thirty-four is the summary of why this lesson devotes a whole section to cost.

With this, the five questions from module 4 are answered. The alert genuinely arrives, the logs are correlated, the eight-second order has a culprit and a fix, we know who decrypted the backup, and now nothing can disable a bucket's encryption without an alert going off —and being fixed by itself in eight minutes—.

One last piece remains, and it is different from everything above. Config checks the rules you have told it about. It is powerful and it is exact, but it has an obvious limit: it cannot warn you about what has not occurred to you. It does not know you have three orphan EBS volumes costing money, nor that there is an unassociated elastic IP being billed for not being used, nor that asg-mercadofresco-tienda is about to hit a service quota nobody has ever looked at, nor that one of your instances has spent two months at 4 % CPU. Nobody has written rules for that, and nobody will, because to write them you would first have to suspect that the problem exists.

There is a service that does precisely that review: comparing your account with the best practices AWS has distilled from millions of customers, and telling you what you had not thought to ask. In lesson 05-05, "AWS Trusted Advisor", we will look at its five categories —cost optimisation, performance, security, fault tolerance and service limits—, which checks you really see with the Basic support plan and which you do not, how to make up for what is missing with what you already know, a commented walkthrough of a real report on MercadoFresco's architecture, the service quotas and the specific case of the ASG that cannot go beyond four instances because of a vCPU limit nobody had looked at, and the monthly review routine Marta adopts. And with it we close the module.

© Copyright 2026. All rights reserved