TT Lab
Get started
Learn Learning paths Courses

Kubernetes Operations

Seeing the Plaintext in etcd Yourself, and Stopping It

Continue in TT Lab

Goal

You open etcd directly to check how a Secret is actually stored, and write each of the four layers of response, encryption at rest, envelope encryption, access control, and an external store, as a manifest.

Why it matters

The most widespread misconception is that "a Secret is safe because it is base64." base64 is a representation with no key, so reversing it takes a single command, and more important is the fact that with the default settings the apiserver writes Secrets to etcd as plaintext, as they are. The etcd snapshot you learned about in the previous module is therefore also the most sensitive file in the cluster. In this lab you confirm that fact with your own hands rather than taking someone's word for it. The response is not a single layer. EncryptionConfiguration stops someone who obtains the disk, and KMS envelope encryption takes even that key out of the cluster. But encrypting storage is of no use against someone who can read through the API, so RBAC is needed, and although resourceNames can narrow things down to the name level, you also need to know the limit that it does not work for list/watch. Finally, through the external secrets approach that keeps the values entirely outside the cluster, the core of this lab is distinguishing what each of the four layers blocks and what it cannot.

Steps

  1. Create the namespace secret-lab and in it create the Secret db-password. The type is Opaque and the value of the key password is labhub-Pr0d-2026. Also create one more Secret, other-secret, as a control group for comparing permissions later.
  2. Save the value of db-password's data.password, as is, to /root/ops/secrets/out/encoded.txt, and the decoded plaintext to /root/ops/secrets/out/decoded.txt (the decoded result must be labhub-Pr0d-2026 and the two files must correspond to each other). And in /root/ops/secrets/out/base64-note.txt, write in one line the conclusion that base64 is only encoding, not encryption.
  3. Write /root/ops/secrets/encryption-config.yaml. apiVersion is apiserver.config.k8s.io/v1, kind is EncryptionConfiguration, and resources[0].resources must contain secrets. The first of providers is a real encryption provider (one of aescbc, secretbox, and aesgcm) and must have keys[0].name, and the last must be identity.
  4. Write /root/ops/secrets/kms-config.yaml. kind is EncryptionConfiguration and providers[0] must be kms. kms.apiVersion is v2, kms.endpoint is a socket path starting with unix://, and kms.name must also be present. And in /root/ops/secrets/out/envelope-note.txt, write an explanation of the two-layer structure of envelope encryption (data key and master key).
  5. In /root/ops/secrets/external-secret.yaml, write two documents. One is kind: SecretStore and the other is kind: ExternalSecret. The ExternalSecret must have all of spec.refreshInterval, spec.secretStoreRef.name, spec.target.name, spec.target.creationPolicy, and spec.data[0].remoteRef.key. The real password labhub-Pr0d-2026 must not be in this file.
  6. In secret-lab, create the ServiceAccount db-client and the Role db-secret-reader. The rule must be, for the secrets resource, only a single resourceNames entry db-password and a single verbs entry get. Bind this role to db-client with a RoleBinding. As a result, db-client must read db-password, must not read other-secret, and must not be able to list secrets (list) either. And in /root/ops/secrets/out/resourcenames-note.txt, write that resourceNames does not work for list/watch.
  7. In secret-lab, create the Secret pinned-config. It is immutable: true and the value of the key build is 2026-08-20. Then try to change this value and save the rejection message to /root/ops/secrets/out/immutable-error.txt. Even after that, the build value must still be 2026-08-20.
  8. Save the output of querying the key /registry/secrets/secret-lab/db-password from etcd to /root/ops/secrets/out/etcd-secret.txt. The plaintext labhub-Pr0d-2026 must be visible in this file. Then create /root/ops/secrets/out/secrets-audit.json. total_secrets must match the actual number of Secrets in secret-lab, encrypted_at_rest is false, and plaintext_visible_in_etcd is true. Write 3 or more mitigations in the mitigations array, and it must include encryption at rest (EncryptionConfiguration or "encryption") and access control (RBAC or "permissions").

Reference

Create two Secrets for the lab

Create the namespace secret-lab and in it create the Secret db-password. The type is Opaque and the value of the key password is labhub-Pr0d-2026. Also create one more Secret, other-secret, as a control group for comparing permissions later.

Check what the default type is. You create two because you need a control group for comparing the permission scope later.

Reverse the base64 yourself

Save the value of db-password's data.password, as is, to /root/ops/secrets/out/encoded.txt, and the decoded plaintext to /root/ops/secrets/out/decoded.txt (the decoded result must be labhub-Pr0d-2026 and the two files must correspond to each other). And in /root/ops/secrets/out/base64-note.txt, write in one line the conclusion that base64 is only encoding, not encryption.

This step is for confirming the difference between encoding and encryption by hand. Also check that the reversed value and the encoded value correspond to each other.

Write the encryption-at-rest configuration

Write /root/ops/secrets/encryption-config.yaml. apiVersion is apiserver.config.k8s.io/v1, kind is EncryptionConfiguration, and resources[0].resources must contain secrets. The first of providers is a real encryption provider (one of aescbc, secretbox, and aesgcm) and must have keys[0].name, and the last must be identity.

What is used for writes is the first in the list. Think about what must remain at the end so that you can keep reading the existing plaintext. A key needs a name.

Write the KMS v2 envelope encryption configuration

Write /root/ops/secrets/kms-config.yaml. kind is EncryptionConfiguration and providers[0] must be kms. kms.apiVersion is v2, kms.endpoint is a socket path starting with unix://, and kms.name must also be present. And in /root/ops/secrets/out/envelope-note.txt, write an explanation of the two-layer structure of envelope encryption (data key and master key).

The plugin attaches through a socket, not the network. Leave a note on what the two-layer structure is.

Write the external secret store integration manifest

In /root/ops/secrets/external-secret.yaml, write two documents. One is kind: SecretStore and the other is kind: ExternalSecret. The ExternalSecret must have all of spec.refreshInterval, spec.secretStoreRef.name, spec.target.name, spec.target.creationPolicy, and spec.data[0].remoteRef.key. The real password labhub-Pr0d-2026 must not be in this file.

You need two documents. One defines where to fetch from, and the other defines what to create under which name. If the real password ends up in the manifest, the reason for using it disappears.

Create a role that opens just one secret

In secret-lab, create the ServiceAccount db-client and the Role db-secret-reader. The rule must be, for the secrets resource, only a single resourceNames entry db-password and a single verbs entry get. Bind this role to db-client with a RoleBinding. As a result, db-client must read db-password, must not read other-secret, and must not be able to list secrets (list) either. And in /root/ops/secrets/out/resourcenames-note.txt, write that resourceNames does not work for list/watch.

There is a field that narrows the scope by name. But also sort out that there are verbs for which that field does not work.

Create an immutable Secret and confirm the modification is rejected

In secret-lab, create the Secret pinned-config. It is immutable: true and the value of the key build is 2026-08-20. Then try to change this value and save the rejection message to /root/ops/secrets/out/immutable-error.txt. Even after that, the build value must still be 2026-08-20.

After locking it, actually try to change it and keep the rejection message. The error goes to standard error.

Pull the plaintext out of etcd and keep it as evidence

Save the output of querying the key /registry/secrets/secret-lab/db-password from etcd to /root/ops/secrets/out/etcd-secret.txt. The plaintext labhub-Pr0d-2026 must be visible in this file. Then create /root/ops/secrets/out/secrets-audit.json. total_secrets must match the actual number of Secrets in secret-lab, encrypted_at_rest is false, and plaintext_visible_in_etcd is true. Write 3 or more mitigations in the mitigations array, and it must include encryption at rest (EncryptionConfiguration or "encryption") and access control (RBAC or "permissions").

Recall the key path rule for Kubernetes objects. The value is protobuf so it looks garbled, but the strings read as they are.