Sign and Verify Files with SSH Keys Using ssh-keygen -Y

Deepak Prasad
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
Short on time?Beta Get the gist of this 13-minute read in seconds, then ask follow-up questions

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_signers that 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:

bash
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:

bash
printf '%s %s\n' "alice@example.com" "$(awk '{print $1" "$2}' signing_key.pub)" > allowed_signers

Sign document.txt with namespace file (OpenSSH writes document.txt.sig):

bash
printf 'Contract text for lab signing demo.\n' > document.txt
ssh-keygen -Y sign -f signing_key -n file document.txt

Verify by feeding the original file on stdin:

bash
ssh-keygen -Y verify -f allowed_signers -I alice@example.com -n file -s document.txt.sig < document.txt

A 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_keys
  • sshd
  • 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:

bash
ssh -V
output
OpenSSH_10.2p1 Ubuntu-2ubuntu3.6, OpenSSL 3.5.5 27 Jan 2026

Any 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:

bash
man ssh-keygen | grep -E 'ssh-keygen -Y (sign|verify)'
output
       ssh-keygen -Y sign [-O option] -f key_file -n namespace file ...
       ssh-keygen   -Y   verify   [-O   option]   -f   allowed_signers_file  -I

If 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:

bash
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-agent when 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:

bash
cat signing_key.pub
output
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJT8bVYh4doAMTTCwjbaf/M/Entd2p88kbHOJGb0Xn7I alice@example.com

Sign a file

Place the data you want to protect in document.txt, then sign it with namespace file:

bash
printf 'Contract text for lab signing demo.\n' > document.txt

Sign the file with namespace file:

bash
ssh-keygen -Y sign -f signing_key -n file document.txt
output
Signing file document.txt
Write signature to document.txt.sig

OpenSSH creates a detached signature beside the source file. Inspect it:

bash
head -2 document.txt.sig
output
-----BEGIN SSH SIGNATURE-----
U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAglPxtViHh2gAxNMLCNtp/8z8Se1

The 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:

  • file and email are conventional tutorial labels
  • for your own app, prefer something specific such as release@golinuxcloud.com or artifact@example.org so 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:

text
principals [options] keytype base64-key

Each field means:

  • principals — one identity such as alice@example.com, or a comma-separated pattern list
  • options — optional restrictions such as namespaces="file" or valid-before=...
  • keytype — for example ssh-ed25519
  • base64-key — the Base64 public key blob from the .pub file (not the comment at the end)

For a single signer with no extra options, one line is enough:

bash
printf '%s %s\n' "alice@example.com" "$(awk '{print $1" "$2}' signing_key.pub)" > allowed_signers

Show the file:

bash
cat allowed_signers
output
alice@example.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJT8bVYh4doAMTTCwjbaf/M/Entd2p88kbHOJGb0Xn7I

Read the line left to right:

  • the first field is the principals pattern — pass the same string to -I during verify
  • keytype and 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:

text
alice@example.com namespaces="file" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJT8bVYh4doAMTTCwjbaf/M/Entd2p88kbHOJGb0Xn7I

Even 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:

bash
ssh-keygen -Y sign -f signing_key -n email document.txt

OpenSSH writes document.txt.sig again, this time for namespace email. Verify that signature against the restricted allowed_signers line:

bash
ssh-keygen -Y verify -f allowed_signers -I alice@example.com -n email -s document.txt.sig < document.txt
output
allowed_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 time
  • valid-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
  • -I with the expected identity
  • -n with the namespace used at sign time
  • -s pointing at the signature file
bash
ssh-keygen -Y verify -f allowed_signers -I alice@example.com -n file -s document.txt.sig < document.txt
output
Good "file" signature for alice@example.com with ED25519 key SHA256:Am7HZfvic/FaWUULAOVZmb0TYUskc4nmFEsHyWBC+aU

Exit 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:

bash
printf 'tampered\n' > document.txt

Run verify again with the same flags:

bash
ssh-keygen -Y verify -f allowed_signers -I alice@example.com -n file -s document.txt.sig < document.txt
output
Signature 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:

bash
printf 'Contract text for lab signing demo.\n' > document.txt

Pass -n email even though the signature was created with -n file:

bash
ssh-keygen -Y verify -f allowed_signers -I alice@example.com -n email -s document.txt.sig < document.txt
output
Couldn'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:

bash
ssh-keygen -Y verify -f allowed_signers -I bob@example.com -n file -s document.txt.sig < document.txt
output
Could 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:

bash
awk '{print $1" "$2}' signing_key.pub > revoked_keys

Run verify with -r and the same flags as before:

bash
ssh-keygen -Y verify -f allowed_signers -I alice@example.com -n file -s document.txt.sig -r revoked_keys < document.txt
output
Could 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:

bash
printf 'pipe payload\n' | ssh-keygen -Y sign -f signing_key -n file > stdin.sig

The 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:

bash
ssh-keygen -Y verify -f allowed_signers -I alice@example.com -n file -s stdin.sig <<< 'pipe payload'
output
Good "file" signature for alice@example.com with ED25519 key SHA256:Am7HZfvic/FaWUULAOVZmb0TYUskc4nmFEsHyWBC+aU

Sign 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:

bash
eval "$(ssh-agent -s)"
ssh-add signing_key

Copy the document to a new name so signing does not prompt to overwrite an existing .sig:

bash
cp document.txt agent-doc.txt

Sign through the agent with -U and the public key path:

bash
ssh-keygen -Y sign -U -f signing_key.pub -n file agent-doc.txt
output
Signing file agent-doc.txt
Write signature to agent-doc.txt.sig

Verify the agent-backed signature the usual way:

bash
ssh-keygen -Y verify -f allowed_signers -I alice@example.com -n file -s agent-doc.txt.sig < agent-doc.txt
output
Good "file" signature for alice@example.com with ED25519 key SHA256:Am7HZfvic/FaWUULAOVZmb0TYUskc4nmFEsHyWBC+aU

Stdin signing through the agent uses the same flags:

bash
printf 'agent pipe\n' | ssh-keygen -Y sign -U -f signing_key.pub -n file > agent-stdin.sig

Stop the agent when you are done:

bash
ssh-agent -k

ssh-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:

bash
ssh-keygen -Y check-novalidate -n file -s document.txt.sig < document.txt
output
Good "file" signature with ED25519 key SHA256:Am7HZfvic/FaWUULAOVZmb0TYUskc4nmFEsHyWBC+aU

That 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:

bash
ssh-keygen -Y find-principals -s document.txt.sig -f allowed_signers
output
alice@example.com

Summary

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


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.

Finished reading?Beta Get the key takeaways in seconds, then ask follow-up questions
Omer Cakmak

Linux Administrator

Highly skilled at managing Debian, Ubuntu, CentOS, Oracle Linux, and Red Hat servers. Proficient in bash scripting, Ansible, and AWX central server management, he handles server operations on OpenStack, KVM, Proxmox, and VMware.

  • Debian
  • Ubuntu
  • Linux
  • Red Hat Enterprise Linux
  • Shell Script
  • System Administration