Dev.to Security 🔐 Cybersecurity 👁 0 📖 7 min read

Base64 is not encryption: how to actually secure an API key

A pull request lands with a config file that contains this line: PAYMENTS_API_KEY: b3BzX2xpdmVfN2YzYTljMmU= The author's note says the key is "encoded, so it's fine to commit". It isn't. Anyone who can read that

A pull request lands with a config file that contains this line:

PAYMENTS_API_KEY: b3BzX2xpdmVfN2YzYTljMmU=

The author's note says the key is "encoded, so it's fine to commit". It isn't. Anyone who can read that line can run one command and get the key back, and so can every scanner that crawls public repositories looking for exactly this pattern.

TL;DR

  • Base64 is an encoding. It has no key, so anyone can reverse it, instantly.
  • Kubernetes Secret.data is Base64 because YAML needs text, not because it hides anything.
  • What protects a key is where it lives and who can read it: a secret manager or KMS, injection at runtime, narrow scope, rotation, and TLS in transit.
  • Base64 is the right tool for moving bytes through text-only channels. That is all it is for.

What Base64 actually does

Base64, defined in RFC 4648, turns arbitrary bytes into a string drawn from 64 printable characters: A–Z, a–z, 0–9, + and /, with = as padding. It reads the input three bytes (24 bits) at a time and writes four characters of six bits each. The three ASCII bytes of Man come out as TWFu.

That 3-to-4 ratio is why encoded data is about 33% larger than the original. Whenever the input length isn't a multiple of three, the output gets one or two = characters so its length stays a multiple of four.

The same RFC defines a URL-safe variant, base64url, which swaps + for - and / for _ so the result can sit in a URL or filename. JWTs use it, usually with the padding stripped.

There's no secret anywhere in that process. Decoding is the same table read backwards:

printf '%s' 'b3BzX2xpdmVfN2YzYTljMmU=' | base64 -d
# ops_live_7f3a9c2e

On older macOS releases the flag is -D; --decode works on both GNU and BSD.

Tip: use printf '%s' or echo -n when you encode. A plain echo appends a newline, and that newline gets encoded too. A Base64 value ending in Cg== often means someone encoded "value\n" by accident, and the stray newline quietly breaks the credential.

Is Base64 encryption? Why it looks like it, and why it isn't

Encryption transforms data with a key, so the output is useless to anyone without that key. Base64 has no key. The algorithm is public, identical everywhere, and reversible by anyone.

It looks like encryption because the output is unreadable to a human at a glance. YWRtaW46aHVudGVyMg== doesn't obviously say admin:hunter2, so it feels protected. It isn't. The disguise holds against a casual glance and nothing else, and automated secret scanners decode Base64 as a matter of routine.

The Kubernetes Secret case

The most common source of this confusion is Kubernetes. A Secret manifest stores its values Base64-encoded under data:

apiVersion: v1
kind: Secret
metadata:
  name: payments
type: Opaque
data:
  api-key: b3BzX2xpdmVfN2YzYTljMmU=

The encoding exists so that binary values, such as a keystore or a certificate, fit in a text manifest. The stringData field accepts the same values as plain text and the API server encodes them for you, which shows how little the encoding protects.

The Kubernetes documentation is explicit about this: by default, Secrets are stored unencrypted in the API server's data store, etcd. Encryption at rest is something you turn on separately, with an EncryptionConfiguration on the API server or a KMS provider. Base64 is not part of it.

Where Base64-encoded secrets leak

Since encoding hides nothing, an encoded secret leaks through every channel a plain-text one would. The usual ones:

  • Git history. A committed Secret manifest or .env file stays in history after you delete it from the tip. Removing the line in a later commit does not remove the key. Rotate it, then consider rewriting history.
  • Logs. Debug logging that dumps a request, an environment or a rendered config writes the encoded value right alongside everything else. Log pipelines then copy it somewhere with wider read access.
  • kubectl get secret -o yaml. Anyone with get on Secrets in a namespace can read every value in it, and that permission is often broader than intended. Output pasted into a ticket or chat carries the keys with it.
  • Client bundles. A key that ships in front-end JavaScript or a mobile app is public, encoded or not. Anyone can open DevTools or unpack the app and decode it.
  • Container images. A key baked in with ENV or copied in during a build stays in the image layers, and docker history or a layer extract will show it.

Warning: if a real key has been committed, logged or shipped in a bundle, the fix is rotation. Deleting the file or making the repo private closes off future exposure. It does not tell you who has already read it.

How to actually secure an API key

No single step does it. Protection comes from controlling where a key lives, who can read it, and how much damage it does if it escapes.

Store it in a secret manager or KMS

Use a purpose-built store: AWS Secrets Manager or SSM Parameter Store, Google Secret Manager, Azure Key Vault, or HashiCorp Vault. These encrypt values at rest under keys you don't handle directly, log every read, and put access behind IAM policies you can audit. For Kubernetes, enable encryption at rest for Secrets and tighten RBAC so that few identities can get them. Tools such as External Secrets Operator or Sealed Secrets keep plaintext out of your manifests.

Inject it at runtime

Have the application read the key when it starts, from the secret store or from an environment variable or mounted file that the platform fills in. The key then never appears in source, images or build logs. Keep a .env.example with placeholder names in the repo and the real .env out of it. The .env example checker flags drift between the two.

Scope it narrowly

Issue keys with the smallest permission set the job needs: read-only where read is enough, one key per service and environment, IP or referrer restrictions where the provider supports them. A leaked read-only staging key is an inconvenience. A leaked admin production key is an incident.

Rotate it

Expiring keys and scheduled rotation limit how long a leak stays useful. Make rotation routine: the day you need it is the day a key has escaped.

Encrypt it in transit with TLS

Send keys only over HTTPS. HTTP Basic authentication (RFC 7617) sends username:password as Base64 in the Authorization header, and over plain HTTP anyone on the path can decode it. TLS protects the channel. Base64 only formats the header.

When Base64 is the right tool

None of this makes Base64 bad. It's the correct tool for its actual job, which is carrying bytes through a channel that only handles text:

  • Binary data in JSON or YAML. Neither format has a byte type, so images, certificates and keystores go in as Base64 strings.
  • HTTP Basic auth. The header format requires it, as above.
  • Data URIs. RFC 2397 lets a small image or font be inlined as data:image/png;base64,….
  • Email attachments. MIME (RFC 2045) uses Base64 to carry binary over mail transport, wrapped at 76 characters. GNU base64 wraps at 76 by default for the same reason; pass -w 0 for one unbroken line.
  • Tokens and URLs. base64url carries binary identifiers and JWT segments without escaping.

In every one of those cases, the reader is expected to decode the value. That's the point.

Check the string without sending it anywhere

When you find a suspicious string in a config file or a log line, decode it to see what it is. But don't paste a possible production key into a site that might send it somewhere.

The Base64 Encoder / Decoder on this site runs entirely in your browser. It handles standard and URL-safe alphabets and full UTF-8, and nothing you paste is uploaded. As with any tool, don't take that on trust: open DevTools, clear the Network tab, paste the string, and check that no request carries it. If the string turns out to be a JWT, the JWT decoder splits and decodes all three segments.

Checklist: Base64 and API keys

  • [ ] No API key in the repo, encoded or not, including in history.
  • [ ] Kubernetes Secrets encrypted at rest, and RBAC get on Secrets limited to the identities that need it.
  • [ ] Keys held in a secret manager or KMS and injected at runtime, never baked into images or front-end bundles.
  • [ ] One narrowly scoped key per service and environment.
  • [ ] A rotation procedure that has been run at least once.
  • [ ] Credentials sent only over TLS, Basic auth included.
  • [ ] Logs scrubbed of headers, environment dumps and rendered configs.
  • [ ] Base64 used only to carry bytes through text, never to hide them.

How does your team catch encoded secrets before they merge: a pre-commit scanner, a CI check, or reviewers who know what b3Bz looks like?

📰 Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.