| Tested on | Ubuntu 26.04 (OpenSSH_10.2p1) |
|---|---|
| Package | openssh-client 1:10.2p1-2ubuntu3.6 |
| Applies to | Any Linux, macOS, or Windows host with OpenSSH 8.2 or later that supports ssh-keygen -Y |
| Privilege | Normal user |
| Scope | Local file and stdin signing with ssh-keygen -Y sign and verification with allowed_signers, signer identity (-I), namespace (-n), and the original data on stdin. Does not cover SSH login keys, OpenSSH user certificates, or GPG workflows. |
| Related guides | SSH key authentication SSH config file ssh command |
In this article we will sign a file with ssh-keygen -Y sign and verify the detached signature with ssh-keygen -Y verify, an allowed_signers file, a signer identity, and a namespace string.
Verification is not ssh-keygen verify publickey file.sig. OpenSSH needs:
- the original bytes on stdin
- a namespace that matches the one used at sign time
- a line in
allowed_signersthat maps your chosen identity to the public key
The walkthrough runs entirely on one machine — no sshd, second VM, or network setup.
Quick example
This is the full happy path. Each piece is explained in the sections below.
Create a dedicated signing key and an allowed_signers file that names who may sign:
ssh-keygen -t ed25519 -f signing_key -C "alice@example.com"Build allowed_signers from the public key and the signer identity you want verifiers to trust:
printf '%s %s\n' "alice@example.com" "$(awk '{print $1" "$2}' signing_key.pub)" > allowed_signersSign document.txt with namespace file (OpenSSH writes document.txt.sig):
printf 'Contract text for lab signing demo.\n' > document.txt
ssh-keygen -Y sign -f signing_key -n file document.txtVerify by feeding the original file on stdin:
ssh-keygen -Y verify -f allowed_signers -I alice@example.com -n file -s document.txt.sig < document.txtA good result prints Good "file" signature for alice@example.com and exits with status 0.
What ssh-keygen -Y does
OpenSSH 8.2 added a lightweight signing mode under the -Y flag. You can sign arbitrary file or stdin data with an SSH private key and verify it later with a public key listed in allowed_signers.
The -Y subcommands you will use most often:
| Subcommand | Role |
|---|---|
sign |
Create a detached signature from a private key (or from ssh-agent with -U) |
verify |
Check a signature against allowed_signers, identity, namespace, and stdin data |
find-principals |
Show which identity in allowed_signers matches a signature |
match-principals |
Confirm an identity string appears in allowed_signers |
check-novalidate |
Validate signature structure only — no trust file |
This is separate from SSH login. Signing does not require:
authorized_keyssshd- a network connection
The same ed25519 key format appears in both places.
┌──────────────┐ ssh-keygen -Y sign ┌─────────────────┐
│ signing_key │ ──────────────────────────► │ document.txt.sig │
│ (private) │ -n file │ (SSH SIGNATURE) │
└──────────────┘ └────────┬────────┘
│
┌──────────────┐ ssh-keygen -Y verify │
│allowed_signers│ ◄── -I alice@example.com ───────────┘
│ + stdin data │ -n file
└──────────────┘Check whether your OpenSSH supports -Y
File signing needs OpenSSH 8.2 or later. Check the client version first:
ssh -VOpenSSH_10.2p1 Ubuntu-2ubuntu3.6, OpenSSL 3.5.5 27 Jan 2026Any OpenSSH_8.2 or higher build should include -Y sign and -Y verify. If your package is older, upgrade openssh-client (or the platform equivalent) before continuing.
Confirm the subcommands exist in the manual:
man ssh-keygen | grep -E 'ssh-keygen -Y (sign|verify)' ssh-keygen -Y sign [-O option] -f key_file -n namespace file ...
ssh-keygen -Y verify [-O option] -f allowed_signers_file -IIf man ssh-keygen shows those lines, your binary supports the workflow in this article.
Create an SSH signing key
Use a dedicated key for signing so you do not mix it with your daily SSH login key. Ed25519 is the default choice on modern OpenSSH:
ssh-keygen -t ed25519 -f signing_key -C "alice@example.com"ssh-keygen prompts for a passphrase. Protect a real signing key by:
- choosing a strong passphrase
- loading the key into
ssh-agentwhen you sign - using hardware-backed storage when available
For a throwaway automation demo you can pass -N "", but do not use an empty passphrase on a real signing key.
ssh-keygen writes signing_key (private) and signing_key.pub (public). The comment after -C is only a label in the .pub file — verification uses the principals string you put in allowed_signers, not the comment.
List the public key to confirm the algorithm and fingerprint:
cat signing_key.pubssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJT8bVYh4doAMTTCwjbaf/M/Entd2p88kbHOJGb0Xn7I alice@example.comSign a file
Place the data you want to protect in document.txt, then sign it with namespace file:
printf 'Contract text for lab signing demo.\n' > document.txtSign the file with namespace file:
ssh-keygen -Y sign -f signing_key -n file document.txtSigning file document.txt
Write signature to document.txt.sigOpenSSH creates a detached signature beside the source file. Inspect it:
head -2 document.txt.sig-----BEGIN SSH SIGNATURE-----
U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAglPxtViHh2gAxNMLCNtp/8z8Se1The armor block is not a GPG .asc file. Keep document.txt and document.txt.sig together when you distribute them.
What is the signature namespace?
The -n argument sets a namespace string that is baked into the signature. The same file bytes signed with -n file will not verify when you pass -n email, even if the key and allowed_signers line are correct.
Namespaces stop cross-context replay. A signature meant for a file release cannot be presented as an email attestation, because the namespace embedded at sign time must match at verify time.
Namespace examples:
fileandemailare conventional tutorial labels- for your own app, prefer something specific such as
release@golinuxcloud.comorartifact@example.orgso your namespace does not collide with someone else's
Use the same string at sign and verify.
Create an allowed_signers file
Verification does not take a bare .pub path on the command line. You maintain a text file — by convention allowed_signers — where each line authorizes one or more principals to sign with a given public key:
principals [options] keytype base64-keyEach field means:
principals— one identity such asalice@example.com, or a comma-separated pattern listoptions— optional restrictions such asnamespaces="file"orvalid-before=...keytype— for examplessh-ed25519base64-key— the Base64 public key blob from the.pubfile (not the comment at the end)
For a single signer with no extra options, one line is enough:
printf '%s %s\n' "alice@example.com" "$(awk '{print $1" "$2}' signing_key.pub)" > allowed_signersShow the file:
cat allowed_signersalice@example.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJT8bVYh4doAMTTCwjbaf/M/Entd2p88kbHOJGb0Xn7IRead the line left to right:
- the first field is the principals pattern — pass the same string to
-Iduring verify keytypeand the Base64 blob must match the public half of the signing key
Restrict signing trust with allowed_signers options
You can add optional fields on each allowed_signers line to limit when and how a key may sign. A common option is namespaces="file", which means the key is trusted only for signatures created with -n file:
alice@example.com namespaces="file" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJT8bVYh4doAMTTCwjbaf/M/Entd2p88kbHOJGb0Xn7IEven if Alice's key signs the same bytes under another namespace, verification against this entry rejects it. Sign document.txt again with the email namespace:
ssh-keygen -Y sign -f signing_key -n email document.txtOpenSSH writes document.txt.sig again, this time for namespace email. Verify that signature against the restricted allowed_signers line:
ssh-keygen -Y verify -f allowed_signers -I alice@example.com -n email -s document.txt.sig < document.txtallowed_signers:1: key is not permitted for use in signature namespace "email"
Could not verify signature.You can also time-bound trust:
valid-after=— do not trust the key before this timevalid-before=— stop trusting the key after this time
Both use OpenSSH time expressions and timestamps, so check man ssh-keygen for the accepted format before using them in automation. This article does not walk through certificate-authority signing; see OpenSSH certificate authentication for that path.
Verify the file signature
A cryptographically valid signature only proves that some private key signed the bytes. allowed_signers is the authorization list: it decides whether that key is one you trust. -I states which signer identity you expect, and OpenSSH accepts the signature only when that identity matches a principals pattern tied to the trusted key.
Pipe the original file bytes on stdin. For verify you need:
-f allowed_signers-Iwith the expected identity-nwith the namespace used at sign time-spointing at the signature file
ssh-keygen -Y verify -f allowed_signers -I alice@example.com -n file -s document.txt.sig < document.txtGood "file" signature for alice@example.com with ED25519 key SHA256:Am7HZfvic/FaWUULAOVZmb0TYUskc4nmFEsHyWBC+aUExit status 0 means the signature matches the stdin data, namespace, identity, and trusted public key in allowed_signers.
Test what happens if the file changes
Change one byte in document.txt and run the same verify command:
printf 'tampered\n' > document.txtRun verify again with the same flags:
ssh-keygen -Y verify -f allowed_signers -I alice@example.com -n file -s document.txt.sig < document.txtSignature verification failed: incorrect signature
Could not verify signature.The signature file is unchanged, but the stdin payload no longer matches what was signed. Always verify against the exact bytes that were signed.
Test the wrong namespace
Restore the original content, then verify with a different namespace:
printf 'Contract text for lab signing demo.\n' > document.txtPass -n email even though the signature was created with -n file:
ssh-keygen -Y verify -f allowed_signers -I alice@example.com -n email -s document.txt.sig < document.txtCouldn't verify signature: namespace does not match
Could not verify signature.The key and identity are fine; the namespace string does not match the one used at sign time (file).
Test an unauthorized signer
Keep the correct file and namespace, but claim a signer who is not in allowed_signers:
ssh-keygen -Y verify -f allowed_signers -I bob@example.com -n file -s document.txt.sig < document.txtCould not verify signature.There is no bob@example.com line in allowed_signers, so verification fails even though the signature was produced by alice@example.com's key.
Reject a revoked signing key
At some point you may need to stop trusting a signing key, even if it is still listed in allowed_signers. ssh-keygen -Y verify accepts -r revocation_file, which can be:
- an OpenSSH KRL
- a one-key-per-line public-key list
See revoke SSH keys and certificates with a KRL for building a KRL; a simple revoked-key list is enough for a signing key you no longer trust.
List the public key you want to block:
awk '{print $1" "$2}' signing_key.pub > revoked_keysRun verify with -r and the same flags as before:
ssh-keygen -Y verify -f allowed_signers -I alice@example.com -n file -s document.txt.sig -r revoked_keys < document.txtCould not verify signature.The key may still appear in allowed_signers, but -r tells OpenSSH to reject it anyway.
Sign data from stdin
When the payload is a pipe or here-string, omit the filename argument. OpenSSH reads the message from standard input and writes the armored signature to stdout:
printf 'pipe payload\n' | ssh-keygen -Y sign -f signing_key -n file > stdin.sigThe progress line goes to stderr; redirect stdout alone into stdin.sig so the file contains only the BEGIN SSH SIGNATURE block.
Verify stdin signatures the same way — feed the identical bytes on stdin:
ssh-keygen -Y verify -f allowed_signers -I alice@example.com -n file -s stdin.sig <<< 'pipe payload'Good "file" signature for alice@example.com with ED25519 key SHA256:Am7HZfvic/FaWUULAOVZmb0TYUskc4nmFEsHyWBC+aUSign using a key loaded in ssh-agent
If the private key lives in ssh-agent, add -U and point -f at the public key file instead of the private key:
eval "$(ssh-agent -s)"
ssh-add signing_keyCopy the document to a new name so signing does not prompt to overwrite an existing .sig:
cp document.txt agent-doc.txtSign through the agent with -U and the public key path:
ssh-keygen -Y sign -U -f signing_key.pub -n file agent-doc.txtSigning file agent-doc.txt
Write signature to agent-doc.txt.sigVerify the agent-backed signature the usual way:
ssh-keygen -Y verify -f allowed_signers -I alice@example.com -n file -s agent-doc.txt.sig < agent-doc.txtGood "file" signature for alice@example.com with ED25519 key SHA256:Am7HZfvic/FaWUULAOVZmb0TYUskc4nmFEsHyWBC+aUStdin signing through the agent uses the same flags:
printf 'agent pipe\n' | ssh-keygen -Y sign -U -f signing_key.pub -n file > agent-stdin.sigStop the agent when you are done:
ssh-agent -kssh-keygen signatures vs GPG signatures
Both tools prove that a holder of a private key signed a message, but the trust models differ:
| Topic | ssh-keygen -Y |
GPG |
|---|---|---|
| Trust store | allowed_signers maps trusted identities to SSH keys |
OpenPGP keyring and configured trust model |
| Verify inputs | allowed_signers, -I, -n, stdin data, -s |
Public key or keyring lookup |
| Signature format | -----BEGIN SSH SIGNATURE----- armor |
OpenPGP packets (.asc / binary) |
| Namespace | Required -n string |
Context implied by key usage |
| Typical use | CI artifacts, config blobs, internal releases | Email, package repos, broad distribution |
Use ssh-keygen -Y when your team already manages SSH keys and wants detached signatures without maintaining a GPG keyring. Use GPG when you need OpenPGP interoperability outside the SSH ecosystem.
ssh-keygen -Y check-novalidate checks signature structure without consulting allowed_signers:
ssh-keygen -Y check-novalidate -n file -s document.txt.sig < document.txtGood "file" signature with ED25519 key SHA256:Am7HZfvic/FaWUULAOVZmb0TYUskc4nmFEsHyWBC+aUThat confirms the blob parses and matches the stdin message for namespace file, but not that you trust the signer.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
Unsupported operation for -Y or no -Y in man ssh-keygen |
OpenSSH older than 8.2 | Upgrade openssh-client / platform OpenSSH |
Could not verify signature. with no namespace line |
Wrong -I identity or missing allowed_signers line |
Add the signer identity and public key; match -I exactly |
namespace does not match |
Different -n at sign vs verify |
Use the same namespace string both times |
incorrect signature |
File changed since signing | Verify against the original byte stream |
Couldn't parse signature: missing header |
stdin.sig contains stderr text |
Redirect only stdout: ... > file.sig |
Too few arguments for sign: missing key |
Agent not loaded or missing -f |
Run ssh-add and use -U -f key.pub for agent signing |
Verify command treats .pub as signature |
Wrong mental model (GPG-style) | Use -f allowed_signers -I identity -s file.sig and data on stdin |
key is not permitted for use in signature namespace |
namespaces="..." on the allowed_signers line |
Sign and verify with a namespace listed in the option, or remove the restriction |
Could not verify signature. with -r |
Key listed in revocation file | Remove the key from revoked_keys or the KRL when trust is restored |
To see which identity matches a signature file:
ssh-keygen -Y find-principals -s document.txt.sig -f allowed_signersalice@example.comSummary
OpenSSH file signing lives under ssh-keygen -Y. You sign with a private key and namespace (ssh-keygen -Y sign -f signing_key -n file document.txt), which writes a detached document.txt.sig armor file. Verification is the part that trips people up: you pass allowed_signers, the signer identity (-I), the same namespace (-n), the signature path (-s), and the original bytes on stdin — not a lone public key file.
A valid signature and a trusted signer are separate questions. allowed_signers is the authorization list; -I names the identity you expect. Optional line options such as namespaces="file" narrow trust further, and -r can block a key that is otherwise still listed.
Namespaces bind a signature to a purpose so a file signature cannot be replayed as email. Tampering the payload, swapping the identity, or changing the namespace each produces a clear failure. Keys in ssh-agent sign with -U and the .pub path.
For release checks or CI gates, keep allowed_signers under version control, pin principals to real maintainer keys, use domain-style namespaces for your own apps, and script verify with the same namespace you use at sign time.
References
- ssh-keygen(1) — OpenBSD manual
- OpenSSH 8.2 release notes
- PROTOCOL.sshsig — SSH signature format
Frequently Asked Questions
Can I verify with only the .pub file and no allowed_signers?
No. ssh-keygen -Y verify requires an allowed_signers file (-f), a signer identity (-I), a namespace (-n), the signature file (-s), and the original data on stdin. The public key alone is not a valid -f argument for verify.
Does the namespace have to be the word file?
No. file is just an example. For your own application, prefer a specific label such as release@example.org so it does not collide with another team's namespace. Keep the string identical at sign and verify time.
Can I use my existing ~/.ssh/id_ed25519 login key?
You can, but a dedicated signing key is safer. Login keys appear on servers; a separate key limits exposure if a signing key must be shared with automation.
Is ssh-keygen -Y the same as OpenSSH user certificates?
No. User certificates (ssh-keygen -s with a CA) authenticate SSH sessions. -Y sign produces detached signatures over arbitrary data. The two mechanisms share key formats but serve different purposes.

