There are a high number of posts on this topic, but I was still not able to find a solution that worked well for me. I don’t believe my setup is that special, but I want everything to be declarative.
Short summary of what I’m using:
- MacOS
- NiX
- Home-manager
- Fish shell
- No use of ssh-agent from earlier
- YubiKey 5c
Note: The current version of OpenSSH on MacOS does not support the sk-options, so we will need to use a version of OpenSSH from the nixpkgs or homebrew. I’m using a version from nixpkgs.
Install the needed software.
On my system, all software is installed using nix. I had to enable homebrew to get one of the packages I needed.
homebrew = {
enable = true;
onActivation = {
autoUpdate = true;
upgrade = true;
};
taps = [
"jorgelbg/tap"
];
brews = [
"pinentry-touchid"
];
};
}
I also made a really simple YubiKey module to install the software I need for nixpkgs:
{ pkgs, lib, config, ... }:
with lib; let
cfg = config.features.cli.yubikey;
in {
options.features.cli.yubikey.enable = mkEnableOption "Enable YubiKey Support";
config = mkIf cfg.enable {
home.packages = with pkgs; [
openssh
yubikey-manager
];
home.file = {
".bin/ssh-askpass" = {
enable = true;
source = ../../files/ssh-askpass;
executable = true;
};
};
};
}
The shell script referred to can be found here.
I only made one small change, letting the script use pinentry-touchid istead of pinentry-mac.
Set PIN on the YubyKey
The pin can be set using ykman:
ykman fido access change-pin
Generate a ssh-key on the YubiKey
There is a bug in OpenSSH not honoring the -O no-touch-required setting. I believe this is fixed now, but it does not seem to be upstream yet. Bug report is found here. I want the no-touch-required setting to be enable so I don’t need to touch the key for every ssh operation involving the key. I only want to enter the PIN when the key is reseated.
So I’m generating the key with the no-touch-required option in the hopes that it will work soon.
How I generated my key:
ssh-keygen -t ed25519-sk -O resident -O no-touch-required -f ~/.ssh/id_ed25519-darwnix -O application=ssh:darwnix20241210 -N "" -C"Darwnix-2024-12-10"
The available security options when generating a ssh-key on a security key can be found here
The battle of ssh-agent
Since the version of OpenSSH shipped with MacOS does not support security keys, the same goes for ssh-agent. I had to add a few hacks to make this work. It’s not pretty, but it does the job.
I added the following functions to my config.fish
set -gx SSH_ASKPASS /Users/$(whoami)/.bin/ssh-askpass
set -gx DISPLAY ":0"
if not pgrep -f 'ssh-agent -c' >/dev/null
eval (ssh-agent -c -a /Users/davalex/.ssh-socket.fish)
end
if pgrep -f 'ssh-agent -l' >/dev/null
kill $(pgrep -f 'ssh-agent -l')
end
set -gx SSH_AUTH_SOCK /Users/davalex/.ssh-socket.fish
My home-manager config for fish:
{ pkgs, lib, config, ... }:
with lib; let
cfg = config.features.cli.fish;
in {
options.features.cli.fish = {
enable = mkEnableOption "Enable Fish shell";
workstation = mkOption {
type = types.bool;
default = false;
description = "Apply workstation-specific Fish shell configuration";
};
};
config = mkIf cfg.enable {
programs.fish = {
enable = true;
shellAliases = {
ls = "eza";
grep = "rg";
ps = "procs";
cat = "bat";
};
functions = mkIf cfg.workstation {
rebuild = {
body = ''
cd ~/.config/nix-config/
nix flake update
darwin-rebuild check --flake .
darwin-rebuild switch --flake .
cd -
'';
};
awsctx = {
body = "set -gx AWS_PROFILE (aws configure list-profiles | fzf)";
};
rebuild-diff = {
body = ''
cd ~/.config/nix-config/
nix flake update
darwin-rebuild build --flake .
nvd diff /run/current-system ./result
rm result
cd -
'';
};
};
shellAbbrs = mkIf cfg.workstation {
".." = "cd ..";
"..." = "cd ../..";
};
interactiveShellInit = mkIf cfg.workstation ''
set -gx LANG en_US.UTF-8
set -gx EDITOR nvim
set -gx VISUAL nvim
set -U fish_user_paths /opt/homebrew/bin $fish_user_path $fish_user_paths
set -gx SSH_ASKPASS /Users/$(whoami)/.bin/ssh-askpass
set -gx DISPLAY ":0"
if not pgrep -f 'ssh-agent -c' > /dev/null
eval (ssh-agent -c -a /Users/$(whoami)/.ssh-socket.fish)
end
if pgrep -f 'ssh-agent -l' > /dev/null
kill $(pgrep -f 'ssh-agent -l')
end
set -gx SSH_AUTH_SOCK /Users/$(whoami)/.ssh-socket.fish
'';
};
};
}
This is a hack, but it makes sure the ssh-agent shipped with MacOS is killed and a new one started creating a socket in my home folder. The SSH_AUTH_SOCK is pointed at this sockets. This makes sure that the agent is available in a all shells at all times.
That should be about it. Now add start a fresh shell and add the keys to the agent:
ssh-add -K
And you should be good to go! Good luck! :)