This guide will show you how to sign, encrypt, and decrypt content where GPG is in a Coder workspace while the private key is on your local machine.
Step 1: Configure your local machine
We assume that you're already capable of using and signing GPG on your local machine.
The examples in this guide were created using macOS 11 (Big Sur); Windows and Linux users may need to modify the provided instructions.
First, make sure that you've:
- Installed GnuPG (GPG) using Homebrew or gpg-suite
- Verified that
pinentryis installed (if not, install
- Created or imported both your public and private GPG keys
You can verify your GnuPG installation and version number as follows:
gpg --version gpg (GnuPG) 2.3.1
When running any
gpg command, your system knows to start
creates the sockets needed and performs the cryptographic activity. However, if
you connect to a workspace via SSH using the
-R flag to remote forward the
sockets, your local
gpg-agent won't start automatically since this process
doesn't invoke the
To address this issue, add the gpg-agent to your local
.zshrc, or configuration script that runs for each terminal session:
gpgconf --launch gpg-agent
Alternatively, you can run
gpg-agent --daemon to prepare your local system.
If you don't perform either of the steps above, there won't be sockets for
mounting and the remote
gpg command won't work (instead, you'll end up
starting an agent in the remote system that has no keys).
Step 2: Configure Coder
The following steps must be performed by a Coder user assigned the site manager role.
To use GPG agent forwarding, ensure that you've enabled:
- SSH access to workspaces;
you must use OpenSSH (the basic
libsshserver doesn't support forwarding)
- Container-based virtual machines (CVMs);
CVMs are required to run
systemd, which is required for OpenSSH to start
Step 3: Configure a Coder image to support GPG forwarding
Update the image on which your workspace is based to include the following dependencies for GPG forwarding:
StreamLocalBindUnlink yesset in the
- Socket masking
OpenSSHenabled (so that Coder doesn't inject its own ssh daemon)
Your updated Dockerfile would look something like:
FROM ubuntu:20:04 RUN apt-get update && \ DEBIAN_FRONTEND="noninteractive" apt-get install --yes \ openssh-server \ gnupg2 \ systemd \ systemd-sysv RUN echo "StreamLocalBindUnlink yes" >> /etc/ssh/sshd_config && \ systemctl --global mask gpg-agent.service \ gpg-agent.socket gpg-agent-ssh.socket \ gpg-agent-extra.socket gpg-agent-browser.socket && \ systemctl enable ssh
Alternatively, you can create a new image from scratch. If so, we recommend starting with Coder's Enterprise Base image, which helps establish dependencies and conventions that improves the Coder user experience.
If you use the Enterprise Base image as your starting point:
apt-get install gnupg2 openssh-server
Add the following to the Dockerfile:
RUN echo "StreamLocalBindUnlink yes" >> /etc/ssh/sshd_config && \ systemctl --global mask gpg-agent.service \ gpg-agent.socket gpg-agent-ssh.socket \ gpg-agent-extra.socket gpg-agent-browser.socket && \ systemctl enable ssh
Step 4: Create/update a workspace using the image
Once you've created your image, you can import it for use. When creating a workspace using that image, be sure to create a CVM-enabled workspace.
Step 5: Define the workspace-startup configurations using dotfiles
The configuration detailed in this section must be be run after you've created and started your workspace (the configurations must be run within the context of your user). We recommend defining your configuration using Coder personalization scripts (otherwise known as dotfiles).
To use your local private key on the remote Coder workspace, you must provide
the workspace a reference to the public key and the key must be trusted. You
must also account for the fact that not all images will include GPG. To do both,
add the following to an
install.sh script, then add the file to your dotfiles
if hash gpg 2>/dev/null; then echo "gpg found, configuring public key" gpg --import ~/dotfiles/.gnupg/mterhar_coder.com-publickey.asc echo "16AD...B84AC:6:" | gpg --import-ownertrust git config --global user.signingkey F371232FA31B84AC echo "pinentry-mode loopback" > ~/.gnupg/gpg.conf echo "export GPG_TTY=\$(tty)" > ~/.profile echo "to enable commit signing, run" echo "git config --global commit.gpgsign true" else echo "gpg not found, no git signing" fi
Notes regarding the sample script:
- Adding the public key export directly to the dotfiles repository (as shown in the example) allows it to be imported.
gpg --import-ownertrustcommand gets the fingerprint of the key that was just imported with a trust level of
6(this indicates a trust level of ultimate).
"pinentry-mode loopback" > ~/.gnupg/gpg.confallows the remote system to trigger
pinentryinline so that you can type your passphrase into the same terminal where you're running the GPG command to unlock the mounted socket.
pinentrytime to send the request for a passphrase to the correct place. The use of a single
>prevents that line from being added to
.profilerepeatedly, though anything you have in the file will be erased.
Step 6: Connect to Coder
On your local machine, ensure that
gpg-agent is running and that it works when
you attempt to perform a GPG action (e.g.,
echo "test" | gpg --clearsign).
Note that you'll be prompted to provide your pin; as such, the socket will be
open for a bit unless you kill and restart the GPG agent.
gpg-agent and connect to Coder:
gpgconf --launch gpg-agent coder config-ssh ssh -R /run/user/1000/gnupg/S.gpg-agent:/Users/mterhar/.gnupg/S.gpg-agent coder.<workspace name>
At this point, there is a connection from your local filesystem socket to the remote filesystem socket, so you can begin running GPG actions:
echo "test " | gpg --clearsign -v gpg: using character set 'utf-8' gpg: using pgp trust model gpg: key F371232FA31B84AC: accepted as trusted key gpg: writing to stdout -----BEGIN PGP SIGNED MESSAGE----- Hash: SHA256 test gpg: EDDSA/SHA256 signature from: "F371232FA31B84AC Mike Terhar <[email protected]>" -----BEGIN PGP SIGNATURE----- iHUEARYIAB0WIQQWraRO2qW8c4RXhlTzcSMvoxuErAUCYPm2fwAKCRDzcSMvoxuE rHYNAQCrGPbF9Z89dDjemFMtgt0dfsPSUcAlgVj1PKGsg/K8lgEAj8MeTXi1RQhv dqbC8blPKTAzupH7OeQpe6EbweZHjAI= =tgC/ -----END PGP SIGNATURE-----
If you decide to run a web terminal or use the terminal within code-server, you'll be prompted for to enter your pin and to use the SSH socket (this is true for terminals that are running from different devices as well).
Example: GPG forwarding action
The following is an example of what a GPG forwarding action looks like:
gpgconf --launch gpg-agent ssh -R /run/user/1000/gnupg/S.gpg-agent:/Users/mterhar/.gnupg/S.gpg-agent coder.gpg Welcome to Ubuntu 20.04.2 LTS (GNU/Linux 5.4.0-1039-gke x86_64) * Documentation: https://help.ubuntu.com * Management: https://landscape.canonical.com * Support: https://ubuntu.com/advantage This system has been minimized by removing packages and content that are not required on a system that users do not log into. To restore this content, you can run the 'unminimize' command. Last login: Thu Jul 22 18:17:57 2021 from 127.0.0.1 echo "test " | gpg --clearsign -v gpg: using character set 'utf-8' gpg: using pgp trust model gpg: key F371232FA31B84AC: accepted as trusted key gpg: writing to stdout -----BEGIN PGP SIGNED MESSAGE----- Hash: SHA256 test gpg: EDDSA/SHA256 signature from: "F371232FA31B84AC Mike Terhar <[email protected]>" -----BEGIN PGP SIGNATURE----- iHUEARYIAB0WIQQWraRO2qW8c4RXhlTzcSMvoxuErAUCYPm2fwAKCRDzcSMvoxuE rHYNAQCrGPbF9Z89dDjemFMtgt0dfsPSUcAlgVj1PKGsg/K8lgEAj8MeTXi1RQhv dqbC8blPKTAzupH7OeQpe6EbweZHjAI= =tgC/ -----END PGP SIGNATURE-----
To sign Git commits via the command line:
git commit -m "trigger signature" [gpg-test 2ece8ea] trigger signature 1 file changed, 2 insertions(+) git verify-commit 2ece8ea gpg: Signature made Thu Jul 22 19:15:50 2021 UTC gpg: using EDDSA key 16ADA44EDAA5BC7384578654F371232FA31B84AC gpg: Good signature from "Mike Terhar <[email protected]>" [ultimate]
Now, when you push commits to GitHub/GitLab, you'll see that the commits are flagged as verified.
Using a Yubikey or other smart card
The Yubikey configurations required to make GPG work with the local machine are all that is necessary to use it as a smart card.
Once you've configured Yibikey, you can follow the steps detailed in this
article to set up GPG forwarding; the only difference is that you should provide
pinentry with your Yubikey PIN, not the private key passphrase.
As soon as the cryptographic action is complete, be sure remove the Yubikey from the USB port to prevent any additional cryptographic actions from occurring through the GPG forwarding socket.
Limitations for code-server users
The Git functionality in code-server will sign the commit and obey the
.gitconfig file. However, it lacks the ability to ask for a GPG pin, so the
forwarding process only works if the socket is already open due to some other
activity. For example, the following Git CLI command would typically prompt you
to unlock the GPG key:
git verify-commit <commit>
However, if the socket isn't already open, you'd get an error saying
Git: gpg failed to sign the data, even if the configuration setting is
Any time you use a private key, you expose it to the systems that are granted access to the key.
Furthermore, actions such as typing the passphrase or using
gpg-preset-passphrase to keep the socket open each have different risk
profiles associated (e.g., the risk of someone looking over your shoulder and
the risk of someone accessing the system with open socket from another
The following are steps you can take to minimize your risk:
default-cache-ttl 30, which will prompt you for your PIN more frequently. While the signing activity only takes a short amount of time to complete, the GPG socket remains open longer.
Connect to the local
.extrasocket rather than the primary socket, which helps limit key exposure (if you do this, modify examples in this article to use the appropriate socket).
Create a separate sub-key for Coder to use to prevent the primary key from being compromised if a security incident occurs. You'll need to add the sub-keys to your Git provider, and if there's a security incident, the old commits signed using the affected keys may be considered unverified.
As of Coder v1.22.x, running
coder config-sshenables the
ControlMastermechanism, which caches connections even you exit the interactive shell. This means that GPG actions on the remote system can occur even if there's no apparent connection. To disable
ControlMasteron your GPG-forwarded SSH connection, add the following options to your command:
-o ControlMaster=no -o ControlPath=none.
The following sections explain how you can troubleshoot errors you may see when using up GPG forwarding.
Unable to connect to the default agent port
connect to /Users/mterhar/.gnupg/S.gpg-agent port -2 failed: No such file or directory gpg: no running gpg-agent - starting '/usr/bin/gpg-agent'
If you see this error, the socket wasn't present on the local machine when you
ssh command. This is caused by a lack of
ssh configuration, so update your configuration accordingly.
No secret key
gpg: key F371232FA31B84AC: accepted as trusted key gpg: no default secret key: No secret key gpg: [stdin]: clear-sign failed: No secret key
This error can happen if there's a
gpg agent running in the remote workspace
that is intercepting the GPG commands before they get to the remote socket.
You can fix this by:
gpgconf --kill gpg-agent
ps ax | grep gpg-agentto find and kill all of the pids.
Then, reconnect your
ssh session to re-establish the socket forwarding.
Inappropriate ioctl for the device
echo "test " | gpg --clearsign -vvv gpg: using character set 'utf-8' gpg: using pgp trust model gpg: key F371232FA31B84AC: accepted as trusted key gpg: writing to stdout -----BEGIN PGP SIGNED MESSAGE----- Hash: SHA256 test gpg: pinentry launched (1744 curses 1.1.1 - xterm-256color - - 501/20 0) gpg: signing failed: Inappropriate ioctl for device gpg: [stdin]: clear-sign failed: Inappropriate ioctl for device
gpg: pinentry launched (1744 curses 1.1.1 - xterm-256color - - 501/20 0)
does not include the
/dev/pts/1 after the version number, you may need to add
GPG_TTY environment variable to a process that runs before trying to use
GPG_TTY is set to the same output as
tty, be sure you have a
.gnupg/gpg.conf file that contains
SSH error with remote port forwarding
If you receive this error when connecting via SSH:
Warning: remote port forwarding failed for listen path /run/user/1000/gnupg/S.gpg-agent
The likely cause is that
openssh isn't running. This could be a result of:
- The image you're using doesn't include
systemctl enable sshcommand didn't work
- The workspace doesn't have CVMs enabled.
Unverified commits in GitHub or GitLab
Both GitHub and GitLab display verification statuses beside signed commits. If you see a commit that's unverified, it could be that the signing key hasn't been uploaded to the associated account.
To fix this issue, add the GPG key to your account:
If this doesn't fix the issue, ensure that the email address in the author field matches the email associated with the username and signing key.
The signing still works after disconnecting the session
coder config-ssh command uses session caching:
Host coder.[workspace name] [...] ControlMaster auto ControlPath ~/.ssh/.connection-%[email protected]%h:%p ControlPersist 600
Therefore, the connection persists for some time and the GPG socket forwarding remains open to make opening a new shell fast.
If you're having issues with GPG forwarding, getting verbose logs is helpful for
pinpointing where the issue may be. One way to do so is to add
-v to the SSH
command you run.
You can also add
--verbose to the
gpg command. For example, if your sockets
aren't where you expected them and you receive the following output, you'll need
to get additional information via verbose logs:
gpgconf --list-dirs sysconfdir:/etc/gnupg bindir:/usr/bin libexecdir:/usr/lib/gnupg libdir:/usr/lib/x86_64-linux-gnu/gnupg datadir:/usr/share/gnupg localedir:/usr/share/locale socketdir:/run/user/1000/gnupg dirmngr-socket:/run/user/1000/gnupg/S.dirmngr agent-ssh-socket:/run/user/1000/gnupg/S.gpg-agent.ssh agent-extra-socket:/run/user/1000/gnupg/S.gpg-agent.extra agent-browser-socket:/run/user/1000/gnupg/S.gpg-agent.browser agent-socket:/run/user/1000/gnupg/S.gpg-agent homedir:/home/coder/.gnupg