note · encoding
Pushkar Kumar 8 min read
Base64 is not encryption: how to actually secure an API key
Is Base64 encryption? No. What Base64 actually does, where encoded secrets leak, what really protects an API key, and when Base64 is the right tool.
On this page
- What Base64 actually does
- Is Base64 encryption? Why it looks like it, and why it isn’t
- The Kubernetes Secret case
- Where Base64-encoded secrets leak
- How to actually secure an API key
- Store it in a secret manager or KMS
- Inject it at runtime
- Scope it narrowly
- Rotate it
- Encrypt it in transit with TLS
- When Base64 is the right tool
- Check the string without sending it anywhere
- Checklist: Base64 and API keys
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.datais 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_7f3a9c2eOn older macOS releases the flag is -D; --decode works on both GNU and BSD.
Tip: use
printf '%s'orecho -nwhen you encode. A plainechoappends a newline, and that newline gets encoded too. A Base64 value ending inCg==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
.envfile 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 withgeton 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
ENVor copied in during a build stays in the image layers, anddocker historyor 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
base64wraps at 76 by default for the same reason; pass-w 0for 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
geton 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?
Related posts
-
9 min read
A jwt.io alternative: decode a JWT without pasting it into someone else's site
Looking for a jwt.io alternative? What a browser-based JWT decoder can and can't prove about your data, how to check it yourself, and an offline option.
-
8 min read
chown command in Linux: change file owner and group
The chown command in Linux explained: user:group syntax, chown -R and symlinks, --reference, chgrp, chmod vs chown, and fixing Docker volume permission errors with numeric IDs.
-
10 min read
chmod command in Linux: syntax, examples and common mistakes
The chmod command in Linux explained: octal and symbolic modes, chmod +x, chmod -R and its traps, setuid, setgid and sticky bits, umask, and how to fix Permission denied.