Base64 Encode SSH Keys & PEM Certificates

Storing multi-line SSH private keys and PEM certificates in environment variables, Docker secrets, or CI/CD pipelines requires single-line encoding. Base64 is the standard solution. Here is how to do it correctly.

Why keys need encoding

SSH private keys and PEM certificates are text files, but they are multi-line text files. A typical OpenSSH or RSA private key spans dozens of lines, each terminated by a newline character, wrapped between -----BEGIN----- and -----END----- markers. Newlines are exactly what breaks most secret-storage mechanisms.

Environment variables, .env files, JSON config, shell exports, and many CI/CD secret stores are designed for single-line values. When you paste a multi-line key directly, the newlines get stripped, mangled into literal \n sequences, or split into fragments — and the key silently becomes invalid. OpenSSH is unforgiving about this: a key missing even one newline or with the wrong line wrapping will fail with invalid format or error in libcrypto.

Base64 solves the problem cleanly. By encoding the entire key file (including its headers and newlines) into a single continuous string from the 64-character alphabet, you get a value that survives copy-paste, environment variables, JSON escaping, and pipeline interpolation untouched. At runtime you decode it back to the original bytes — newlines and all — restoring a byte-for-byte identical key file.

The golden rule: encode the whole file, store the single line, then decode the single line back to the file. Never try to manually reconstruct newlines with sed or tr — Base64 round-trips the bytes perfectly so you do not have to.

PEM format is already Base64

Here is the part that confuses almost everyone. A PEM file is itself Base64. The "PEM" format (Privacy-Enhanced Mail) takes binary DER-encoded data, Base64-encodes it, wraps it at 64 characters per line, and surrounds it with -----BEGIN ...----- / -----END ...----- headers. Open any certificate and you are already looking at Base64.

-----BEGIN CERTIFICATE-----
MIIDdzCCAl+gAwIBAgIEAgAAuTANBgkqhkiG9w0BAQUFADBaMQswCQYDVQQGEwJJ
RTESMBAGA1UEChMJQmFsdGltb3JlMRMwEQYDVQQLEwpDeWJlclRydXN0MSIwIAYD
VQQDExlCYWx0aW1vcmUgQ3liZXJUcnVzdCBSb290
-----END CERTIFICATE-----

So when a DevOps guide says "base64 encode your certificate," it is not asking you to encode the DER bytes (those are already encoded inside the PEM). It means: take the entire PEM text file — headers, line breaks, footers, everything — and Base64-encode that into one flat line. This is a second layer of Base64 applied on top of the PEM. The purpose is purely to flatten the multi-line file into a single transport-safe token.

Do not strip the -----BEGIN----- / -----END----- headers before encoding. The headers tell parsers what the data is (certificate, private key, EC key, etc.). Encode the file exactly as it sits on disk, including the trailing newline.

Encode a key or certificate to a single line

The key flag is the one that disables line wrapping, so you get one unbroken string. On Linux (GNU coreutils) use -w 0; on macOS (BSD) the default output is already unwrapped, but you can be explicit with -b 0 or pipe through tr.

# Linux (GNU coreutils) — -w 0 disables line wrapping
base64 -w 0 ~/.ssh/id_ed25519 > key.b64

# macOS (BSD base64) — read from a file with -i
base64 -i ~/.ssh/id_ed25519 > key.b64

# Cross-platform fallback — strip any newlines the tool adds
base64 ~/.ssh/id_ed25519 | tr -d '\n' > key.b64

# Encode a PEM certificate the same way
base64 -w 0 fullchain.pem > cert.b64

OpenSSL works identically and is handy when it is already installed but GNU base64 is not. The -A flag tells OpenSSL to treat the input/output as a single line instead of wrapping at 64 characters:

# Encode to a single line with OpenSSL
openssl base64 -A -in ~/.ssh/id_rsa -out key.b64

# Copy the result straight to the clipboard (macOS)
base64 -i ~/.ssh/id_rsa | pbcopy

# Linux with xclip
base64 -w 0 ~/.ssh/id_rsa | xclip -selection clipboard

Verify the round-trip before you trust it: base64 -w 0 key | base64 -d | diff - key should produce no output. If diff prints anything, your encoding wrapped lines or dropped the trailing newline.

Store in environment variables and .env files

Because the encoded value is a single line of safe characters (A-Z, a-z, 0-9, +, /, =), it drops straight into an environment variable or .env file with no escaping headaches:

# .env file — single line, no quotes needed for the value
SSH_PRIVATE_KEY=LS0tLS1CRUdJTiBPUEVOU1NIIFBSSVZBVEUgS0VZLS0tLS0K...
TLS_CERT=LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUR...

# Export directly in a shell
export SSH_PRIVATE_KEY="$(base64 -w 0 ~/.ssh/id_ed25519)"

This sidesteps the classic .env pitfall where a raw multi-line key either breaks the parser or gets collapsed. Most dotenv libraries (Node's dotenv, Python's python-dotenv, etc.) cannot reliably hold a multi-line value, but a single Base64 line is trivially safe.

In your application, read the variable and decode it. In Node.js, for example:

const fs = require('fs');
const key = Buffer.from(process.env.SSH_PRIVATE_KEY, 'base64');
fs.writeFileSync('/tmp/id_ed25519', key, { mode: 0o600 });

An encoded secret is still a secret — Base64 is encoding, not encryption. Anyone who reads the env var can decode it instantly. Keep .env files out of version control and treat these values exactly as you would the raw key.

Docker secrets and Kubernetes secrets

Docker lets you pass the encoded value as a build arg or environment variable and decode it in an entrypoint script. A common pattern materializes the key on container start:

# Pass the encoded key at runtime
docker run -e SSH_PRIVATE_KEY="$(base64 -w 0 ~/.ssh/id_ed25519)" myimage

# entrypoint.sh — decode before the app starts
mkdir -p /root/.ssh
echo "$SSH_PRIVATE_KEY" | base64 -d > /root/.ssh/id_ed25519
chmod 600 /root/.ssh/id_ed25519

Kubernetes is the headline case, because a Secret object stores its values as Base64 by design. The data: field must contain Base64-encoded values — Kubernetes decodes them automatically when mounting the secret as a file or injecting it as an env var:

apiVersion: v1
kind: Secret
metadata:
  name: ssh-key
type: Opaque
data:
  # value here is base64-encoded; k8s decodes it on mount
  id_ed25519: LS0tLS1CRUdJTiBPUEVOU1NIIFBSSVZBVEUgS0VZLS0tLS0K...
# Generate the manifest without hand-encoding anything
kubectl create secret generic ssh-key \
  --from-file=id_ed25519=$HOME/.ssh/id_ed25519 \
  --dry-run=client -o yaml > ssh-secret.yaml

If you use stringData: instead of data:, Kubernetes accepts the raw multi-line key and encodes it for you. Either way, what is stored in etcd is Base64. For real confidentiality, enable encryption-at-rest or use an external secrets manager.

GitHub Actions, GitLab CI, and other pipelines

CI/CD secret stores are the most common reason people Base64-encode keys, because the secret UI fields are single-line and pipeline YAML interpolation mangles raw newlines. Encode once, paste the single line into the secret, decode in the job.

GitHub Actions — store the encoded string as a repository secret named SSH_PRIVATE_KEY, then:

- name: Set up SSH key
  run: |
    mkdir -p ~/.ssh
    echo "${{ secrets.SSH_PRIVATE_KEY }}" | base64 -d > ~/.ssh/id_rsa
    chmod 600 ~/.ssh/id_rsa
    ssh-keyscan github.com >> ~/.ssh/known_hosts

GitLab CI — define a masked CI/CD variable. Note GitLab masking requires single-line, base64-safe values, which the encoded key already satisfies:

deploy:
  script:
    - mkdir -p ~/.ssh && chmod 700 ~/.ssh
    - echo "$SSH_PRIVATE_KEY" | base64 -d > ~/.ssh/id_rsa
    - chmod 600 ~/.ssh/id_rsa

CircleCI, Jenkins, and others follow the same shape: store the single Base64 line as an environment variable or credential, then echo "$VAR" | base64 -d into a file at the start of the job. The decode step is identical everywhere because the encoding is standard.

On macOS-based runners, base64 -d is the BSD decode flag — but GNU also accepts -d, so the snippets above are portable. Avoid base64 --decode only if you must support extremely old BusyBox builds; otherwise it is equivalent.

Decode back to PEM at runtime

Decoding restores the exact original bytes, including every newline and the headers, so the resulting file is a valid PEM or OpenSSH key with no post-processing. The two things people forget are file permissions and making the directory first.

# Decode and restore the key, then lock down permissions
echo "$SSH_PRIVATE_KEY" | base64 -d > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519

# Decode a TLS certificate and key pair
echo "$TLS_CERT" | base64 -d > /etc/ssl/certs/app.pem
echo "$TLS_KEY"  | base64 -d > /etc/ssl/private/app.key
chmod 600 /etc/ssl/private/app.key

SSH refuses to use a private key whose permissions are too open, failing with Permissions 0644 for 'id_ed25519' are too open. Always follow the decode with chmod 600 (or 0400). If you decode into a fresh ~/.ssh, create the directory with mkdir -p ~/.ssh && chmod 700 ~/.ssh first.

You can verify a decoded key without exposing it. For SSH keys, ask OpenSSH to print the public fingerprint; for PEM certificates, dump the subject and dates with OpenSSL:

# Confirm the decoded SSH key parses and show its fingerprint
ssh-keygen -y -f ~/.ssh/id_ed25519 > /dev/null && echo "key OK"

# Inspect a decoded certificate
openssl x509 -in /etc/ssl/certs/app.pem -noout -subject -dates

If decoding produces a file that tools reject, the cause is almost always a wrapped (multi-line) Base64 value that lost characters during copy-paste, or a value that picked up a stray space. Re-encode with -w 0, paste carefully, and re-run the diff round-trip check.


FAQ

How do I base64 encode an SSH private key?

Use: base64 -w 0 ~/.ssh/id_rsa (Linux) or base64 -i ~/.ssh/id_rsa (macOS) to get a single-line Base64 string. Store that as a secret in your CI/CD system, then decode it at runtime with: echo "$SECRET" | base64 -d > ~/.ssh/id_rsa && chmod 600 ~/.ssh/id_rsa

Is a PEM file already Base64?

Yes. The content between -----BEGIN CERTIFICATE----- and -----END CERTIFICATE----- headers is already Base64-encoded DER data. When DevOps guides say "base64 encode your certificate," they mean encode the entire PEM file (including headers) into a single-line string for storage.

How do I use a Base64-encoded private key in GitHub Actions?

Store the Base64 string as a repository secret (e.g., SSH_PRIVATE_KEY). In your workflow, decode it: echo "${{ secrets.SSH_PRIVATE_KEY }}" | base64 -d > ~/.ssh/id_rsa && chmod 600 ~/.ssh/id_rsa

Try Base64 encoding and decoding instantly

Paste any string or file — base64.dev auto-detects and converts it instantly.

Open base64.dev →