At the end of 04-01 we left a promise half kept. The policy pol-mercadofresco-tienda requires every thumbnail to be uploaded with s3:x-amz-server-side-encryption: aws:kms, and rol-lambda-miniaturas has been granted kms:Decrypt and kms:GenerateDataKey on a key identified by a UUID. But that key, alias/mercadofresco-datos, is still little more than a name: nobody has decided who administers it, who may use it, how often it rotates, or what exactly happens when S3 "encrypts" an object.

And there is a business reason behind it. MercadoFresco stores names, delivery addresses, phone numbers and order histories of Spanish customers. That is personal data under the GDPR, and article 32 of the regulation cites encryption explicitly as an appropriate technical measure. If a laptop holding a copy of the database ends up on the back seat of a taxi, the difference between "incident" and "breach notifiable within 72 hours" is usually whether the contents were encrypted.

AWS Key Management Service (KMS) is the service that guards the encryption keys, controls who may use them and records every use. In this lesson Marta really understands it —including the mechanism of envelope encryption, which explains everything else— and finally encrypts the database backups, the volumes and the instance mercadofresco-pedidos.

Warning. The examples in this lesson are teaching material. Any configuration of encryption, key management or regulatory compliance (GDPR, PCI DSS, ENS) that is going to be applied to real customer data must be reviewed by a security or compliance professional before it reaches production. Badly implemented encryption gives a false sense of security, and a badly deleted key destroys data irreversibly. Every identifier and piece of data in this course is fictitious.

Contents

  1. Why encrypt: the GDPR and MercadoFresco's data
  2. At rest and in transit
  3. Symmetric and asymmetric cryptography, just enough
  4. What KMS is and what it is not
  5. Key types
  6. Multi-Region keys, imported material and CloudHSM
  7. Envelope encryption
  8. Decryption, step by step
  9. Client-side and server-side encryption
  10. SSE-S3, SSE-KMS and SSE-C
  11. S3 Bucket Keys and the cost of requests
  12. The key policy and its relationship with IAM
  13. kms:ViaService and the encryption context
  14. Grants
  15. Automatic rotation and what it really means
  16. Encrypting mercadofresco-copias-basedatos
  17. Encrypting EBS volumes and snapshots
  18. Encrypting mercadofresco-pedidos, which already exists unencrypted
  19. Sharing an encrypted snapshot with another account
  20. KMS from boto3
  21. Limits, quotas and the real cost for MercadoFresco
  22. ACM is not KMS
  23. Deleting a key: the 7 to 30 day wait

Why encrypt: the GDPR and MercadoFresco's data

Encryption does not protect against everything. It protects against a very specific set of threats, and it is worth being honest about which ones:

Threat Does encryption at rest help?
Somebody steals a physical disk from the data centre Yes, completely
A snapshot or a backup is shared with another account by mistake Yes, if they do not have the key
A bucket is left publicly exposed Yes with SSE-KMS: on top of the S3 permission you need the KMS one
An audit's regulatory requirement is met Yes, and with a record of every use as well
An attacker steals the application's credentials No: the application has permission to decrypt
A SQL injection extracts the customer table No: the database returns the data already in the clear

That table matters: encryption at rest is a layer, not a magic shield. The last two rows are exactly the reasons why 04-01 exists and why 04-05 will exist.

For MercadoFresco there are three data sets that must be encrypted, no argument:

  1. mercadofresco-copias-basedatos: full backups of the pedidos database, with the names, addresses and phone numbers of every customer. It is the most dangerous asset in the account.
  2. The instance mercadofresco-pedidos and its automatic snapshots.
  3. The EBS volumes of the shop instances and the volume mercadofresco-fotos-01.

The public photo catalogue (productos/) contains no personal data, but it is encrypted anyway because the marginal cost is zero and not having to think about what is encrypted and what is not is in itself an operational advantage.

At rest and in transit

They are two different problems, solved by different mechanisms:

At rest In transit
What it protects Data stored on disk Data travelling over the network
Mechanism AES-256 over the volume, the object or the row TLS 1.2/1.3
Service KMS ACM for the certificates
In MercadoFresco S3, EBS, RDS, snapshots HTTPS on CloudFront and on the ALB (03-03, 03-05)
How it is enforced Bucket default encryption, kms: in the policy Deny with aws:SecureTransport: false

Encryption in transit has been solved since module 3: the ACM certificate on the ALB and on CloudFront, and the Deny we wrote into pol-mercadofresco-tienda for when aws:SecureTransport is false. This lesson is about the other one.

Symmetric and asymmetric cryptography, just enough

You do not need to know cryptography to use KMS, but you do need to tell two families apart:

Symmetric. The same key encrypts and decrypts. It is blazingly fast: AES-256 encrypts hundreds of megabytes per second on any modern CPU. Its problem is distribution: the key has to reach the other end without anybody intercepting it.

Asymmetric. Two mathematically related keys: the public one encrypts and the private one decrypts (or the other way round, for signing). It solves distribution —the public one can be published— but it is orders of magnitude slower and can only encrypt very small amounts of data (a 2048-bit RSA key encrypts at most 190 bytes).

Symmetric Asymmetric
Keys One Public/private pair
Speed Very fast Slow
Data size Unlimited Hundreds of bytes
Typical algorithm AES-256-GCM RSA-2048, ECC
Use in KMS Default, data encryption Signing, encryption between parties with no shared secret

In KMS, unless you ask for something else, every key is symmetric AES-256. And the combination of both ideas —using symmetric for the data and protecting that key with another key— is exactly the envelope encryption we look at two sections from here.

What KMS is and what it is not

KMS is a service that:

  • Generates and guards master keys inside hardware modules certified to FIPS 140-3 level 3, from which the key material never leaves in the clear.
  • Performs cryptographic operations (Encrypt, Decrypt, GenerateDataKey, Sign, Verify) on small amounts of data.
  • Controls who may use each key, through policies.
  • Records every call in CloudTrail (05-03), including who decrypted what and when.

KMS is not:

  • A secrets store: for passwords and connection strings there is Secrets Manager (04-03).
  • A service for encrypting large files: there is a hard limit of 4 KB per Encrypt call.
  • A certificate authority: that is ACM, and we clear it up at the end.
  • A place you can extract your key from and take it away. With KMS-managed keys, the material cannot be exported. That is a guarantee, not a limitation.

That last sentence deserves a pause: if the material never leaves, how is the data encrypted? The answer is envelope encryption.

Key types

In KMS the fundamental unit is the KMS key (formerly called the CMK, Customer Master Key). There are three categories by origin:

Type Who creates it Policy Rotation Cost When
AWS owned AWS, shared across customers You never see it AWS decides Free S3 default encryption (SSE-S3)
AWS managed (aws/s3, aws/rds, aws/ebs) AWS, one per service and account Not editable Annual, mandatory Free (requests are not) Getting started fast, no fine control
Customer managed (CMK) You Editable Optional, configurable 1 USD/month When you need to control who uses the key

The decisive difference between the last two is not the price, it is control:

  • With an AWS managed key you cannot edit the key policy. Any principal in your account with S3 permissions will be able to decrypt. You cannot use it from another account nor deny its use to a specific role.
  • With a customer managed key you decide exactly who administers and who uses it, you can deny a role access even if it has S3 permissions, you can share it between accounts and you can disable it, which makes everything encrypted with it unreadable instantly. That last one is a very powerful emergency button.

For mercadofresco-copias-basedatos, which holds personal data, the key must be customer managed. That is the role of alias/mercadofresco-datos.

And a note on aliases: an alias is a mutable pointer to a key. Using alias/mercadofresco-datos in the code instead of the UUID lets you change the underlying key without touching the application. Each alias is regional and cannot be repeated within the region.

Multi-Region keys, imported material and CloudHSM

Three variants worth knowing about even though MercadoFresco does not use them today:

Multi-Region keys. A primary key in eu-west-1 and replicas in other regions that share the same cryptographic material and the same identifier (with the mrk- prefix). What is encrypted in one region decrypts in another with no cross-region calls. It is essential for cross-region backups and for disaster recovery. It breaks regional isolation, so use it only when it is genuinely needed.

Imported material (BYOK). You bring your own key material and KMS merely guards it. It is used when a regulation requires the organisation to generate the keys. It comes with a serious catch: if you lose your copy of the material and it expires in KMS, the data is unrecoverable. And it does not support automatic rotation.

Custom key store on CloudHSM. The material lives in a CloudHSM cluster you own, dedicated, single-tenant. It is what some financial or public-sector environments demand. It costs of the order of 1.50 USD per hour and HSM —over 1,000 USD a month per HSM, and two are recommended— so it is a compliance decision, not an engineering one. We only mention it.

Envelope encryption

This is the central concept of the lesson. If the key material never leaves KMS and Encrypt only takes 4 KB, how is a 200 GB snapshot encrypted?

The answer is that large data never travels to KMS. Two levels of key are used:

  • The master key (the KMS key) lives in KMS and never leaves.
  • The data key is a single-use AES-256 key that KMS generates, hands over in two formats and then forgets.
sequenceDiagram
    participant A as Application / S3
    participant K as KMS<br/>alias/mercadofresco-datos
    participant D as Disk

    Note over A,D: ENCRYPTION
    A->>K: GenerateDataKey(KeyId, KeySpec=AES_256)
    K->>K: Generates a random AES-256 key
    K-->>A: Plaintext (key in the clear)<br/>+ CiphertextBlob (the same key, encrypted<br/>with the master key)
    A->>A: Encrypts the 200 GB with Plaintext (local AES, fast)
    A->>A: WIPES Plaintext from memory
    A->>D: Stores: encrypted data + CiphertextBlob together

The steps, in words:

  1. The application asks KMS for a data key. It does not send the data.
  2. KMS generates a random AES-256 key and returns two versions of the same key: in the clear (Plaintext) and encrypted with the master key (CiphertextBlob).
  3. The application encrypts the 200 GB locally with the clear version. It runs at CPU speed, with no network in the way.
  4. The application wipes the clear version from memory. This is the step that makes the system secure.
  5. It stores the encrypted data and the CiphertextBlob together. The blob is useless to anybody who cannot call kms:Decrypt on the master key.

The advantages of this design, all of them consequences of the same trick:

Advantage Why
Performance The data is encrypted locally at CPU speed, not at network speed
Cost One KMS call per file (or fewer), not per byte
No size limit The 4 KB limit only affects the data key, which takes 32 bytes
Cheap rotation Rotating the master key does not force you to re-encrypt the data (we see this later)
Traceability Every GenerateDataKey and every Decrypt lands in CloudTrail

Decryption, step by step

sequenceDiagram
    participant D as Disk
    participant A as Application / S3
    participant K as KMS

    Note over D,K: DECRYPTION
    D-->>A: encrypted data + CiphertextBlob
    A->>K: Decrypt(CiphertextBlob)
    K->>K: Checks the key policy<br/>and the caller's IAM permissions
    alt Authorised
        K-->>A: Plaintext (data key in the clear)
        A->>A: Decrypts the data locally
        A->>A: WIPES Plaintext
    else Not authorised
        K-->>A: AccessDeniedException
        Note over A: The data is unreadable<br/>even if you have the file
    end

Notice the critical point: to decrypt you do not have to say which key was used. The CiphertextBlob carries the master key identifier inside it, and KMS finds it on its own. That is why Decrypt does not need the KeyId parameter with symmetric keys.

And notice the else branch: whoever has the file but no permission on the key is left with noise. That is why an encrypted snapshot shared by mistake is still safe.

Client-side and server-side encryption

Server-side (SSE) Client-side (CSE)
Who encrypts AWS, on receiving the data Your application, before sending it
What AWS sees The data in the clear while processing Only encrypted data, never in the clear
Complexity Almost none: a checkbox High: you manage the process
Searches, indexes Work Do not work over what is encrypted
When 95 % of cases Extreme confidentiality against the provider

MercadoFresco uses server-side everywhere. Client-side encryption makes sense when the threat model includes the cloud provider itself or when a regulation requires it; for a fresh produce shop, the extra complexity is not worth it. If it were ever needed, there is the AWS Encryption SDK and the S3 encryption client, which implement the envelope for you.

SSE-S3, SSE-KMS and SSE-C

S3 offers three forms of server-side encryption. The difference lies in who controls the key:

SSE-S3 SSE-KMS SSE-C
Master key AWS owned Yours, in KMS Yours, sent on every request
Header AES256 aws:kms not aws:kms; -customer- headers
Access policy S3 IAM only S3 IAM and key policy You hold it
CloudTrail record No Yes, every use No
Cost Free 1 USD/month + requests Free in KMS
Rotation Automatic, invisible Configurable and auditable Manual, yours
Double authorisation No Yes No
When Non-sensitive data Personal data, compliance Almost never

The decisive row is double authorisation. With SSE-KMS, reading an object takes two permissions: s3:GetObject and kms:Decrypt on the key. If a bucket with personal data were accidentally exposed to the public, the attacker would get the encrypted object and could not open it, because they have no permission on the key. It is a real safety net that SSE-S3 does not give.

SSE-C, where you send the key on every request and AWS uses it and forgets it, forces you to manage key distribution yourself and leaves no record. It is mentioned for completeness.

The decision for MercadoFresco:

Bucket Encryption Reason
mercadofresco-copias-basedatos SSE-KMS with alias/mercadofresco-datos Personal data, GDPR, auditing
mercadofresco-catalogo-fotos SSE-KMS Consistency; the shop policy already requires it
mercadofresco-informes-analitica SSE-KMS Aggregated business data
mercadofresco-registros-web SSE-S3 High volume, no direct personal data
mercadofresco-tienda-web-desarrollo SSE-S3 Public static content

S3 Bucket Keys and the cost of requests

Here is an economic trap that sinks budgets. With nothing else in place, every object uploaded to or downloaded from a bucket with SSE-KMS generates a KMS call. mercadofresco-catalogo-fotos serves —before CloudFront— of the order of 2 million requests a month. At 0.03 USD per 10,000 requests, that is 6 USD a month in KMS calls alone, more than the key itself.

The solution is the S3 Bucket Key: S3 asks KMS for one bucket-level key, keeps it cached for a few hours and derives the individual object keys from it.

Without a bucket key With a bucket key
KMS calls One per object One every few hours
Cost reduction Up to 99 %
CloudTrail record One entry per object One entry per bucket key
Added cost None

The trade-off is the third row: you lose the per-object trace in CloudTrail. For mercadofresco-catalogo-fotos, with millions of public photos, it is an excellent trade. For mercadofresco-copias-basedatos, where you want to know exactly who decrypted which backup and when, you may want to leave it off: it is few requests a month and the trace is worth more than the saving.

Turn it on whenever you do not need the per-object trace. It is free and saves 99 %.

The key policy and its relationship with IAM

Here is the exception we announced in 04-01. In almost every service, within the same account it is enough for one of the two policies to grant. In KMS it is not. Every key has a key policy (a resource-based policy) and:

If the key policy does not allow it, nobody can use the key. Not the account administrator, not even root.

This has a consequence worth burning into memory: if you create a key from the CLI with a policy that includes nobody, the key is unusable forever and all you can do is schedule its deletion. AWS prevents this in the console, but not from the CLI.

There are two models:

  1. The key policy delegates to IAM. You include the statement "Principal": {"AWS": "arn:aws:iam::111122223333:root"} with "Action": "kms:*", which means "this account's IAM policies may grant access to this key". It is what the console does by default.
  2. The key policy grants directly, naming specific roles. More restrictive and more explicit.

MercadoFresco uses a mixed model: it delegates to IAM for administration but explicitly names whoever uses the key. This is the complete policy for alias/mercadofresco-datos:

{
  "Version": "2012-10-17",
  "Id": "politica-mercadofresco-datos",
  "Statement": [
    {
      "Sid": "PermitirQueIAMGobierneLaClave",
      "Effect": "Allow",
      "Principal": { "AWS": "arn:aws:iam::111122223333:root" },
      "Action": "kms:*",
      "Resource": "*"
    },
    {
      "Sid": "MartaAdministraLaClavePeroNoLaUsa",
      "Effect": "Allow",
      "Principal": { "AWS": "arn:aws:iam::111122223333:user/marta" },
      "Action": [
        "kms:Create*", "kms:Describe*", "kms:Enable*", "kms:List*", "kms:Put*",
        "kms:Update*", "kms:Revoke*", "kms:Disable*", "kms:Get*", "kms:Delete*",
        "kms:TagResource", "kms:UntagResource", "kms:ScheduleKeyDeletion", "kms:CancelKeyDeletion"
      ],
      "Resource": "*"
    },
    {
      "Sid": "LosRolesDeAplicacionUsanLaClave",
      "Effect": "Allow",
      "Principal": {
        "AWS": [
          "arn:aws:iam::111122223333:role/rol-mercadofresco-tienda",
          "arn:aws:iam::111122223333:role/rol-lambda-miniaturas"
        ]
      },
      "Action": [
        "kms:Encrypt", "kms:Decrypt", "kms:ReEncrypt*",
        "kms:GenerateDataKey*", "kms:DescribeKey"
      ],
      "Resource": "*",
      "Condition": {
        "StringEquals": {
          "kms:ViaService": ["s3.eu-west-1.amazonaws.com"]
        }
      }
    },
    {
      "Sid": "PermitirQueRDSYEBSUsenLaClaveMedianteConcesiones",
      "Effect": "Allow",
      "Principal": { "AWS": "arn:aws:iam::111122223333:root" },
      "Action": "kms:CreateGrant",
      "Resource": "*",
      "Condition": {
        "Bool": { "kms:GrantIsForAWSResource": "true" },
        "StringEquals": {
          "kms:ViaService": ["rds.eu-west-1.amazonaws.com", "ec2.eu-west-1.amazonaws.com"]
        }
      }
    },
    {
      "Sid": "ProhibirElUsoFueraDeEuWest1",
      "Effect": "Deny",
      "Principal": "*",
      "Action": "kms:*",
      "Resource": "*",
      "Condition": {
        "StringNotEquals": { "aws:RequestedRegion": "eu-west-1" }
      }
    }
  ]
}

Statement by statement:

  • PermitirQueIAMGobierneLaClave: the "switch" that makes IAM policies count for this key. Without it, no identity-based policy would have any effect on it. It is what stops you locking yourself out of your own key.
  • MartaAdministraLaClavePeroNoLaUsa: Marta can enable, disable, tag, change the policy and schedule deletion... but kms:Decrypt and kms:Encrypt do not appear. That is separation of duties: whoever administers the key cannot read the data it protects. It is a standard audit requirement and here you can see it implemented in two lines.
  • LosRolesDeAplicacionUsanLaClave: the two roles we formalised in 04-01 can encrypt and decrypt, but only through S3, thanks to kms:ViaService. If somebody steals the instance's credentials and calls kms:Decrypt directly against a blob they obtained some other way, KMS rejects it.
  • PermitirQueRDSYEBSUsenLaClaveMedianteConcesiones: RDS and EBS do not call the key directly; they create grants on your behalf. The kms:GrantIsForAWSResource condition limits that ability to AWS services, not to people.
  • ProhibirElUsoFueraDeEuWest1: a data residency barrier. It fits the GDPR argument about keeping processing inside the EU.

And the shop role additionally needs the same KMS permissions in its own identity-based policy —remember: within the same account one grant is enough, unless the resource one denies, but in KMS the resource one must always allow. In practice you write both.

kms:ViaService and the encryption context

Two mechanisms that refine control and that come up constantly in real policies.

kms:ViaService. Restricts use of the key to requests arriving through a specific service, and only when the service is acting on behalf of the principal. Typical values: s3.eu-west-1.amazonaws.com, rds.eu-west-1.amazonaws.com, ec2.eu-west-1.amazonaws.com, secretsmanager.eu-west-1.amazonaws.com. It is the most useful condition in KMS: it turns a generic key into an "S3 only" key.

Encryption context. A dictionary of key-value pairs associated with an encryption operation. It is not secret —it appears in the CloudTrail logs in the clear— but it is authenticated: if you do not supply exactly the same context when decrypting, the operation fails.

context = {"proyecto": "mercadofresco", "componente": "copias", "entorno": "produccion"}

It serves two purposes:

  1. Readable auditing. In CloudTrail you see "something from the copias component was decrypted" instead of an opaque UUID.
  2. Granular authorisation. The key policy can be conditioned on a specific context:
{
  "Sid": "SoloDescifrarCopias",
  "Effect": "Allow",
  "Principal": { "AWS": "arn:aws:iam::111122223333:role/rol-restauracion" },
  "Action": "kms:Decrypt",
  "Resource": "*",
  "Condition": {
    "StringEquals": { "kms:EncryptionContext:componente": "copias" }
  }
}

That role can decrypt database backups and nothing else, even though everything is encrypted with the same key. S3 automatically uses the bucket or object ARN as the context, so you already benefit from this without doing anything.

Grants

A grant is a temporary, granular permission on a key, designed for services and processes, not for people:

Key policy Grant
Format JSON API call
Duration Permanent until edited Removed with RetireGrant
Granularity Principal + action + condition Principal + specific operations + context
Who uses them Humans and roles AWS services, mostly

When you create an encrypted RDS instance, RDS creates a grant on the key so that it can decrypt the volume at every start-up, every failover and every snapshot restore. That is why the key policy had to allow kms:CreateGrant with kms:GrantIsForAWSResource.

A very important practical consequence: if you retire that grant, the database stops starting. And if the grant is retired while the database is stopped, it will not come back. Do not touch the grants that services create.

aws kms list-grants --key-id alias/mercadofresco-datos \
  --query 'Grants[].[GranteePrincipal,Operations[0],Name]' --output table \
  --profile mercadofresco-dev

Automatic rotation and what it really means

Annual rotation can be switched on with a single call:

aws kms enable-key-rotation --key-id alias/mercadofresco-datos --profile mercadofresco-dev
aws kms get-key-rotation-status --key-id alias/mercadofresco-datos --profile mercadofresco-dev

And now the part almost everybody gets wrong. When KMS rotates a key:

  • It generates new cryptographic material and ties it to the same logical key and the same ARN.
  • It keeps all the earlier material, indefinitely.
  • The new material is used for new encryption operations.
  • Data already encrypted is not re-encrypted. It keeps its old material, which KMS finds itself.

In other words: rotation is transparent and free, but it does not "rejuvenate" old data. If an attacker had somehow obtained the old material, the data encrypted with it is still compromised. To genuinely renew the data you have to re-encrypt it, copying the objects over themselves or using ReEncrypt.

KMS automatic rotation Real re-encryption of the data
What changes The material for new operations The data already stored
Cost Free KMS requests + time
Compliance Covers "annual key rotation" Covers "renewal after an incident"
Effort One command A batch process

Since 2024 the rotation period is configurable between 90 and 2,560 days with --rotation-period-in-days. For MercadoFresco the default annual rotation is enough and it is what most audits expect to see.

Encrypting mercadofresco-copias-basedatos

First, create the key with the policy from the previous section:

aws kms create-key \
  --description "MercadoFresco personal data key (GDPR)" \
  --key-usage ENCRYPT_DECRYPT \
  --key-spec SYMMETRIC_DEFAULT \
  --policy file:///tmp/politica-clave.json \
  --tags TagKey=Proyecto,TagValue=mercadofresco TagKey=Entorno,TagValue=produccion \
         TagKey=Componente,TagValue=datos TagKey=Propietario,TagValue=marta \
         TagKey=CentroCoste,TagValue=tecnologia \
  --profile mercadofresco-dev

# Returns KeyId: 1234abcd-12ab-34cd-56ef-1234567890ab
aws kms create-alias \
  --alias-name alias/mercadofresco-datos \
  --target-key-id 1234abcd-12ab-34cd-56ef-1234567890ab \
  --profile mercadofresco-dev

aws kms enable-key-rotation --key-id alias/mercadofresco-datos --profile mercadofresco-dev

Next, default encryption on the bucket. Note that here we do not turn on the bucket key, because we want the per-object trace:

aws s3api put-bucket-encryption \
  --bucket mercadofresco-copias-basedatos \
  --server-side-encryption-configuration '{
    "Rules": [{
      "ApplyServerSideEncryptionByDefault": {
        "SSEAlgorithm": "aws:kms",
        "KMSMasterKeyID": "arn:aws:kms:eu-west-1:111122223333:alias/mercadofresco-datos"
      },
      "BucketKeyEnabled": false
    }]
  }' \
  --profile mercadofresco-dev

On the photo catalogue, by contrast, we do turn it on:

aws s3api put-bucket-encryption \
  --bucket mercadofresco-catalogo-fotos \
  --server-side-encryption-configuration '{
    "Rules": [{
      "ApplyServerSideEncryptionByDefault": {
        "SSEAlgorithm": "aws:kms",
        "KMSMasterKeyID": "arn:aws:kms:eu-west-1:111122223333:alias/mercadofresco-datos"
      },
      "BucketKeyEnabled": true
    }]
  }' \
  --profile mercadofresco-dev

And finally, the bucket policy that rejects any unencrypted upload, so that default encryption does not depend on nobody switching it off:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "RechazarSubidasSinKMS",
      "Effect": "Deny",
      "Principal": "*",
      "Action": "s3:PutObject",
      "Resource": "arn:aws:s3:::mercadofresco-copias-basedatos/*",
      "Condition": {
        "StringNotEquals": {
          "s3:x-amz-server-side-encryption": "aws:kms"
        }
      }
    },
    {
      "Sid": "RechazarOtraClave",
      "Effect": "Deny",
      "Principal": "*",
      "Action": "s3:PutObject",
      "Resource": "arn:aws:s3:::mercadofresco-copias-basedatos/*",
      "Condition": {
        "StringNotEqualsIfExists": {
          "s3:x-amz-server-side-encryption-aws-kms-key-id":
            "arn:aws:kms:eu-west-1:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab"
        }
      }
    }
  ]
}

Important: default encryption does not encrypt retroactively. The objects already in the bucket stay exactly as they were. To encrypt them you have to copy them over themselves:

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

With versioning enabled this creates a new encrypted version and leaves the old one unencrypted. You have to delete the old versions afterwards, or the work is worthless. It is a detail that is frequently forgotten and that an audit spots straight away.

Encrypting EBS volumes and snapshots

The simplest thing: turn on EBS default encryption in the region. From that moment on, every new volume is encrypted without anybody having to remember.

aws ec2 enable-ebs-encryption-by-default --region eu-west-1 --profile mercadofresco-dev
aws ec2 modify-ebs-default-kms-key-id \
  --kms-key-id alias/mercadofresco-datos --region eu-west-1 --profile mercadofresco-dev
aws ec2 get-ebs-default-kms-key-id --region eu-west-1 --profile mercadofresco-dev

For a volume that already exists unencrypted —like mercadofresco-fotos-01, created in 02-02— you cannot encrypt it in place. The procedure is snapshot, encrypted copy, new volume:

# 1. Snapshot of the unencrypted volume
aws ec2 create-snapshot --volume-id vol-0abc123 \
  --description "mercadofresco-fotos-01 before encryption" \
  --profile mercadofresco-dev

# 2. Copy the snapshot, ENCRYPTING along the way
aws ec2 copy-snapshot \
  --source-region eu-west-1 --source-snapshot-id snap-0abc123 \
  --encrypted --kms-key-id alias/mercadofresco-datos \
  --description "mercadofresco-fotos-01 encrypted" \
  --profile mercadofresco-dev

# 3. Create the new volume from the encrypted snapshot
aws ec2 create-volume --snapshot-id snap-0def456 \
  --availability-zone eu-west-1a --volume-type gp3 \
  --tag-specifications 'ResourceType=volume,Tags=[{Key=Name,Value=mercadofresco-fotos-01},
      {Key=Proyecto,Value=mercadofresco},{Key=Entorno,Value=produccion}]' \
  --profile mercadofresco-dev

# 4. Stop the instance, detach the old volume, attach the new one, start

The trick is in step 2: copy-snapshot --encrypted is the only EBS operation that turns something unencrypted into something encrypted. Remember it: it is the piece missing from almost every attempt.

And a rule that saves you scares: an encrypted volume only produces encrypted snapshots, and an encrypted snapshot only produces encrypted volumes. The property is inherited and cannot be removed.

Encrypting mercadofresco-pedidos, which already exists unencrypted

This is the most delicate operation of the module, because it involves downtime for the production database. RDS does not allow encrypting an existing instance. The route is:

flowchart TD
    A["mercadofresco-pedidos<br/>NOT encrypted (production)"] --> B["1. Manual snapshot<br/>mercadofresco-pedidos-precifrado"]
    B --> C["2. copy-db-snapshot --kms-key-id<br/>alias/mercadofresco-datos"]
    C --> D["3. restore-db-instance-from-db-snapshot<br/>mercadofresco-pedidos-cifrada"]
    D --> E["4. Verify: data, parameters,<br/>subnet group, SG"]
    E --> F["5. Maintenance window:<br/>stop writes"]
    F --> G["6. Rename: the old one to -antigua,<br/>the new one to mercadofresco-pedidos"]
    G --> H["7. Re-enable Multi-AZ and the replica<br/>mercadofresco-pedidos-lectura"]
    H --> I["8. Keep the old one 7 days,<br/>then delete"]
# 1 and 2
aws rds create-db-snapshot \
  --db-instance-identifier mercadofresco-pedidos \
  --db-snapshot-identifier mercadofresco-pedidos-precifrado \
  --profile mercadofresco-dev

aws rds copy-db-snapshot \
  --source-db-snapshot-identifier mercadofresco-pedidos-precifrado \
  --target-db-snapshot-identifier mercadofresco-pedidos-cifrado \
  --kms-key-id alias/mercadofresco-datos \
  --profile mercadofresco-dev

# 3
aws rds restore-db-instance-from-db-snapshot \
  --db-instance-identifier mercadofresco-pedidos-cifrada \
  --db-snapshot-identifier mercadofresco-pedidos-cifrado \
  --db-subnet-group-name sng-mercadofresco-datos \
  --db-parameter-group-name pg16-mercadofresco \
  --multi-az \
  --no-publicly-accessible \
  --profile mercadofresco-dev

Four warnings from the field:

  • The restore does not preserve the parameter group, the security groups or the Multi-AZ configuration. You have to specify them explicitly, as in the command above, or the new instance will start with the default values and end up misconfigured.
  • The endpoint name changes. If the application points at the RDS DNS name, it has to be updated. That is why in 03-05 we created the private zone interno.mercadofresco.example: if the shop connects to pedidos.interno.mercadofresco.example, changing one CNAME is enough.
  • The replica mercadofresco-pedidos-lectura does not migrate. It has to be recreated from the new instance.
  • Do the full rehearsal in development first. Marta does this migration on a Tuesday night, never a Thursday: the Friday peak is no time to discover surprises.

Sharing an encrypted snapshot with another account

A snapshot encrypted with the AWS managed key (aws/rds) cannot be shared. Only those encrypted with a customer managed key can, and two permissions are needed:

# 1. Share the snapshot with the target account
aws rds modify-db-snapshot-attribute \
  --db-snapshot-identifier mercadofresco-pedidos-cifrado \
  --attribute-name restore --values-to-add 444455556666 \
  --profile mercadofresco-dev
{
  "Sid": "PermitirQueLaCuentaDeAuditoriaDescifreElSnapshot",
  "Effect": "Allow",
  "Principal": { "AWS": "arn:aws:iam::444455556666:root" },
  "Action": ["kms:Decrypt", "kms:DescribeKey", "kms:CreateGrant"],
  "Resource": "*",
  "Condition": {
    "StringEquals": { "kms:ViaService": "rds.eu-west-1.amazonaws.com" }
  }
}

In other words: share the snapshot and give access to the key. If you only do the first, the other account will see the snapshot and will not be able to restore it. And do not forget that the target account also has to allow it in its own identity-based policies.

This, incidentally, is the mechanism that makes accidental exposure safe: sharing a snapshot without sharing the key reveals absolutely nothing.

KMS from boto3

The typical use case —encrypting a file of arbitrary size with the envelope, by hand:

import boto3
import os
from cryptography.fernet import Fernet   # local example only
import base64

session = boto3.Session(profile_name="mercadofresco-dev", region_name="eu-west-1")
kms = session.client("kms")

KEY = "alias/mercadofresco-datos"
CONTEXT = {"proyecto": "mercadofresco", "componente": "copias"}


def encrypt_file(source_path: str, target_path: str) -> None:
    """Encrypts a file of any size using envelope encryption."""
    # 1. Ask for a data key: BOTH versions come back
    response = kms.generate_data_key(
        KeyId=KEY,
        KeySpec="AES_256",
        EncryptionContext=CONTEXT,
    )
    key_in_the_clear = response["Plaintext"]      # 32 bytes, to encrypt here
    encrypted_key = response["CiphertextBlob"]    # stored next to the file

    # 2. Encrypt locally. The data NEVER travels to KMS.
    f = Fernet(base64.urlsafe_b64encode(key_in_the_clear))
    with open(source_path, "rb") as source:
        encrypted_data = f.encrypt(source.read())

    # 3. Store the encrypted key + the encrypted data in the same file
    with open(target_path, "wb") as target:
        target.write(len(encrypted_key).to_bytes(4, "big"))
        target.write(encrypted_key)
        target.write(encrypted_data)

    # 4. Wipe the clear version from memory
    del key_in_the_clear


def decrypt_file(source_path: str, target_path: str) -> None:
    with open(source_path, "rb") as source:
        length = int.from_bytes(source.read(4), "big")
        encrypted_key = source.read(length)
        encrypted_data = source.read()

    # KMS works out the master key from the blob itself: no KeyId needed
    response = kms.decrypt(
        CiphertextBlob=encrypted_key,
        EncryptionContext=CONTEXT,   # must match EXACTLY
    )
    f = Fernet(base64.urlsafe_b64encode(response["Plaintext"]))

    with open(target_path, "wb") as target:
        target.write(f.decrypt(encrypted_data))


# Direct encryption, only for data smaller than 4 KB
small = kms.encrypt(KeyId=KEY, Plaintext=b"short piece of data", EncryptionContext=CONTEXT)
print(kms.decrypt(CiphertextBlob=small["CiphertextBlob"],
                  EncryptionContext=CONTEXT)["Plaintext"])

Points to take away from the example:

  • generate_data_key returns both versions of the same key. That is the essence of the envelope.
  • The encrypted file is self-contained: it carries the encrypted key inside. It can be moved anywhere and only somebody with permission on alias/mercadofresco-datos will open it.
  • decrypt receives no KeyId: the blob carries it inside.
  • The EncryptionContext must match character for character on decryption. If not, InvalidCiphertext.
  • In production you do not use Fernet by hand: the AWS Encryption SDK does all this correctly, including secure memory wiping and the message format. The example is to show the mechanics.

Limits, quotas and the real cost for MercadoFresco

Item Price
Customer managed key 1.00 USD per key per month (pro rata)
Symmetric requests 0.03 USD per 10,000
RSA-2048 requests 0.03 USD per 10,000
ECC / RSA >2048 requests 0.15 USD per 10,000
AWS managed and AWS owned keys Free (requests are charged)
Free tier 20,000 requests a month, always
Custom key store (CloudHSM) ~1.50 USD/hour per HSM, separately

Request-per-second quotas in eu-west-1: of the order of 50,000 for symmetric Decrypt, GenerateDataKey and Encrypt, and far lower (a few hundred) for asymmetric operations. They are shared across every key in the account, so an aggressive batch process can trigger a ThrottlingException across the whole account. The SDKs retry with exponential backoff, but it is worth knowing.

The calculation for MercadoFresco:

Item Quantity Monthly cost
Key alias/mercadofresco-datos 1 1.00 USD
mercadofresco-catalogo-fotos with a bucket key ~2,000,000 requests → ~700 calls 0.00 USD (free tier)
mercadofresco-copias-basedatos without a bucket key 60 backups × 2 (upload + verify) = 120 0.00 USD
EBS and RDS (grants and start-ups) ~500 0.00 USD
Total 1.00 USD/month

One dollar a month to encrypt every piece of personal data in the company, with an audit trail of every use. It is by far the best cost-benefit ratio in the whole course.

The contrast is useful: without the bucket key, those 2 million requests would have generated 2 million KMS calls, some 6 USD a month. That is not real money, but at a scale of 100 million requests it would be 300 USD a month for not having ticked a box.

ACM is not KMS

A frequent confusion, because both talk about keys:

AWS KMS AWS Certificate Manager
Protects Data at rest Data in transit
Object Symmetric encryption keys X.509 certificates
Cost 1 USD/key/month Free for public certificates
In MercadoFresco alias/mercadofresco-datos ALB and CloudFront certificates (03-03, 03-05)
Renewal Annual rotation of the material Automatic renewal via DNS

They are independent services solving different halves of the same problem. The mercadofresco.example certificate we validated with a CNAME in 03-05 has nothing to do with alias/mercadofresco-datos.

Deleting a key: the 7 to 30 day wait

A KMS key cannot be deleted immediately, and that is deliberate: deleting it would irreversibly destroy every piece of data encrypted with it. All you can do is schedule the deletion, with a waiting period of between 7 and 30 days.

# Before anything else: is anybody using it? Check CloudTrail (05-03)
aws kms schedule-key-deletion \
  --key-id alias/mercadofresco-datos --pending-window-in-days 30 \
  --profile mercadofresco-dev

# Cancel while it is still pending
aws kms cancel-key-deletion --key-id 1234abcd-12ab-34cd-56ef-1234567890ab \
  --profile mercadofresco-dev

A far more sensible alternative for testing the impact is to disable the key: it is instantly reversible and produces exactly the same effect as deleting it —everything encrypted stops being readable— but without destroying anything.

aws kms disable-key --key-id alias/mercadofresco-datos --profile mercadofresco-dev
aws kms enable-key  --key-id alias/mercadofresco-datos --profile mercadofresco-dev

Disable it for 30 days, check that nothing breaks and only then schedule the deletion: that is the correct procedure. And bear in mind that while the key is disabled you go on paying its monthly dollar.

Common Mistakes and Tips

Writing a key policy that locks everybody out. It is the irreversible mistake of KMS. If you create the key from the CLI with a policy that does not include "Principal": {"AWS": "arn:aws:iam::111122223333:root"} with kms:*, the key is unusable and all you can do is schedule its deletion. Always include that statement.

Believing that a bucket's default encryption encrypts what was already there. It does not. The existing objects stay unencrypted until you copy them over themselves, and with versioning enabled you also have to purge the old versions.

Forgetting kms:Decrypt on the role. The symptom is confusing: s3:GetObject is granted, the object exists and AccessDenied still arrives. With SSE-KMS you need two permissions. The first thing to check on an AccessDenied in an encrypted bucket is always KMS.

Not turning on the bucket key on high-traffic buckets. It multiplies the KMS cost by the number of requests. Turn it on unless you need the per-object trace.

Using Encrypt for large files. It fails with ValidationException past 4 KB. That is what GenerateDataKey and the envelope are for.

Confusing disabling with deleting. Disabling is reversible and still costs 1 USD a month; deleting is irreversible and destroys the data. Always start by disabling.

Retiring a grant created by RDS or EBS. The database stops starting. The grants that services create are not to be touched.

Tip: use aliases, never UUIDs, in the code. alias/mercadofresco-datos is readable and lets you change the underlying key without a deployment. In policies, on the other hand, use the key ARN: it is unambiguous and does not depend on where the alias points.

Tip: apply separation of duties from day one. Whoever administers the key should not be able to decrypt the data, as in the alias/mercadofresco-datos policy. It costs two lines and it is the first thing an auditor asks about.

Tip: turn on EBS default encryption in the region. One command, free, and from then on nobody can create an unencrypted volume by accident.

Tip: put an alarm on kms:Decrypt calls. An anomalous spike in decryptions is one of the earliest signals of data exfiltration. We will build it with CloudWatch in 05-01 on top of the CloudTrail events of 05-03.

Exercises

Exercise 1: designing the policy for a new key

MercadoFresco creates a second key, alias/mercadofresco-analitica, to encrypt mercadofresco-informes-analitica. Requirements:

  • Marta administers the key but cannot decrypt.
  • Sara (user sara) can decrypt reports, but only through S3 and only if the encryption context includes componente=informes.
  • A role rol-mercadofresco-etl can encrypt and decrypt with no context restriction, also only through S3.
  • Nobody can use the key outside eu-west-1.
  • The account's IAM policies must be able to grant access.

Write the complete key policy.

Exercise 2: calculating the cost and deciding about the bucket key

MercadoFresco opens in Portugal and the catalogue starts serving 40 million requests a month, of which CloudFront absorbs 90 % (only 10 % reaches S3). In addition, the bucket mercadofresco-copias-basedatos receives 3 backups a day and mercadofresco-informes-analitica generates 50,000 objects a month.

Work out the monthly KMS cost in two scenarios —with and without the bucket key on the catalogue— using 2 customer managed keys, and decide which configuration you would recommend for each bucket, justifying the trade-off between cost and traceability.

Exercise 3: diagnosing an encryption failure

Luis deploys a new version of the shop. The photos read fine, but when generating a thumbnail the function mercadofresco-generar-miniaturas fails with:

An error occurred (AccessDenied) when calling the PutObject operation:
User: arn:aws:sts::111122223333:assumed-role/rol-lambda-miniaturas/mercadofresco-generar-miniaturas
is not authorized to perform: kms:GenerateDataKey on resource:
arn:aws:kms:eu-west-1:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab
because no identity-based policy allows the kms:GenerateDataKey action

Answer: (a) why does it fail on writing if reading worked?; (b) which two different places do you have to look at, and which one has the problem according to the message?; (c) write the minimum fix; (d) if the error had said with an explicit deny in a resource-based policy, what would you suspect, knowing that the key policy includes kms:ViaService?

Solutions

Solution 1

{
  "Version": "2012-10-17",
  "Id": "politica-mercadofresco-analitica",
  "Statement": [
    {
      "Sid": "PermitirQueIAMGobierneLaClave",
      "Effect": "Allow",
      "Principal": { "AWS": "arn:aws:iam::111122223333:root" },
      "Action": "kms:*",
      "Resource": "*"
    },
    {
      "Sid": "MartaAdministra",
      "Effect": "Allow",
      "Principal": { "AWS": "arn:aws:iam::111122223333:user/marta" },
      "Action": [
        "kms:Describe*", "kms:Get*", "kms:List*", "kms:Enable*", "kms:Disable*",
        "kms:Update*", "kms:Put*", "kms:Revoke*", "kms:TagResource", "kms:UntagResource",
        "kms:ScheduleKeyDeletion", "kms:CancelKeyDeletion"
      ],
      "Resource": "*"
    },
    {
      "Sid": "SaraDescifraSoloInformes",
      "Effect": "Allow",
      "Principal": { "AWS": "arn:aws:iam::111122223333:user/sara" },
      "Action": ["kms:Decrypt", "kms:DescribeKey"],
      "Resource": "*",
      "Condition": {
        "StringEquals": {
          "kms:ViaService": "s3.eu-west-1.amazonaws.com",
          "kms:EncryptionContext:componente": "informes"
        }
      }
    },
    {
      "Sid": "ElProcesoETLCifraYDescifra",
      "Effect": "Allow",
      "Principal": { "AWS": "arn:aws:iam::111122223333:role/rol-mercadofresco-etl" },
      "Action": ["kms:Encrypt", "kms:Decrypt", "kms:GenerateDataKey*", "kms:DescribeKey"],
      "Resource": "*",
      "Condition": {
        "StringEquals": { "kms:ViaService": "s3.eu-west-1.amazonaws.com" }
      }
    },
    {
      "Sid": "ProhibirFueraDeEuWest1",
      "Effect": "Deny",
      "Principal": "*",
      "Action": "kms:*",
      "Resource": "*",
      "Condition": {
        "StringNotEquals": { "aws:RequestedRegion": "eu-west-1" }
      }
    }
  ]
}

Key points: in Marta's statement no kms:Encrypt, kms:Decrypt or kms:GenerateDataKey appears —separation of duties—; Sara only has Decrypt, never Encrypt, and with a double condition; and the final Deny applies to "Principal": "*", which means it applies to Marta and to root as well.

Solution 2

KMS requests per bucket:

  • Catalogue: 40,000,000 × 10 % = 4,000,000 S3 requests a month.
    • Without a bucket key: 4,000,000 KMS calls.
    • With a bucket key: roughly one every few hours, of the order of 250 a month.
  • Backups: 3 a day × 30 = 90 objects → ~180 calls (write + verify).
  • Reports: 50,000 objects → 50,000 calls without a bucket key, ~250 with it.

Scenario A, no bucket key anywhere:

  • Requests: 4,000,000 + 180 + 50,000 = 4,050,180. Less the 20,000 free tier = 4,030,180.
  • Cost: 4,030,180 ÷ 10,000 × 0.03 = 12.09 USD
  • Keys: 2 × 1.00 = 2.00 USD
  • Total: 14.09 USD/month

Scenario B, with a bucket key on catalogue and reports, without it on backups:

  • Requests: 250 + 180 + 250 = 680, well below the free tier of 20,000.
  • Request cost: 0.00 USD
  • Keys: 2.00 USD
  • Total: 2.00 USD/month

Recommendation. A saving of 12.09 USD a month, some 145 USD a year, and it grows linearly with scale. Proposed configuration:

Bucket Bucket key Justification
mercadofresco-catalogo-fotos Yes Millions of requests, public photos, the per-object trace adds nothing
mercadofresco-informes-analitica Yes 50,000 objects a month, aggregated data with no personal identifiers
mercadofresco-copias-basedatos No 90 objects a month: the saving is zero and the trace of who decrypted which backup of personal data is exactly what a GDPR audit will ask for

The general reasoning: turn the bucket key on unless the volume is low and per-object traceability has compliance value.

Solution 3

(a) Because reading and writing require different KMS permissions. Downloading an object encrypted with SSE-KMS needs kms:Decrypt; uploading a new one needs kms:GenerateDataKey, because S3 has to ask for a new data key to encrypt it. In 04-01 we granted both to rol-lambda-miniaturas, so what has happened here is that somebody has deployed a version of the policy with only kms:Decrypt, or the bucket has changed key.

(b) You have to look in two places: the role's identity-based policy and the key policy of alias/mercadofresco-datos. The message settles it: because no identity-based policy allows points unambiguously at the role's identity-based policy, and it also indicates an implicit deny —an Allow is missing— not an explicit Deny.

(c) Add the missing action to rol-lambda-miniaturas:

{
  "Sid": "UsarLaClaveDelBucket",
  "Effect": "Allow",
  "Action": ["kms:Decrypt", "kms:GenerateDataKey"],
  "Resource": "arn:aws:kms:eu-west-1:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab",
  "Condition": {
    "StringEquals": { "kms:ViaService": "s3.eu-west-1.amazonaws.com" }
  }
}

The minimum is kms:GenerateDataKey; kms:Decrypt is included because the function also reads the original. kms:Encrypt is not needed: with envelope encryption, S3 never uses it.

(d) I would suspect the kms:ViaService condition. If the function had started calling KMS directly —because somebody changed the code to encrypt before uploading, for instance, instead of letting S3 do it— the request would no longer arrive "through S3", the condition would not be met and the key policy would reject it. It could also be the aws:RequestedRegion Deny if the boto3 client had been created pointing at another region. In both cases the fix is not to widen permissions, but to correct how the call is being made.

Conclusion

alias/mercadofresco-datos has stopped being a name. It is now a customer managed key, with a policy of its own, annual rotation switched on, project tags and an explicit separation of duties written into it: Marta administers the key but cannot decrypt with it; rol-mercadofresco-tienda and rol-lambda-miniaturas use it but only through S3, thanks to kms:ViaService; RDS and EBS can create grants because they are AWS services; and nobody, not even the root user, can use it outside eu-west-1.

You understand the mechanism that holds it all up, envelope encryption: KMS generates a data key and returns it in two formats —in the clear and encrypted with the master key—, the application encrypts locally at CPU speed, wipes the clear version and stores the CiphertextBlob next to the data. That is why large data never travels to KMS, why the 4 KB limit of Encrypt is not a real problem, and why Decrypt does not need you to tell it which key to use. You also know what automatic rotation does and does not do: it changes the material for new operations, keeps the old material forever and does not re-encrypt what is already stored.

You can tell SSE-S3, SSE-KMS and SSE-C apart, and you know that the real advantage of SSE-KMS is double authorisations3:GetObject and kms:Decrypt—, which turns an accidental exposure into a scare rather than a breach. You know that S3 Bucket Keys cut KMS calls by up to 99 % at the price of losing the per-object trace, and you have decided to enable them on mercadofresco-catalogo-fotos and not on mercadofresco-copias-basedatos, where the trace is worth more than the saving.

In practical terms, MercadoFresco has encrypted the backups bucket with a bucket policy that rejects any upload without KMS, has enabled EBS default encryption in eu-west-1, knows how to convert an unencrypted volume using copy-snapshot --encrypted —the only operation that makes that jump— and has migrated mercadofresco-pedidos to an encrypted instance by the route of snapshot, encrypted copy and restore, without forgetting that the restore preserves neither the parameter group nor Multi-AZ and that the endpoint changes. All of it for 1.00 USD a month, the best cost-benefit ratio in the course. And you know that deleting a key is irreversible, that the sensible route is to disable it first and that the 7 to 30 day wait exists precisely to save you.

There remains, however, the most uncomfortable hole of all, and it is one of those that cryptography does not fix. The password of the mfadmin user of mercadofresco-pedidos is still where it should not be: it went through the user data of the template lt-mercadofresco-tienda, it lives in a configuration file on the server and there is a copy in the .env Luis shared over chat when he set up the development environment. It makes no difference that the database is encrypted with AES-256: anybody who reads that file gets in with valid credentials, and encryption, as we saw in the first table of this lesson, does not protect against that. In lesson 04-03, "Secrets Manager and Parameter Store", that password will finally leave where it is: we will see the difference between configuration and secret, Parameter Store for the former and Secrets Manager for the latter, how it is consumed from the application without ever writing it into a file again, and above all the automatic rotation every 30 days with a managed Lambda, with its createSecret / setSecret / testSecret / finishSecret cycle — which rests, precisely, on the key you have just built.

© Copyright 2026. All rights reserved