Security Guides

SSH key least privilege: keep production root keys off ephemeral environments that can be compromised

A production root SSH key on a GPU pod or CI runner means one compromise gives root on production. Use a non-root user and a command-restricted key.

Published 2026-06-11 Updated 2026-10-06 13 min read
On this page

For: anyone reaching production over SSH from an ephemeral environment — a GPU-cloud pod, a CI runner, a throwaway VM. No attack steps — only making keys least-privilege so a leak causes less damage. For the basics of separating keys, see the baseline checklist.

This site's view: treat ephemeral environments as untrusted

Throwaway compute often skips hardening — it's a place that can be compromised. Put a production root key there and a compromise of that environment becomes a compromise of production. This site uses a distinct key per host, scopes each to a purpose, and removes keys as soon as they're unused (→ separate keys to minimize damage). Treating a key not as "a handy convenience" but as "the most critical asset, one that gives access to every server if it leaks" is, in the end, the safest approach.

✗ root key on an ephemeral env

env compromised → root on production with that key → reaches files, secrets, and other services — everything.

○ command-restricted non-root key

even if the same key leaks, it can run only the one specified command. No shell, no forwarding — damage is contained to one operation.

Keep what a key can do small from the start. It changes the damage when it leaks.

Why a root key on an ephemeral environment is dangerous

Ephemeral environments tend to skip hardening ("we'll delete it soon"), making them an easy target and a convenient relay point for attacking production. A production root key there makes the damage as large as it can be.

root→all
a leaked key gives full control of production
throwaway
ephemeral envs skip hardening
reused
one leak exposes every host
least priv
limit a key to one operation and the damage is limited too

Say you spin up a pod for compute and, to work on production from it, register a key in root's authorized_keys. Handy — but if that pod is compromised, the attacker gets into production as root with that key. Whether the attack starts with an RCE or a key leak, the end result is the same: production taken with a stolen key (related: a key stolen via RCE and billed for fraud).

Three steps to least-privilege keys

Switch from "grant full power because it's convenient" to "limit what it can do to the minimum."

1

Remove the ephemeral environment's root key

Delete keys you no longer use from production's ~/.ssh/authorized_keys. If idle, removing them has no impact. First, don't leave unused keys in place.
2

Even if needed again, don't go back to root

When you re-register for production work, create a dedicated non-root user and register the key to it. Even if it leaks, the reach is limited to that user's privileges.
3

Restrict the key to a single operation

Add command="..." and restrict to the key line in authorized_keys. Even a successful login can run only the one specified command, with port forwarding and a shell disabled. A leak is contained to that one operation.
# Register a key restricted to ONE operation in a non-root user's authorized_keys.
# Logging in with it can run only the given command; forwarding and PTY are off.
command="/usr/local/bin/deploy-pull",restrict ssh-ed25519 AAAA...rest-of-public-key deploy@ci

restrict disables forwarding, agent forwarding, PTY, X11, and more in one go; command="..." pins the executable action to one thing. The basic move is a key scoped to a single purpose — only pull a deploy, only run a specific script — created per use case.

A common setup (dangerous)

  • register a key to production root from a pod/CI
  • leave finished keys in place
  • reuse one key across many hosts
  • scatter keys that grant a full shell

The least-privilege setup

  • reach production only as a non-root user
  • keys are limited to one operation (command-restricted)
  • separate keys per host and purpose
  • remove keys from authorized_keys the moment they're unused

Treat a reused key as your most critical asset

Think of a key as "a handy convenience" and you reuse it and forget to delete it. Instead, treat it as the most critical asset, one that gives access to every server if it leaks. Separate per host and purpose, keep production keys off ephemeral environments, and always remove them after use. That alone prevents one leak from spreading to everything.

The main restrictions you can put on a key line

In OpenSSH, options written at the start of a key line in authorized_keys narrow what that key can do. The common ones:

OptionWhat it restrictsWhen to use it
command="..."Logging in with the key runs only the specified command; whatever command the client sent is ignoredKeys that should do one fixed job, such as pulling a deploy or taking a backup
restrictDisables port, agent and X11 forwarding, PTY allocation, and running ~/.ssh/rc in one go (OpenSSH 7.2+)Almost every automation key; turn back on only what you need, e.g. pty
from="..."Besides the key, the client's host name or IP address must match the listCI or work servers with a fixed source IP
expiry-time="..."The key is no longer accepted after the given date/time (OpenSSH 7.7+)Keys meant to work only for a temporary job
PermitRootLogin (in sshd_config)Whether direct root login is allowed; forced-commands-only allows root only for keys with command=no, unless you truly need direct root access

The OpenSSH release notes describe restrict as including "all current and future key restrictions." Rather than listing no-pty, no-port-forwarding and so on one by one, add restrict and switch back on only what you need, so nothing slips through when new features appear.

# Also narrowing the source and the lifetime (the IP is a documentation address)
from="192.0.2.10",expiry-time="20261231",command="/usr/local/bin/deploy-pull",restrict ssh-ed25519 AAAA...rest-of-public-key deploy@ci-2026-10

If you manage time-limited access across many keys, OpenSSH certificates are another option (sign with ssh-keygen -s, set the validity with -V, and restrict with force-command or source-address). For a small setup, options on the key line are enough to start.

How to check your own environment

These checks only read the settings and logs of servers you manage. When you change settings, keep one SSH session open, test a new connection from another terminal, and close the old session only once you know you aren't locked out.

1. List the registered keys

# Show the fingerprint and comment of each key registered for root and each user
sudo ssh-keygen -lf /root/.ssh/authorized_keys
sudo ssh-keygen -lf /home/deploy/.ssh/authorized_keys

Each key prints one line such as 256 SHA256:... deploy@ci (ED25519). Use the comment (the deploy@ci part at the end) to pick out keys of unknown origin, keys from ephemeral environments you've finished with, and keys registered to root. The old file name authorized_keys2 is also read by default, so check it the same way if it exists.

2. Check the configuration sshd actually uses

# Check the syntax, then print the settings actually in effect
sudo sshd -t
sudo sshd -T | grep -iE 'permitrootlogin|authorizedkeysfile|pubkeyauthentication|passwordauthentication'

sshd -T prints the effective values after reading the configuration files. If permitrootlogin is yes or prohibit-password, anyone with a key can log in as root directly. If authorizedkeysfile points somewhere other than the default, add that location to step 1.

3. See which keys are actually used

After a successful key login, sshd normally logs a line like Accepted publickey for deploy from 192.0.2.10 port 50022 ssh2: ED25519 SHA256:.... Read it in /var/log/auth.log on Debian and Ubuntu, /var/log/secure on Red Hat-family systems, or with journalctl -u ssh on systemd hosts (-u sshd on Red Hat-family systems).

Matching these fingerprints against the list from step 1 shows which keys go unused. NIST's report on SSH key management (NISTIR 7966) suggests watching the logs for roughly 6 to 12 months before removing unused keys, so that rarely used keys aren't removed by mistake. Keys you know are finished with, such as an ephemeral environment's key, can go right away.

4. Check the ephemeral side

  • Is a private key that reaches production sitting in CI secrets or in a GPU pod's ~/.ssh?
  • Are you connecting to the ephemeral environment with ssh -A (agent forwarding) on the way to production?
  • Is one key shared across several CI systems or servers?

Common mistakes and how to fix them

  • Deleting the private key on the ephemeral side and stopping there: if the private key was already copied, it keeps working as long as the public key stays on the server. Always remove the matching line from production's authorized_keys.
  • Running the client's command (the SSH_ORIGINAL_COMMAND environment variable) as-is inside the script that command= calls: that's the same as no restriction. Use the value only when it exactly matches one of a fixed set of choices.
  • Switching to a non-root user but allowing unrestricted sudo: if that user can sudo anything without a password, it is effectively root. If sudo is needed, limit it to one command in sudoers.
  • Entering an ephemeral environment with ssh -A: anyone with enough privileges there can use the keys loaded in your local agent through the forwarded connection (the OpenSSH manual warns about this). If you need to go through a host, use ssh -J (connecting via a jump host).
  • Leaving keys without a comment: during a review you can't tell where they came from. Add the purpose and date when you create them, e.g. ssh-keygen -C "deploy@ci-2026-10".
  • Sharing one key among several people or environments: NISTIR 7966 also calls for prohibiting sharing private keys or copying them to other systems. You can't trace who used it where, and one leak means replacing it everywhere.

A real example: SSH keys were among what a compromised CI tool collected

In March 2026, a malicious release of the open-source vulnerability scanner Trivy appeared, and its GitHub Actions tags were rewritten. According to the developer, Aqua Security, the malicious code collected credentials from CI runners and sent them out, and its targets included private keys in ~/.ssh, authorized_keys, known_hosts, and SSH host keys. Aqua asks anyone who may have run an affected version to treat every secret that pipeline could read, SSH keys included, as exposed and rotate it immediately (details: the Trivy supply-chain compromise (March 2026)).

In the 2021 Codecov case, a script run in CI was also modified at its source, and for about two months secrets from customers' CI environments were sent out (→ the Codecov supply-chain attack).

Both show that keys inside CI can be taken even when your own code is fine. If a taken key is registered to production root, production is handed over at that point. With a dedicated non-root user, command= plus restrict, and the source narrowed with from=, the same leaked key can do only the one job you defined, and connections from anywhere else are refused.

What this site does itself

This site uses a dedicated key per server and never reuses keys. Deploys land on a non-root, purpose-built user, and production is one-directional — "it receives, it doesn't go out to fetch" (→ production receives only). It doesn't leave standing keys from ephemeral compute into production; it connects only when needed, at least privilege. The reason is exactly this article's: keep what each key can do small in advance, so one compromise can't spread to everything.

Sources

  • OpenBSD manual, "sshd(8)", AUTHORIZED_KEYS FILE FORMAT (command, restrict, from, expiry-time, SSH_ORIGINAL_COMMAND): man.openbsd.org
  • OpenBSD manual, "sshd_config(5)", PermitRootLogin and AuthorizedKeysFile: man.openbsd.org
  • OpenBSD manual, "ssh(1)", -A (agent forwarding caution) and -J: man.openbsd.org
  • OpenBSD manual, "ssh-keygen(1)", -l, -C and certificate options: man.openbsd.org
  • OpenSSH 7.2 release notes (restrict added): openssh.org
  • OpenSSH 7.7 release notes (expiry-time added): openssh.org
  • NIST, "NISTIR 7966: Security of Interactive and Automated Access Management Using Secure Shell (SSH)" (October 2015): csrc.nist.gov
  • Wikibooks, "OpenSSH/Logging and Troubleshooting" (key fingerprints in successful-login log lines): en.wikibooks.org
  • Aqua Security, "Trivy Supply Chain Attack: What You Need to Know" (2026): aquasec.com
  • Codecov, "Bash Uploader Security Update" (2021): about.codecov.io

FAQ

QWhy is putting a root key on an ephemeral environment dangerous?
A

Ephemeral environments like GPU-cloud pods or CI runners are throwaway, so hardening is often skipped and they can be compromised. Put a root key to production there, and the moment that environment is compromised, the attacker gets root on production along with the key. Treat ephemeral environments as untrusted and keep production root keys off them.

QWhat if I still need to reach production from an ephemeral environment?
A

Don't go back to root. Create a dedicated non-root user and register a key that limits what it can do to a single operation. In authorized_keys, a command-restricted, restrict-flagged key means even a successful login can only run that one command. A leak is then contained to that one operation.

QWhat's the key principle for key management?
A

Treat a reused key as your most critical asset and never build a setup where one leak exposes everything. Separate keys per host and per purpose, and remove keys from authorized_keys when they're no longer needed. That stops a single leak from spreading to everything.

QHow do I check which SSH keys are registered on a server?
A

Run ssh-keygen -lf against each user's ~/.ssh/authorized_keys to list every registered key with its fingerprint (SHA256:...) and comment. Check every account, root included, and pick out keys whose origin you can't explain and keys from ephemeral environments you no longer use. To see which keys are actually used, match those fingerprints against the successful-login lines sshd writes to its log.

QWhat should PermitRootLogin be set to?
A

OpenSSH's default is prohibit-password: no password logins for root, but anyone with a key can log in as root directly. If you don't need direct root logins, set it to no and work from a non-root user. If something like a backup truly needs a root key, forced-commands-only allows root only for keys that carry a command="..." option.

QHow do I deploy over SSH from CI or GitHub Actions safely?
A

CI runners are a textbook ephemeral environment, and there are real cases where a tool used in CI was compromised and secrets including SSH keys were collected. Register the deploy key to a dedicated non-root user and limit it to the one pull/deploy action with command="..." and restrict. If the source IP is fixed, narrow it with from="...". Use a separate key per pipeline and environment, and when a compromise is announced, remove the old keys first, then register new ones.