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
- What a resource's configuration is
- The configuration item and the timeline
- Config versus CloudTrail
- The full flow: recorder, rules and remediation
- Enabling the configuration recorder
- The delivery channel: S3 and SNS
- Choosing which resource types are recorded
- Rules: the heart of the service
- The managed rules MercadoFresco enables
- Change trigger versus periodic trigger
- Mandatory tagging with
required-tags - Custom rules with Lambda
- Custom rules with Guard
- Automatic remediation with Systems Manager
- The two real cases: bucket encryption and an open SG
- The warning about testing in manual mode first
- Conformance packs
- MercadoFresco's compliance dashboard
- Advanced queries for taking inventory
- Multi-account aggregators
- Security Hub and GuardDuty: where each one fits
- Real cost and how not to blow it up
- 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 mainDescribecall 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-1Config 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
AuthorizeSecurityGroupIngressevent, 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-sshrule evaluation flagging it asNON_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:
- Configuration recorder: the process that creates the CIs. There can only be one per account and region.
- Delivery channel: where the CIs are deposited (an S3 bucket) and where changes are notified (an SNS topic). Also one per account and region.
- Rules: the conditions that get evaluated.
- Remediation: the corrective action, run by Systems Manager Automation.
- 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-devWith 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-devThe 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-1Two decisions that are worth real money here:
allSupported: falsewith an explicit list. Recording the 250+ supported types in a live account generates an enormous volume of CIs, and many of them add nothing.recordingModeOverrideswithDAILYfor instances and volumes. This is the most important optimisation in the whole lesson. Theasg-mercadofresco-tiendaASG 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 fromlt-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-1Step 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-1And 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-1And 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-1Change 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-1Syntax details that are not obvious:
tagNKeywithouttagNValuerequires the tag to exist, with any value. That is howComponente,PropietarioandCentroCosteare set: their values are free.tagNValuewith a comma-separated list requires the value to be one of those.Entornocan only beproduccion,preproduccionordesarrollo.- 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_APPLICABLEfor what does not apply, notCOMPLIANT. If you returnCOMPLIANTfor 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. Annotationis 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.ResultTokenmust 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-1Cost: 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:PutEncryptionConfigurationonly overmercadofresco-*. 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-1The 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-1This 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-1This 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-1You 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_CHECKaws 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-1The 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:
mercadofresco-registros-webunencrypted 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.- 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.
- The default security group of
vpc-mercadofrescowith 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-1This 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-1An 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-1With 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-webhas no encryption." That is a configuration problem. - GuardDuty: "An instance of
asg-mercadofresco-tiendais 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:
allSupported: truein an account with auto scaling. Every instance launch generates CIs for the instance, its volumes, its network interfaces and its associations. Thousands a month.- Not using
recordingFrequency: DAILYfor ephemeral resources. The most profitable optimisation and the least well known. - Recording global resources (IAM) in several regions. You pay for the same CI as many times as
you have regions whose recorder has
includeGlobalResourceTypes. - Deploying a 200-rule conformance pack without reading it. Each rule is evaluated against every applicable resource, on every change.
- Custom Lambda rules over very volatile resource types. You pay for the evaluation and the invocation.
The five defences:
- An explicit
resourceTypeslist, neverallSupported. recordingFrequency: DAILYforAWS::EC2::Instance,AWS::EC2::Volumeand anything ephemeral.- Global resources in a single region.
- Periodic rules every 24 h instead of change-triggered whenever you do not need instant detection.
- 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-devStep 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-obligatoriorule flags the same bucket as non-compliant since Sunday at 04:17: it is missing thePropietariotag. - 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"andsessionIssuer.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_evaluationsin batches of 100: that is the API limit. With 120 snapshots a single call would fail.Annotationwith 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:
- Sunday at 04:17, an hour with no human activity: it is an automated process.
rol-despliegue-infrawith 46 write calls in 25 minutes: it is a pipeline or an infrastructure-as-code run.- Three new buckets with no encryption and no tags, created in the same window.
- 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-1Comparing 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-14. 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-1Why CloudTrail on its own would not have raised anything.
This is the central question of the exercise, and there are three reasons:
- 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.
- The 05-03 alarm would not have fired. The metric filters we built watched
DeleteBucketEncryptionandPutBucketPolicy, but this came from a legitimate deployment role and probably with no explicitDeleteBucketEncryption: 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. - 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-devThis 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.
AWS Course
Module 1: Introduction to AWS
- What Is AWS?
- Setting Up Your AWS Account
- AWS Global Infrastructure
- The AWS Management Console
- AWS CLI and SDKs
Module 2: Core AWS Services
Module 3: Networking and Content Delivery
Module 4: Security and Identity
- AWS Identity and Access Management (IAM)
- AWS Key Management Service (KMS)
- Secrets Manager and Parameter Store
- AWS Shield
- AWS WAF
Module 5: Monitoring and Management
Module 6: Databases
Module 7: Application Integration
- Amazon SQS
- Amazon SNS
- Amazon EventBridge
- AWS Step Functions
- Integration Patterns: Idempotency, Retries and Dead-Letter Queues
