The scenario
You just finished provisioning a Fedora server. You want to log in without typing a password every time, but you also want to lock the door against brute-force scripts. You run ssh-keygen, copy the public key to the remote machine, and immediately get Permission denied (publickey). The terminal stares back at you. The fix is usually three permission bits, one config reload, and a clear understanding of how sshd validates trust.
How the handshake actually works
SSH key authentication replaces shared secrets with asymmetric cryptography. You generate a pair of mathematical tokens. The private key stays on your local machine. The public key travels to the server. When you initiate a connection, the server sends a random challenge encrypted with your public key. Your SSH client decrypts it using the private key and sends the answer back. If the math checks out, the server grants a shell.
The system relies on strict file permissions. The OpenSSH daemon refuses to use a private key if any user other than the owner can read it. It ignores an authorized_keys file if the group or others have write access. This is a deliberate security boundary. The daemon assumes that if permissions are loose, an attacker could have swapped the key or injected a backdoor. Trust the permission model. It exists to protect you from yourself.
Run journalctl -xeu sshd first. Read the actual error before guessing.
Generate and deploy the key pair
Here is how to create a modern key pair and place it on the remote host. Fedora ships with ssh-copy-id, which handles directory creation and file appending automatically.
# Generate an Ed25519 key pair. The -C flag adds a comment for identification.
ssh-keygen -t ed25519 -C "admin@fedoraserver"
# Accept the default path (~/.ssh/id_ed25519) and set a strong passphrase.
# The passphrase encrypts the private key on disk. It is not sent over the network.
The command creates two files. id_ed25519 is your private key. id_ed25519.pub is the public key. Keep the private key offline. Never email it. Never commit it to version control.
Next, push the public key to the server. You need password access for this single step.
# Copy the public key to the remote user's authorized_keys file.
# ssh-copy-id creates ~/.ssh and sets correct permissions automatically.
ssh-copy-id user@your-server-ip
# Enter your password when prompted. The tool verifies the upload by running a test login.
If ssh-copy-id is unavailable or you are provisioning a bare metal machine without network access, you can append the key manually. Read the public key locally and pipe it to the remote shell.
# Read the local public key and append it to the remote authorized_keys file.
# The -N flag prevents ssh from asking for a password during the pipe.
cat ~/.ssh/id_ed25519.pub | ssh user@your-server-ip "mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys"
Run ssh-copy-id when you already have password access. Use the manual pipe when you are automating initial provisioning or working around a restricted shell.
Lock down permissions and reload the daemon
The daemon will reject your key if the local or remote file permissions are too open. Check the local side first.
# Restrict the SSH directory to the owner only.
chmod 700 ~/.ssh
# Lock down the private key. Read/write for owner only.
chmod 600 ~/.ssh/id_ed25519
# The public key can be world-readable. This is safe and expected.
chmod 644 ~/.ssh/id_ed25519.pub
Log into the server and verify the remote permissions. The authorized_keys file must be owned by the user and not writable by the group.
# Ensure the remote SSH directory matches local standards.
chmod 700 ~/.ssh
# Restrict authorized_keys to owner read/write.
chmod 600 ~/.ssh/authorized_keys
# Verify ownership matches the current user.
chown -R $(whoami):$(whoami) ~/.ssh
Fedora places daemon configuration in /etc/ssh/sshd_config. User modifications belong in /etc/ssh/sshd_config.d/. Never edit files in /usr/lib/ssh/. Those ship with the package and get overwritten on updates. Create a drop-in file to enforce key authentication and disable passwords.
# /etc/ssh/sshd_config.d/99-keyauth.conf
# Enable public key authentication explicitly.
PubkeyAuthentication yes
# Disable password login after verifying key access works.
PasswordAuthentication no
# Disable challenge-response to block legacy PAM password prompts.
ChallengeResponseAuthentication no
Reload the daemon to apply the changes. A full restart drops active connections. A reload preserves them.
# Apply the new configuration without disconnecting existing sessions.
sudo systemctl reload sshd
# Verify the daemon picked up the drop-in file.
sudo sshd -T | grep -i passwordauthentication
Reload the service after every config change. The runtime state and the persistent config diverge otherwise.
Manage passphrases with ssh-agent
A passphrase encrypts your private key on disk. It stops an attacker from using your key if they steal your laptop. Typing it every time defeats the convenience of key authentication. ssh-agent solves this by holding the decrypted key in memory.
Start the agent and load your key. The agent runs in the background and handles authentication prompts automatically.
# Launch the agent and export its environment variables.
eval "$(ssh-agent -s)"
# Add the private key to the agent's memory.
ssh-add ~/.ssh/id_ed25519
# Enter the passphrase once. The agent caches the decrypted key for the session.
Fedora's desktop environment starts ssh-agent automatically for graphical sessions. Terminal-only sessions require the manual start. Add the eval line to your ~/.bashrc or ~/.zshrc if you work in headless environments. The agent expires when you log out. This keeps the decrypted key out of swap and core dumps.
Check loaded keys with ssh-add -l. Remove a key with ssh-add -d. Clear everything with ssh-add -D. Trust the agent lifecycle. Manual key caching drifts, session boundaries stay.
Verify the connection
Open a new terminal window. Do not test from the session you used to disable passwords. If the key fails, you will lock yourself out.
# Force the client to attempt publickey authentication only.
# This bypasses password fallback and isolates the key handshake.
ssh -o PreferredAuthentications=publickey user@your-server-ip
# If successful, you get a shell immediately. No password prompt appears.
Check the server logs to confirm the daemon accepted the key. The x flag adds explanatory context. The e flag jumps to the end of the journal.
# View recent SSH daemon logs with explanatory text.
sudo journalctl -xeu sshd
# Look for "Accepted publickey for user from port" in the output.
Run the test from a separate terminal first. A botched config change can leave you unable to connect. Keep a backup session open until you verify the new one works.
Common pitfalls and exact error strings
The most common failure is a permission warning that stops the client before it even tries to authenticate.
WARNING: UNPROTECTED PRIVATE KEY FILE!
Permissions 0644 for '/home/user/.ssh/id_ed25519' are too open.
It is required that your private key files are NOT accessible by others.
This private key will be ignored.
The client refuses to send a key that other users can read. Run chmod 600 ~/.ssh/id_ed25519 and try again.
The second most common failure is a silent rejection from the server.
Permission denied (publickey).
This means the server received the connection, checked the authorized_keys file, and found no match or invalid permissions. Check the remote ~/.ssh/authorized_keys file. Ensure it contains exactly one key per line. Trailing spaces or merged lines break the parser. Check the SELinux context if you moved the key to a non-standard directory. Fedora enforces strict file contexts. Run restorecon -Rv ~/.ssh to reset them.
SELinux denials show up in journalctl -t setroubleshoot with a one-line summary. Read those before disabling SELinux. The policy usually just needs a context restore.
When the handshake fails silently, run the client with verbose output. The -v flag prints protocol negotiation steps. The -vvv flag prints cryptographic details. Pipe the output to a file if it scrolls too fast.
# Run the client with maximum verbosity to trace the exact failure point.
ssh -vvv user@your-server-ip 2>&1 | tee ssh-debug.log
# Search the log for "Offering public key" and "Server accepts key".
Run journalctl -xe first. Read the actual error before guessing.
Choose your key type and deployment method
Use Ed25519 when you want shorter keys, faster verification, and protection against implementation flaws. Use RSA when you must support legacy clients that predate 2016 or corporate hardware that lacks modern cipher suites. Use ssh-copy-id when you already have password access and want the tool to handle directory creation. Use manual cat >> ~/.ssh/authorized_keys when you are provisioning a fresh VM without network access yet. Keep password authentication enabled when you are testing remote access for the first time. Disable password authentication when the server faces the public internet. Trust the package manager. Manual file edits drift, snapshots stay.