Three different operations
Base64 regularly shows up in places where people expect protection, so it is worth being exact about what it does. It is an encoding: a fixed, public mapping from bytes to text (see how Base64 works). The mapping has no key and no secret, and the same input always produces the same output.
| Encoding (Base64, hex) | Encryption (AES-GCM) | Hashing (SHA-256, Argon2) | |
|---|---|---|---|
| Purpose | Represent bytes as text | Keep data confidential | Fingerprint or verify data |
| Reversible | Yes, by anyone | Yes, with the key | No |
| Needs a key | No | Yes | No (HMAC uses one) |
| Same input, same output | Always | No, with a random nonce | Yes, unless salted |
| Output size | About 4/3 of input | Input plus nonce and tag | Fixed length |
Decoding takes no effort at all:
echo ZGVwbG95Omh1bnRlcjI= | base64 -d
# deploy:hunter2
That string looks opaque to a person glancing at it. To a script, a log scanner or an attacker, it is plain text with an extra step.
How Base64 data leaks in practice
The pattern is always the same: a value is Base64 encoded for transport, someone mistakes it for protected, and then it ends up somewhere it can be read. These are the common shapes of that mistake.
HTTP Basic Auth headers
Basic authentication (RFC 7617) sends the username and password as Base64(user:password) in every request:
Authorization: Basic ZGVwbG95Omh1bnRlcjI=
The only protection is TLS on the connection. Once the header leaves that tunnel, the password is effectively in clear text. Typical ways that happens: reverse proxies or application frameworks that log request headers, HAR files exported from browser developer tools and attached to a support ticket, curl -v output pasted into a chat, and error-tracking tools that capture full requests. The Basic Auth decoder shows how little it takes to read one.
Kubernetes Secrets
A Kubernetes Secret stores its values Base64 encoded:
apiVersion: v1
kind: Secret
metadata:
name: db
data:
password: czNjcjN0
czNjcjN0 is s3cr3t. The encoding exists so values can hold binary data inside YAML. The Kubernetes documentation states that Secrets are stored unencrypted in etcd by default, and anyone allowed to get the Secret through the API can read it. The common leaks are manifests with real data: values committed to a Git repository, kubectl get secret -o yaml output pasted into tickets, and backups of etcd or cluster state stored without encryption. The Kubernetes Secret decoder decodes all values of a Secret at once, which is also a quick way to check what a manifest actually exposes.
Container registry credentials follow the same pattern. The auth field in a Docker config.json or a .dockerconfigjson Secret is Base64(user:password); the dockerconfigjson decoder reads it.
JWT payloads
A signed JWT looks like a sealed token, but its first two parts are just Base64URL JSON:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI0MiIsImVtYWlsIjoiYW5hQGV4YW1wbGUuY29tIiwicm9sZSI6ImFkbWluIn0.<signature>
The payload decodes to {"sub":"42","email":"ana@example.com","role":"admin"}. The signature protects the token against modification; it does nothing for confidentiality. Every browser extension, proxy, log file and analytics script that sees the token can read what is inside. Personal data, internal IDs you would rather not expose, and anything secret do not belong in a JWT payload. If they must travel in a token, use an encrypted JWT (JWE). More detail is in Base64 in JWT.
“Obfuscated” secrets in code and config
API keys stored as Base64 in front-end JavaScript, mobile app bundles or config files are a classic. The usual reasoning is that it keeps the key from being “plain text”. It does keep it from matching a naive search for the literal key, and that is all. Anything shipped to a client can be read by that client.
The same idea runs in the other direction in attacks. PowerShell’s -EncodedCommand takes Base64 of UTF-16LE text, and it is often used to make malicious commands harder to read at a glance. Defenders decode it in seconds, for example with the PowerShell encoded command decoder.
What to use instead
Pick the tool by the job. Base64 still has a place in each of these, as the text wrapper around bytes that are protected by something else.
To keep data confidential: authenticated encryption
Use AES-256-GCM or ChaCha20-Poly1305, through a well-maintained library. Both are authenticated, so tampering is detected on decryption. Two rules matter more than the algorithm: never reuse a nonce with the same key, and keep the key somewhere other than next to the data (a KMS, a secret manager or an environment the attacker does not also control).
import { randomBytes, createCipheriv, createDecipheriv } from "node:crypto";
const key = randomBytes(32); // in real code, load this from a KMS or secret store
const iv = randomBytes(12); // 96-bit nonce, unique per message
const cipher = createCipheriv("aes-256-gcm", key, iv);
const ct = Buffer.concat([cipher.update("deploy:hunter2", "utf8"), cipher.final()]);
const tag = cipher.getAuthTag();
// Base64 carries the encrypted bytes as text: nonce + tag + ciphertext
const token = Buffer.concat([iv, tag, ct]).toString("base64");
const raw = Buffer.from(token, "base64");
const d = createDecipheriv("aes-256-gcm", key, raw.subarray(0, 12));
d.setAuthTag(raw.subarray(12, 28));
console.log(Buffer.concat([d.update(raw.subarray(28)), d.final()]).toString("utf8"));
// deploy:hunter2
The output is a 56-character Base64 string that changes on every run because the nonce is random. Without the key it is useless. To experiment without writing code, the AES encrypt and AES decrypt tools run in your browser.
To store passwords: a slow, salted hash
Passwords should not be encrypted or encoded. Store a hash from a function designed for passwords: Argon2id, scrypt or bcrypt, with a unique random salt per password. A plain SHA-256 of a password is too fast to compute and falls to brute force. Python’s standard library includes scrypt:
import hashlib, hmac, os
def hash_password(pw: str) -> bytes:
salt = os.urandom(16)
return salt + hashlib.scrypt(pw.encode(), salt=salt, n=2**14, r=8, p=1)
def check_password(pw: str, stored: bytes) -> bool:
salt, digest = stored[:16], stored[16:]
candidate = hashlib.scrypt(pw.encode(), salt=salt, n=2**14, r=8, p=1)
return hmac.compare_digest(candidate, digest)
If you need to put the result in a text column or JSON, Base64 it.
To prove integrity: signatures and HMAC
When the goal is “nobody changed this” rather than “nobody can read this”, use an HMAC with a shared secret or a digital signature. The result is bytes, usually transported as Base64 or hex. The HMAC generator shows the output in both forms.
For credentials in transit and at rest
Use TLS for every connection that carries Basic Auth or tokens, prefer short-lived tokens over static passwords, scrub Authorization headers from logs, enable encryption at rest for Kubernetes Secrets, and keep real secret values out of Git.
FAQ
Is Base64 secure?
Base64 has no security properties. There is no key, so decoding needs nothing but the string itself. It is safe to use as a transport format for data that is already protected, such as ciphertext or a signature.
Is Base64 good enough to hide a password in a config file?
No. Anyone who can read the file can decode the password with base64 -d. Restrict file permissions, use a secret manager, or encrypt the value with a key stored elsewhere.
Why are Kubernetes Secrets Base64 encoded if it is not for security?
So that Secret values can hold arbitrary binary data, such as TLS keys or keystores, inside YAML and JSON. Protection comes from RBAC, encryption at rest for etcd and careful handling of manifests.
Can anyone read a JWT payload without the secret?
Yes. The header and payload are Base64URL JSON that anyone can read. The key is only needed to verify or create the signature. Try it with the JWT decoder.
What is the difference between encoding, encryption and hashing?
Encoding is reversible by anyone. Encryption is reversible only with a key. Hashing is one-way: you can check a value against a hash, but you cannot turn the hash back into the value.