YubiKey-Guide icon indicating copy to clipboard operation
YubiKey-Guide copied to clipboard

SSH in Mac OS: launchctl commands return "Load failed: 5: Input/output error"

Open Danrancan opened this issue 9 months ago • 5 comments

In DR.DUH_GUIDE --> SSH --> macOS I am having some issues with the launchctl commands. I have created the two files $HOME/Library/LaunchAgents/gnupg.gpg-agent.plist and $HOME/Library/LaunchAgents/gnupg.gpg-agent-symlink.plist. Then I run the following commands which produce input/output errors:

as normal user (no sudo)

Danran@MacBook-Pro ~/Library/LaunchAgents$ launchctl load $HOME/Library/LaunchAgents/gnupg.gpg-agent.plist
Load failed: 5: Input/output error
Try running `launchctl bootstrap` as root for richer errors.
Danran@MacBook-Pro ~/Library/LaunchAgents$ launchctl load $HOME/Library/LaunchAgents/gnupg.gpg-agent-symlink.plist
Load failed: 5: Input/output error
Try running `launchctl bootstrap` as root for richer errors.

With sudo:

Danran@MacBook-Pro ~/Library/LaunchAgents$ sudo launchctl load $HOME/Library/LaunchAgents/gnupg.gpg-agent.plist
Warning: Expecting a LaunchDaemons path since the command was ran as root. Got LaunchAgents instead.
`launchctl bootstrap` is a recommended alternative.
Load failed: 5: Input/output error
Try running `launchctl bootstrap` as root for richer errors.
Danran@MacBook-Pro ~/Library/LaunchAgents$ sudo launchctl load $HOME/Library/LaunchAgents/gnupg.gpg-agent-symlink.plist

Warning: Expecting a LaunchDaemons path since the command was ran as root. Got LaunchAgents instead.
`launchctl bootstrap` is a recommended alternative.
Load failed: 5: Input/output error
Try running `launchctl bootstrap` as root for richer errors.

Any reason why I'm getting these errors? What can I do to fix this?

Thanks.

Danrancan avatar May 19 '25 07:05 Danrancan

Well, I don't have these plists installed but I think I can still answer the question (and possibly confuse you even more too).

From the main README:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
    <dict>
        <key>Label</key>
        <string>gnupg.gpg-agent</string>
        <key>RunAtLoad</key>
        <true/>
        <key>KeepAlive</key>
        <false/>
        <key>ProgramArguments</key>
        <array>
            <string>/usr/local/MacGPG2/bin/gpg-connect-agent</string>
            <string>/bye</string>
        </array>
    </dict>
</plist>

The meat of that is:

/usr/local/MacGPG2/bin/gpg-connect-agent

Now, on a Mac (irrespective of Intel or Apple silicon):

$ ls -l /usr/local/MacGPG2/bin/gpg-connect-agent
ls: /usr/local/MacGPG2/bin/gpg-connect-agent: No such file or directory

So that's the basic problem and is why you're seeing the "Load failed: 5: Input/output error".

If I run:

$ which gpg-connect-agent

then:

  • on an Apple Silicon Mac:

    opt/homebrew/bin/gpg-connect-agent
    
  • on an Intel Mac:

    usr/local/bin/gpg-connect-agent
    

And that's the fix. You go back to the basic plist and replace:

/usr/local/MacGPG2/bin/gpg-connect-agent

with the relevant path from your Apple or Intel Mac.

Next, if I install the modified content at the path:

$HOME/Library/LaunchAgents/gnupg.gpg-agent.plist

then there is no need to "load" it. That's because current macOS senses the addition of a plist dynamically. You should get a notification telling you that a background item has been added to the "Allow in the Background" list in the "Login Items" panel in "System Settings".

However, if you want to remove such an item then I believe that the recommended approach is to unload it before removing:

$ launchctl unload ~/Library/LaunchAgents/gnupg.gpg-agent.plist
$ rm ~/Library/LaunchAgents/gnupg.gpg-agent.plist

On the topic of sudo, don't do that. This is a user launch agent, not a system launch agent.

Last thing. As far as I know, kickstart has replaced bootstrap for most situations. It's a bit tricky with user launch agents but, for this situation, the appropriate command would be:

$ launchctl kickstart -k gui/$(id -u)/gnupg.gpg-agent

The id -u returns your numeric user ID, and the tail end of the path is the Label field in the plist.

Although I did install gnupg.gpg-agent.plist while I was writing the above, I didn't try the "symlink.plist". I studied it for a while but it gives me the heebies.

This is the bit that troubles me:

/bin/ln -sf $HOME/.gnupg/S.gpg-agent.ssh $SSH_AUTH_SOCK

Firstly, to the best of my knowledge, the first command argument S.gpg-agent.ssh is a socket that will only exist if the YubiKey has been inserted, unlocked and used. Certainly, none of the socket files is present on my system after a reboot until first use of the YubiKey. That implies any launch agent which assumes the socket always exists will fail when it doesn't.

However, let's assume the socket exists:

$ ls -l $HOME/.gnupg/S.gpg-agent.ssh
srwx------  1 user  group  0 May 20 16:34 $HOME/.gnupg/S.gpg-agent.ssh

Secondly, I do not see how a Launch Agent can "see" the SSH_AUTH_SOCK environment variable so, at face value, it should be blank so the command would reduce to:

$ /bin/ln -sf $HOME/.gnupg/S.gpg-agent.ssh

Executing that results in:

$ ls -l $HOME/.gnupg/S.gpg-agent.ssh
lrwxr-xr-x  1 user  group  37 May 20 17:17 /$HOME/.gnupg/S.gpg-agent.ssh -> $HOME/.gnupg/S.gpg-agent.ssh

So now it's a symlink pointing to itself and the socket is either masked or destroyed (I don't know which). What I do know is that it doesn't work:

$ file $HOME/.gnupg/S.gpg-agent.ssh
$HOME/.gnupg/S.gpg-agent.ssh: broken symbolic link to $HOME/.gnupg/S.gpg-agent.ssh

Computer says no! The only "fix" I've been able to find is to remove that file, reboot, and make the YubiKey do something.

It gets weirder. Let's assume the launch agent can somehow see SSH_AUTH_SOCK. What should its value be? Hunting around the README:

export SSH_AUTH_SOCK=$(gpgconf --list-dirs agent-ssh-socket)

Let's run the meat of that:

$ gpgconf --list-dirs agent-ssh-socket
$HOME/.gnupg/S.gpg-agent.ssh

In short, as far as I can tell, if we assume the Launch Agent can see SSH_AUTH_SOCK the command would become:

ln -sf $HOME/.gnupg/S.gpg-agent.ssh $HOME/.gnupg/S.gpg-agent.ssh

which is still a symlink pointing to itself. Thus, irrespective of whether a launch agent can or can't see SSH_AUTH_SOCK, we still wind up with a masked socket and a broken symlink.

See why it gives me the heebies?

Not on my system! Well, not until someone more knowledgeable than me can explain why any of this makes sense.

Hope this helps.

Paraphraser avatar May 20 '25 07:05 Paraphraser

Well, I don't have these plists installed but I think I can still answer the question (and possibly confuse you even more too).

From the main README:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
    <dict>
        <key>Label</key>
        <string>gnupg.gpg-agent</string>
        <key>RunAtLoad</key>
        <true/>
        <key>KeepAlive</key>
        <false/>
        <key>ProgramArguments</key>
        <array>
            <string>/usr/local/MacGPG2/bin/gpg-connect-agent</string>
            <string>/bye</string>
        </array>
    </dict>
</plist>

The meat of that is:

/usr/local/MacGPG2/bin/gpg-connect-agent

Now, on a Mac (irrespective of Intel or Apple silicon):

$ ls -l /usr/local/MacGPG2/bin/gpg-connect-agent
ls: /usr/local/MacGPG2/bin/gpg-connect-agent: No such file or directory

So that's the basic problem and is why you're seeing the "Load failed: 5: Input/output error".

If I run:

$ which gpg-connect-agent

then:

  • on an Apple Silicon Mac:
     /opt/homebrew/bin/gpg-connect-agent
    
  • on an Intel Mac:
     /usr/local/bin/gpg-connect-agent
    

And that's the fix. You go back to the basic plist and replace:

/usr/local/MacGPG2/bin/gpg-connect-agent

with the relevant path from your Apple or Intel Mac.

I understand all of this part ^^^^^^^^^^, but in my case I am using GPG Suite on my mac, which is the recommended way on the Dr. Duh Yubikey guide. With GPG Suite installed, the paths in that file are correct, as you can see below:

ls -l /usr/local/MacGPG2/bin/gpg-connect-agent
-rwxr-xr-x 1 root wheel 360064 Sep 26  2024 /usr/local/MacGPG2/bin/gpg-connect-agent
which gpg-connect-agent
/usr/local/MacGPG2/bin/gpg-connect-agent

So I don't think I need to replace anything, since GPG Suite has installed gpg-connect-agent in the same paths according to the plist file.

As for this text below, I do not understand it much. What do you mean the heebies? Are you implying its dangerous, or a security concern? Does this apply to me even if GPG Suite is installed with those paths listed in the plist file?

Next, if I install the modified content at the path:

$HOME/Library/LaunchAgents/gnupg.gpg-agent.plist

then there is no need to "load" it. That's because current macOS senses the addition of a plist dynamically. You should get a notification telling you that a background item has been added to the "Allow in the Background" list in the "Login Items" panel in "System Settings".

However, if you want to remove such an item then I believe that the recommended approach is to unload it before removing:

$ launchctl unload ~/Library/LaunchAgents/gnupg.gpg-agent.plist
$ rm ~/Library/LaunchAgents/gnupg.gpg-agent.plist

On the topic of sudo, don't do that. This is a user launch agent, not a system launch agent.

Last thing. As far as I know, kickstart has replaced bootstrap for most situations. It's a bit tricky with user launch agents but, for this situation, the appropriate command would be:

$ launchctl kickstart -k gui/$(id -u)/gnupg.gpg-agent

The id -u returns your numeric user ID, and the tail end of the path is the Label field in the plist.

Although I did install gnupg.gpg-agent.plist while I was writing the above, I didn't try the "symlink.plist". I studied it for a while but it gives me the heebies.

This is the bit that troubles me:

/bin/ln -sf $HOME/.gnupg/S.gpg-agent.ssh $SSH_AUTH_SOCK

Firstly, to the best of my knowledge, the first command argument S.gpg-agent.ssh is a socket that will only exist if the YubiKey has been inserted, unlocked and used. Certainly, none of the socket files is present on my system after a reboot until first use of the YubiKey. That implies any launch agent which assumes the socket always exists will fail when it doesn't.

However, let's assume the socket exists:

$ ls -l $HOME/.gnupg/S.gpg-agent.ssh
srwx------  1 user  group  0 May 20 16:34 $HOME/.gnupg/S.gpg-agent.ssh

Secondly, I do not see how a Launch Agent can "see" the SSH_AUTH_SOCK environment variable so, at face value, it should be blank so the command would reduce to:

$ /bin/ln -sf $HOME/.gnupg/S.gpg-agent.ssh

Executing that results in:

$ ls -l $HOME/.gnupg/S.gpg-agent.ssh
lrwxr-xr-x  1 user  group  37 May 20 17:17 /$HOME/.gnupg/S.gpg-agent.ssh -> $HOME/.gnupg/S.gpg-agent.ssh

So now it's a symlink pointing to itself and the socket is either masked or destroyed (I don't know which). What I do know is that it doesn't work:

$ file $HOME/.gnupg/S.gpg-agent.ssh
$HOME/.gnupg/S.gpg-agent.ssh: broken symbolic link to $HOME/.gnupg/S.gpg-agent.ssh

Computer says no! The only "fix" I've been able to find is to remove that file, reboot, and make the YubiKey do something.

It gets weirder. Let's assume the launch agent can somehow see SSH_AUTH_SOCK. What should its value be? Hunting around the README:

export SSH_AUTH_SOCK=$(gpgconf --list-dirs agent-ssh-socket)

Let's run the meat of that:

$ gpgconf --list-dirs agent-ssh-socket
$HOME/.gnupg/S.gpg-agent.ssh

In short, as far as I can tell, if we assume the Launch Agent can see SSH_AUTH_SOCK the command would become:

ln -sf $HOME/.gnupg/S.gpg-agent.ssh $HOME/.gnupg/S.gpg-agent.ssh

which is still a symlink pointing to itself. Thus, irrespective of whether a launch agent can or can't see SSH_AUTH_SOCK, we still wind up with a masked socket and a broken symlink.

See why it gives me the heebies?

Not on my system! Well, not until someone more knowledgeable than me can explain why any of this makes sense.

Hope this helps.

Thanks for any help!

Danrancan avatar May 31 '25 00:05 Danrancan

Doh!

According to what is in my GnuPG keychain, I set up my YubiKeys in late 2019. I'm pretty sure the notes I made at the time where what I followed when setting up new computers since then. By that I mean I did not go back to the DrDuh guide and start from scratch, so that may well explain why you have a path with MacGPG2 in the middle while my computers have homebrew. That'll teach me to make unwarranted MyMac=YourMac assumptions and draw the wrong conclusions! My bad.

That said, I could not actually replicate your Load failed: 5: Input/output error when I did the copy/paste to create the property list and installed it in the LaunchAgents directory, so I started going line by line to see what jumped out, which was the MacGPG2 issue.

I've just realised that if I had read the lines before the plist more carefully, I might've twigged that they were relevant to both the gpg-agent.conf and the plist. My bad again.

One of the problems with launchd things is that, compared with cron, I've never found its debugging facilities to be useful. With cron on the other hand, any spurious output (which generally means errors) winds up in a local mail message. In theory you can instrument launch agents to redirect stdout and stderr to files. Although the files get created, I've yet to see anything get written to them, which is exactly what happened when I tried the plist with MacGPG2 in situ - nada. Every assistance short of actual help.

In retracing my steps, it occurred to me to try to make a deliberate mistake with the copy/paste so that the property list was mal-formed. Just by deleting the last > in the file and trying a load I get:

Load failed: 5: Input/output error
Try running `launchctl bootstrap` as root for richer errors.

I can get confirmation of the mal-formed file by running:

$ plutil -p gnupg.gpg-agent.plist 
gnupg.gpg-agent.plist: Encountered unexpected EOF

So maybe your situation was (and perhaps still is) as simple as that: a typo in the plist.


You included sudo in the "text below".

The sudo command means "become root and then do the command". The process of becoming root changes a lot of things about your process environment and doing things as root when it isn't necessary is the cause of many problems. Try this:

$ env | sort >user.env ; sudo env | sort >root.env ; diff -y user.env root.env

You'll notice that an awful lot of the environment goes away when you "become root".

If you have a Linux box handy, also try running the same commands there. One of the things that can trap script-writers is $HOME doesn't change on macOS whereas it does change on Linux. It's a tiny difference but quite significant.

In the context of a Launch Agent, it actually makes no sense to use sudo. The idea of a Launch Agent is that it is running on behalf of, and with the privileges of, the user, not root. If something needs root privileges then you need a Launch Daemon.

More to come...

Paraphraser avatar Jun 01 '25 13:06 Paraphraser

Now I'll move to the "heebies" bit.

You know that lawyer's adage about never asking a question unless you already know the answer? That's more or less how I approach computers. I like to have a fair idea of how something is going to behave before I start using it.

When I see a command like this buried in a Launch Agent, I want to know what it does before I consider using it:

/bin/ln -sf $HOME/.gnupg/S.gpg-agent.ssh $SSH_AUTH_SOCK

First off, Launch Agent plists can't substitute variables. That's OK in this case because the whole command is an argument to sh -c which means sh will be doing the variable substitution. When I was writing my original reply I ran the command:

$ echo $SSH_AUTH_SOCK

and got nothing, which is why I concluded that the Launch Agent wouldn't see anything.

I've just run that same command again and now I'm getting:

/private/tmp/com.apple.launchd.zJWr9wHH0R/Listeners

There are two plausible explanations for this. The first is a dumb-head typo by me which I didn't notice when I was writing my earlier reply. The other is that I was running macOS Sonoma 14.7.1 at the time, but have since upgraded to Sequoia 15.5 so maybe that upgrade "fixed" something that I didn't even know was broken. This is a 2019 iMac I'm working on and it's accumulated a lot of software mileage.

Whatever the explanation, I'll expand on what I was thinking then and what I think now.

One bit of context is something I mentioned before:

  • at least on my system, S.gpg-agent.ssh doesn't exist after a reboot until I "do something" with gpg that involves the YubiKey (a gpg -K seems to be enough and I infer that's because the private key stubs in the local keychain point to the YubiKey). My assumption is that that's the purpose of this in the first plist:

    pg-connect-agent /bye
    

Anyway, when I was writing my earlier reply, it was in the belief that SSH_AUTH_SOCK was null, so the possibilities were:

  1. S.gpg-agent.ssh doesn't exist; or
  2. S.gpg-agent.ssh exists (as a socket).

In each case, the f (force) in ln -sf will result in S.gpg-agent.ssh as a symlink pointing to itself. If the socket existed (case 2), it will be destroyed. If the socket didn't exist (case 1) then when gpg tries to create the socket, it will be blocked. If you attempt to use your YubiKey in these situations, you get a barrage of error messages, none of which actually points you to the true cause. You just have to figure it out.

It's one thing to be experimenting and breaking things where you know what you just did and can undo the damage; quite another if this is happening behind the scenes and all you know is your YubiKey doesn't work.

That's what I meant by it giving me the heebies. I didn't see it as dangerous in the sense it could cause permanent damage, or a security concern in the sense that it could lead to a breach of some kind. I meant it in the sense of stuff going wrong where there's very little evidence to lead you towards a solution.

The above was then, so let's move forward to now.

Now that I'm seeing values for SSH_AUTH_SOCK I can probably assume that the second argument to ln -sf will be non-null. We still really only have two possibilities:

  • S.gpg-agent.ssh either exists (as a socket) or it doesn't.

In either case, the symlink will be created at the SSH_AUTH_SOCK path which implicitly destroys the original socket set up by macOS. If S.gpg-agent.ssh exists then any reference to the SSH_AUTH_SOCK path will find the socket at S.gpg-agent.ssh and everything will be sweet.

However, if S.gpg-agent.ssh does not exist then what was a socket at SSH_AUTH_SOCK will be a symlink pointing nowhere. My guess is the assumption underlying the two plists is that it doesn't really matter which order they run in (my understanding is the launch order is undefined), it won't take long before the symlink at SSH_AUTH_SOCK will be pointing to S.gpg-agent.ssh which, hopefully, is a socket.

Unless there is some (small) time window during which the symlink at SSH_AUTH_SOCK has been created and when S.gpg-agent.ssh is created, and some key process happens to run in that window that will fail if a socket isn't there, the strategy is probably fairly safe. So not giving me quite so many heebies.

That said, if I had been writing something like these plists I would likely have done it as a single Launch Agent pointing to a script supplied via this Dr Duh repo to be installed somewhere like ~/.gnupg. That script would have done its own search for gpg-connect-agent (so nobody had to know Intel vs Apple Silicon), plus had all the necessary error-checking to make sure it didn't do anything risky like create a symlink where the source and target didn't exist. In other words, saying it doesn't give me quite so many heebies is not the same as saying heebee-free.

One final observation. I've never had either of these plists or anything remotely like them installed on any Mac. My YubiKeys have worked just fine for over 5 years. And that's because my $HOME/.gnupg/gpg-agent.conf contains:

  • for Intel pinentry-program /usr/local/bin/pinentry-tty
  • for Apple Silicon pinentry-program /opt/homebrew/bin/pinentry-tty

The excellent Dr Duh Guide says:

pinentry-tty set as the pinentry program (in gpg-agent.conf) is reported to cause problems with Mutt TUI, because it uses curses; use pinentry-curses or other graphic pinentry program instead.

I don't know what Mutt TUI is so that caution doesn't trouble me. The guide also says (with an implicit contradiction when it comes to pinentry-curses):

Any pinentry program except pinentry-tty or pinentry-curses may be used. This is because local gpg-agent may start headlessly (by systemd without $GPG_TTY set locally telling which tty it is on), thus failed to obtain the pin. Errors on the remote may be misleading saying that there is IO Error. (Yes, internally there is actually an IO Error since it happens when writing to/reading from tty while finding no tty to use, but for end users this is not friendly.)

I've never had a problem like this. As far as I'm concerned, the tty PIN entry is perfect. In Terminal I'm running some CLI command which involves the YubiKey and the prompt to unlock the card appears inline exactly where my attention is already focused:

Please unlock the card

Number: 99 999 999
Holder: My Name Here
Counter: 999
PIN: 

In other words, I don't have some random dialog appearing elsewhere on screen where I have to change focus to the dialog, then change back to the Terminal window.

I know pinentry-mac follows the Apple "security dialog" norm. It irks me when Apple forces this change of focus on me too.

More to the point, pinentry-tty doesn't depend on those two plists being installed so none of these issues arise.

Still, as I always say: your system, your rules.

Hope this helps.

Paraphraser avatar Jun 02 '25 06:06 Paraphraser

Please see also:

I've been mulling over the earlier discussion in this issue. The bash script at the above repo is an attempt to deal with the problem of the two launch agents that give me the heebies. With the proposed script, if pinentry-mac is being selected, the result is a single launch agent which invokes a delegate which, in turn, performs basic sanity checking (like "is SSH_AUTH_SOCK defined?" and "is S.gpg-agent.ssh a socket or has it been futzed with in some way?") before just ploughing ahead, and writes log messages where it encounters problems (as opposed to the current launch agents which do zero checking and emit no logs).

While solving those problems, I realised that it would be useful to automate the switching of pinentry programs and, from that, an appreciation that the existing hardened config didn't have a good list the candidates likely to be encountered by most users - if they were hand-editing the file. I also realised that the default setting was only going to work on Linux. Solving all that is what #510 is geared towards.

I considered proposing a separate PR to add this script to the DrDuh scripts folder but it seemed to me it would be better to present it separately for comment and additional testing. Integrating it into DrDuh would involve several changes to the README and I'd rather first get some agreement on the approach, and (hopefully) that it doesn't throw up any situations I haven't foreseen on platforms I'm unable to test on.

Paraphraser avatar Jun 18 '25 06:06 Paraphraser