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.
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.
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."
Remove the ephemeral environment's root key
~/.ssh/authorized_keys. If idle, removing them has no impact. First, don't leave unused keys in place.Even if needed again, don't go back to root
Restrict the key to a single operation
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@cirestrict 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:
| Option | What it restricts | When to use it |
|---|---|---|
command="..." | Logging in with the key runs only the specified command; whatever command the client sent is ignored | Keys that should do one fixed job, such as pulling a deploy or taking a backup |
restrict | Disables 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 list | CI 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-10If 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_keysEach 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_COMMANDenvironment variable) as-is inside the script thatcommand=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, usessh -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.
Read next
- A new vulnerability: OpenSSH 10.6p2 fixes a sshd password-authentication bypass on musl Linux without PAM (CVE-2026-123158)
- Basics: the security baseline checklist (least privilege, protecting admin accounts)
- Operations: separate keys to minimize damage / production receives only · the vulnerability-response playbook
- Cases: the Trivy supply-chain compromise (SSH keys targeted in CI) · the Codecov supply-chain attack
- Incident: a key stolen via RCE and billed for fraud · the Benesse data leak (excess privilege and insider misuse)
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