How to make gnupg2 to fall in love with Docker
April 03, 2017While developing my new replacement of self-hosted-mailserver, I noticed a funny problem - I couldn't make gnupg2 to work with docker non-interactively. At first, the problem was with an import:
# gpg --import "/tmp/private.key"
Step 1/4 : FROM alpine
---> 4a415e366388
Step 2/4 : RUN apk add --no-cache wget gnupg
---> Using cache
---> 724679f224aa
Step 3/4 : ADD private.key /tmp/
---> Using cache
---> 49c8b89aecc7
Step 4/4 : RUN gpg --import /tmp/private.key
---> Running in 9a45499d851f
gpg: directory '/root/.gnupg' created
gpg: new configuration file '/root/.gnupg/dirmngr.conf' created
gpg: new configuration file '/root/.gnupg/gpg.conf' created
gpg: keybox '/root/.gnupg/pubring.kbx' created
gpg: /root/.gnupg/trustdb.gpg: trustdb created
gpg: key A7AD7E10789C6F1E: public key "testete <test@example.com>" imported
gpg: key A7AD7E10789C6F1E/F7886F60959E549E: error sending to agent: Not a tty
gpg: Total number processed: 1
gpg: imported: 1
gpg: secret keys read: 1
The command '/bin/sh -c gpg --import /tmp/private.key' returned a non-zero code: 2
You may notice error sending to agent: Not a tty
error. This one was actually easy, there is --batch
parameter
which forces gnupg2 to work in non-interactive mode:
# gpg --batch --import "/tmp/private.key"
Step 1/4 : FROM alpine
---> 4a415e366388
Step 2/4 : RUN apk add --no-cache wget gnupg
---> Using cache
---> 724679f224aa
Step 3/4 : ADD private.key /tmp/
---> Using cache
---> 49c8b89aecc7
Step 4/4 : RUN gpg --batch --import /tmp/private.key
---> Running in 986fd9ae42c0
gpg: directory '/root/.gnupg' created
gpg: new configuration file '/root/.gnupg/dirmngr.conf' created
gpg: new configuration file '/root/.gnupg/gpg.conf' created
gpg: keybox '/root/.gnupg/pubring.kbx' created
gpg: /root/.gnupg/trustdb.gpg: trustdb created
gpg: key A7AD7E10789C6F1E: public key "testete <test@example.com>" imported
gpg: key A7AD7E10789C6F1E: secret key imported
gpg: Total number processed: 1
gpg: imported: 1
gpg: secret keys read: 1
gpg: secret keys imported: 1
---> ba9639723f2c
Removing intermediate container 986fd9ae42c0
Successfully built ba9639723f2c
But the bigger problem arrives, when we try to decrypt something with a password protected key (to make codeblocks readable, I will only paste things which changed):
# gpg -r test@example.com -d "/tmp/file.gpg" > "/tmp/file"
Step 5/6 : ADD file.gpg /tmp/
---> 434115276622
Removing intermediate container 0ee978044580
Step 6/6 : RUN gpg -r test@example.com -d /tmp/file.gpg > /tmp/file
---> Running in a254a182e0f0
gpg: encrypted with 2048-bit RSA key, ID F7886F60959E549E, created 2017-04-03
"testete <test@example.com>"
gpg: public key decryption failed: Not a tty
gpg: decryption failed: No secret key
The command '/bin/sh -c gpg -r test@example.com -d /tmp/file.gpg > /tmp/file' returned a non-zero code: 2
Well, the most obvious thing to do is to add --batch
again and hope this helps:
# gpg --batch -r test@example.com -d "/tmp/file.gpg" > "/tmp/file"
Step 6/6 : RUN gpg --batch -r test@example.com -d /tmp/file.gpg > /tmp/file
---> Running in 66de260f1bf4
gpg: encrypted with 2048-bit RSA key, ID F7886F60959E549E, created 2017-04-03
"testete <test@example.com>"
gpg: public key decryption failed: Not a tty
gpg: decryption failed: No secret key
The command '/bin/sh -c gpg --batch -r test@example.com -d /tmp/file.gpg > /tmp/file' returned a non-zero code: 2
Still the same error! But why? Wasn't --batch
supposed to run gnupg2 in non-interactive mode? Well,
actually it was, and it did. But still it didn't know a password - I needed some way to pass it through. Thankfully,
gnupg2 supports an PASSPHRASE
environmental variable:
# PASSPHRASE="key-password" gpg --batch -r test@example.com -d "/tmp/file.gpg" > "/tmp/file"
Step 6/7 : ENV PASSPHRASE key-password
---> Running in 306b5791abcb
---> 0a3bc0bc1d76
Removing intermediate container 306b5791abcb
Step 7/7 : RUN gpg --batch -r test@example.com -d /tmp/file.gpg > /tmp/file
---> Running in 9311634f162b
gpg: encrypted with 2048-bit RSA key, ID F7886F60959E549E, created 2017-04-03
"testete <test@example.com>"
gpg: public key decryption failed: Not a tty
gpg: decryption failed: No secret key
The command '/bin/sh -c gpg --batch -r test@example.com -d /tmp/file.gpg > /tmp/file' returned a non-zero code: 2
At this point I was puzzled - clearly something was still missing. There is a GPG_TTY
env variable, but it won't
help much - passing it to a terminal when we can't provide user input does not bode well for success. And then it
hit me: it's not a gnupg2 which asks for a password - it's the pinentry application handled by the gpg-agent
which
kicks in in a process. Unfortunately disabling a gpg-agent
was not an option, as it's tightly integrated into
gnupg2 for some reason. Another possible thing was replacing the pinentry program to something else - but it was also
dead end. Every pinetry
program which I found, required me to entry the password at some point.
Finally, after hours of googling and ripping my last hairs out, I found some post which was describing a
--pinentry-mode loopback
option for gnupg2. Basically what it does, it wires password entering back to gnupg2.
Unfortunatelly it still needs to be provided (PASSPHRASE
didn't work), but I felt I was close.
And it happened to be true. There is another parameter to gnupg2 --command-fd
which expects a list of commands,
passed via given input. The input is identified by the number, so --command-fd 0
meant STDIN. All which was left,
was passing the password AGAIN, this time via the pipe.
Eventually I ended up with this nice, and simple command:
echo "key-password" | PASSPHRASE="key-password" gpg --batch \
--pinentry-mode loopback --command-fd 0 -r test@example.com \
-d /tmp/file.gpg > /tmp/file
And voilla! It worked!
echo "key-password" | PASSPHRASE="key-password" gpg --batch --pinentry-mode loopback --command-fd 0 -r test@example.com -d "/tmp/file.gpg" > "/tmp/file"
Step 6/8 : ENV PASSPHRASE key-password
---> Using cache
---> 0a3bc0bc1d76
Step 7/8 : RUN echo "key-password" | gpg --batch --pinentry-mode loopback --command-fd 0 -r test@example.com -d /tmp/file.gpg > /tmp/file
---> Using cache
---> 0b7caaabda65
Step 8/8 : RUN cat /tmp/file
---> Running in 598331a9ea4e
test
---> d14ea6fe5958
Removing intermediate container 598331a9ea4e
Successfully built d14ea6fe5958
The drawback is that those switches were introduced in GPG 2.1, so you're going to need a fairly fresh version of the app. But hey! We're talking cryptography here - you SHOULD use the most recent software available!
P.S. As a bonus, here is the full command for duplicity, for those folks, who want to build encrypted backups in Docker containers:
echo "key-password" | PASSPHRASE="key-password" duplicity \
--gpg-options "--pinentry-mode loopback --command-fd 0" \
--encrypt-key "KEY-ID" /backup/source protocol://host/backup/destination