Profile
Back to NewsBack
Dev.to 7 min
Reader Mode
Day 41: EXPOSE Does Not Publish, and a KMS Key Has No Name

Day 41: EXPOSE Does Not Publish, and a KMS Key Has No Name

16 hours ago

Three things today looked like they did something and did not. A comment at the end of a Dockerfile line is not a comment. EXPOSE does not expose anything. And the name you give a KMS key is not stored on the key.

None of these are bugs. All three are places where the obvious reading is wrong and the failure is quiet.

One Docker task, one AWS task. Write a Dockerfile that runs Apache on port 5004, then encrypt a file with KMS and prove the round trip is lossless. The tasks come from the KodeKloud Engineer platform.

The comment that is not a comment

Here is the Dockerfile, and the formatting is load-bearing:

FROM ubuntu:24.04

# Stop apt from opening interactive prompts during the build
ENV DEBIAN_FRONTEND=noninteractive

# Install apache2 and clean the package lists in the same layer
RUN apt-get update \
    && apt-get install -y apache2 \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/*

# Move Apache to the required port, in both files that mention it
RUN sed -i 's/Listen 80/Listen 5004/' /etc/apache2/ports.conf \
    && sed -i 's/<VirtualHost \*:80>/<VirtualHost *:5004>/' /etc/apache2/sites-enabled/000-default.conf

EXPOSE 5004

CMD ["apachectl", "-D", "FOREGROUND"]

Every comment sits on its own line, and it has to. Docker's documentation says a # at the start of a line is a comment, and that a # marker anywhere else in a line is treated as an argument. So this:

ENV DEBIAN_FRONTEND=noninteractive # make it noninteractive

is not an ENV instruction with a helpful note attached. The # and the words after it get passed to ENV as further arguments, and the build breaks instead of doing the thing it visibly describes. It is a comment in every language you are used to and an argument in this one.

Three more decisions in that file that are not stylistic.

apt-get update and apt-get install share a RUN because each instruction is a cached layer. Split them and a later build can reuse a stale update layer, then install against a package index that no longer matches the mirrors. The symptom is a 404 on a package that definitely exists.

apt-get clean and the rm -rf are in that same RUN for a different reason. Deleting files in a later layer does not shrink the image, because the earlier layer still contains them. Layers only add.

And apachectl -D FOREGROUND is the whole reason the container stays up. A container lives exactly as long as PID 1. Apache's normal startup daemonises and the parent exits, which Docker reads as the container finishing, so it stops immediately with exit code 0 and nothing to explain it. Whatever you put in CMD has to run in the foreground.

Then the one in the title:

EXPOSE 5004

That publishes nothing. Docker documents it as describing which ports the application listens on: documentation for whoever runs the image. The port only opens because of this:

docker run -d -p 5004:5004 nautilus:latest_version
curl http://localhost:5004

An image with EXPOSE 5004 and a docker run without -p gives you a container that is running, correct, and unreachable, which is the same shape of problem as Day 36.

A key with no name

The AWS task was to create a symmetric KMS key called devops-KMS-Key, encrypt a file with it, and prove the decryption is byte-identical.

aws kms create-key has no --name parameter. It returns a UUID. The only fields on the key itself are a free-text description and tags, neither of which is addressable. What everyone calls the key's name is an alias, which is a separate resource pointing at the key:

aws kms create-alias --alias-name alias/devops-KMS-Key --target-key-id $KEY_ID

The alias/ prefix is mandatory and part of the name, not a path convention. --alias-name devops-KMS-Key is rejected.

The console blurs this: the Alias field sits next to the description in the create-key wizard and reads like a name attribute, so people go looking for the equivalent CLI flag. There isn't one. Two API calls, always.

The indirection is the point, within limits. An alias identifies the key in cryptographic operations, DescribeKey and GetPublicKey, and AWS is explicit that it is not a valid identifier in the other KMS calls: scheduling the key for deletion, for one, still wants the ID. Inside that range you can repoint the alias at a different key later and nothing that encrypts or decrypts through it has to change.

Two encodings, both silent when wrong

aws kms encrypt --key-id alias/devops-KMS-Key \
  --plaintext fileb:///root/SensitiveData.txt \
  --query CiphertextBlob --output text \
  | base64 --decode > /root/EncryptedData.bin

fileb:// reads the file as raw bytes. file:// treats it as text, and CLI v2 then expects the contents to already be base64 and tries to decode them. On a plain text file that either errors or silently encrypts the wrong bytes, which is the worse of the two outcomes.

base64 --decode on the way out is needed because the API returns the ciphertext as base64 text, since JSON cannot carry binary. Leave it encoded and the file contains ASCII that KMS later rejects as a malformed blob.

The model that keeps this straight: base64 is JSON's workaround for binary, not part of the ciphertext. Strip it the moment you leave the API, add it back only when you re-enter one.

Worth noticing the sizes. 25 bytes of plaintext became 177 bytes of ciphertext, and the overhead is not padding. The blob carries the key ARN, the algorithm, and the encrypted data key. Which explains the next command:

aws kms decrypt --ciphertext-blob fileb:///root/EncryptedData.bin \
  --query Plaintext --output text | base64 --decode > /root/DecryptedData.txt

No --key-id. AWS documents that you are not required to supply the key ID when decrypting with a symmetric key, because KMS stores that in the ciphertext blob. That is how a validation script can decrypt a file without being told which key made it. Asymmetric keys are the opposite: AWS cannot store metadata in their ciphertext, so both --key-id and --encryption-algorithm are required on decrypt.

Verify with cmp

cmp -s /root/SensitiveData.txt /root/DecryptedData.txt && echo "MATCH" || echo "MISMATCH"

cat showed the right sentence, which proves nothing about trailing whitespace, a missing final newline, or an encoding change. The entire claim of a round trip is that nothing changed, so the check has to compare bytes. Same discipline as ending a deployment on curl rather than a status field.

One ceiling worth knowing before you build anything on this. kms encrypt takes at most 4096 bytes with a symmetric key. Beyond that the pattern is envelope encryption: generate-data-key returns a plaintext key and an encrypted copy of it, you encrypt the file locally, discard the plaintext key, and store the encrypted one alongside. KMS never sees your data, only the small key. That is how S3, EBS and RDS encryption all work underneath, and why "encrypted with KMS" almost never means the bytes went to KMS.

Read the manual for the thing, not for things like it

A trailing # is a comment in Bash, in Python, in YAML, and in almost everything else you type all day. In a Dockerfile it is an argument. EXPOSE sounds like it exposes. A name field that is not on the object it names.

None of these are unreasonable designs once you know them. All three punish the assumption that a familiar-looking token behaves familiarly, and all three fail without saying so clearly.

So here is the Day 41 question. What in your current stack are you confident about because you read its documentation, and what because it resembles something else you already knew?

Day 41 down. Fifty-nine to go.

Chat with me