Recently having docker login credentials stored in plain text on a server was bugging me. Granted the credentials are base64 encoded but easily decoded at which point the container registry is vulnerable as well. I thought using one of the credential helpers would be quick and simple but I was mistaken.
In my case I was using the pass
credential helper on a Docker Swarm manager running Ubuntu server. Part of the problem was that docker-credential-pass is not well-documented; I found various resources that got me most of the way there but the complete solution required info across several resources and none of them automated it as much as I wanted.
With that in mind I pieced together this docker-credentials.sh
script to make this easier. Most explanation of the script is inline with the comments.
#!/bin/sh # Sets up a docker credential helper so docker login credentials are not stored encoded in base64 plain text. # Uses the pass secret service as the credentials store. # # If previously logged in w/o cred helper, docker logout <registry> under each user or remove ~/.docker/config.json. # # For Swarm use just run once on a manager. # Script written for use on Ubuntu. # Run elevated logged in as the target user. # To remove cred helper: # - delete /usr/local/bin/docker-credential-pass # - remove '{ "credsStore": "pass" }' from ~/.docker/config.json # Ensure executable in git: # git update-index --chmod=+x path/docker-credentials.sh if ! [ $(id -u) = 0 ]; then echo "This script must be run as root" exit 1 fi # Install dependencies - jq more optional for existing varying configuration in ~/.docker/config.json. apt update && apt-get -y install gnupg2 pass rng-tools jq # Check for later releases at https://github.com/docker/docker-credential-helpers/releases version="v0.6.3" archive="docker-credential-pass-$version-amd64.tar.gz" url="https://github.com/docker/docker-credential-helpers/releases/download/$version/$archive" # Download cred helper, unpack, make executable, move it where it'll be found wget $url \ && tar -xf $archive \ && chmod +x docker-credential-pass \ && mv -f docker-credential-pass /usr/local/bin/ # Done with the archive rm -f $archive config_path=~/.docker config_filename=$config_path/config.json # Could assume config.json isn't there or overwrite regardless and not use jq (or sed etc.) # echo '{ "credsStore": "pass" }' > $config_filename if [ ! -f $config_filename ] then if [ ! -d $config_path ] then mkdir -p $config_path fi # Create default docker config file if it doesn't exist (never logged in etc.). Empty is fine currently. cat > $config_filename <<EOL { } EOL echo "$config_filename created with defaults" else echo "$config_filename already exists" fi # Whether config is new or existing, read into variable for easier file redirection (cat > truncate timing) config_json=`cat $config_filename` if [ -z "$config_json" ]; then # Empty file will prevent jq from working $config_json="{}" fi # Update Docker config to set the credential store. Used sed before but messy / edge cases. echo "$config_json" | jq --arg credsStore pass '. + {credsStore: $credsStore}' > $config_filename # Output / verify contents echo "$config_filename:" cat $config_filename | jq # Help with entropy to prevent gpg2 full key generation hang # Feeds data from a random number generator to the kernel's random number entropy pool rngd -r /dev/urandom # To cleanup extras from multiple runs: gpg --delete-secret-key <key-id>; gpg --delete-key <key-id> echo "Generating GPG key, accept defaults but consider key size to 2048, supply user info" gpg2 --full-generate-key echo "Adjusting permissions" sudo chown -R $USER:$USER ~/.gnupg sudo find ~/.gnupg -type d -exec chmod 700 {} \; sudo find ~/.gnupg -type f -exec chmod 600 {} \; # List keys gpg2 -k # Grab target key key=$(gpg2 --list-secret-keys | grep uid -B 1 | head -n 1 | sed 's/^ *//g') echo "Initializing pass with key $key" pass init $key # Image can't be found when Swarm attempts to pull later if a pass phrase is here. echo "Do not set a passphrase for this step (*IMPORTANT*)" pass insert docker-credential-helpers/docker-pass-initialized-check # Optionally show password but mask *** # pass show docker-credential-helpers/docker-pass-initialized-check | sed -e 's/\(.\)/\*/g' echo "Docker credential password list (empty initially):" docker-credential-pass list echo "Done. Ready to test. Run: docker login <registry>" echo "After login run: docker-credential-pass list; cat ~/.docker/config.json; pass show"
The initial sticking points were:
- docker-credential-helpers #186 –
docker-pass-initialized-check
didn’t seem to work with Docker swarm when a passphrase was used. The helper setup would work but despite using docker stack deploy--with-registry-auth
, credentials would no longer correctly sync with other nodes so the images couldn’t be found when starting services. - Needing to install gpg2 and pass.
- rng-tools was needed to generate enough entropy so gpg2 did not hang generating a new key pair.
- Needing to adjust permissions on
~/.gnupg
. - Automating initializing pass with the desired
gpg-id
.
Running the script on the server looked like this…
mr-robot@ubun2:~$ sudo ./docker-credentials.sh
Hit:1 http://azure.archive.ubuntu.com/ubuntu bionic InRelease Hit:2 http://azure.archive.ubuntu.com/ubuntu bionic-updates InRelease Hit:3 http://azure.archive.ubuntu.com/ubuntu bionic-backports InRelease Hit:4 http://security.ubuntu.com/ubuntu bionic-security InRelease Hit:5 https://download.docker.com/linux/ubuntu bionic InRelease Reading package lists... Done Building dependency tree Reading state information... Done 3 packages can be upgraded. Run 'apt list --upgradable' to see them. Reading package lists... Done Building dependency tree Reading state information... Done The following packages were automatically installed and are no longer required: grub-pc-bin linux-headers-4.15.0-117 Use 'sudo apt autoremove' to remove them. The following additional packages will be installed: libice6 libjq1 libonig4 libqrencode3 libsm6 libxmu6 libxt6 qrencode tree x11-common xclip Suggested packages: libxml-simple-perl ruby The following NEW packages will be installed: gnupg2 jq libice6 libjq1 libonig4 libqrencode3 libsm6 libxmu6 libxt6 pass qrencode rng-tools tree x11-common xclip 0 upgraded, 15 newly installed, 0 to remove and 3 not upgraded. Need to get 726 kB of archives. After this operation, 2604 kB of additional disk space will be used. Get:1 http://azure.archive.ubuntu.com/ubuntu bionic-updates/main amd64 x11-common all 1:7.7+19ubuntu7.1 [22.5 kB] Get:2 http://azure.archive.ubuntu.com/ubuntu bionic/main amd64 libice6 amd64 2:1.0.9-2 [40.2 kB] Get:3 http://azure.archive.ubuntu.com/ubuntu bionic/main amd64 libsm6 amd64 2:1.2.2-1 [15.8 kB] Get:4 http://azure.archive.ubuntu.com/ubuntu bionic/universe amd64 libonig4 amd64 6.7.0-1 [119 kB] Get:5 http://azure.archive.ubuntu.com/ubuntu bionic/universe amd64 libjq1 amd64 1.5+dfsg-2 [111 kB] Get:6 http://azure.archive.ubuntu.com/ubuntu bionic/universe amd64 jq amd64 1.5+dfsg-2 [45.6 kB] Get:7 http://azure.archive.ubuntu.com/ubuntu bionic/universe amd64 libqrencode3 amd64 3.4.4-1build1 [23.9 kB] Get:8 http://azure.archive.ubuntu.com/ubuntu bionic/main amd64 libxt6 amd64 1:1.1.5-1 [160 kB] Get:9 http://azure.archive.ubuntu.com/ubuntu bionic/main amd64 libxmu6 amd64 2:1.1.2-2 [46.0 kB] Get:10 http://azure.archive.ubuntu.com/ubuntu bionic-updates/universe amd64 gnupg2 all 2.2.4-1ubuntu1.2 [4668 B] Get:11 http://azure.archive.ubuntu.com/ubuntu bionic/universe amd64 tree amd64 1.7.0-5 [40.7 kB] Get:12 http://azure.archive.ubuntu.com/ubuntu bionic/universe amd64 pass all 1.7.1-3 [36.3 kB] Get:13 http://azure.archive.ubuntu.com/ubuntu bionic/universe amd64 qrencode amd64 3.4.4-1build1 [20.4 kB] Get:14 http://azure.archive.ubuntu.com/ubuntu bionic/universe amd64 rng-tools amd64 5-0ubuntu4 [22.5 kB] Get:15 http://azure.archive.ubuntu.com/ubuntu bionic/main amd64 xclip amd64 0.12+svn84-4build1 [17.5 kB] Fetched 726 kB in 0s (15.7 MB/s) Selecting previously unselected package x11-common. (Reading database ... 76844 files and directories currently installed.) Preparing to unpack .../00-x11-common_1%3a7.7+19ubuntu7.1_all.deb ... dpkg-query: no packages found matching nux-tools Unpacking x11-common (1:7.7+19ubuntu7.1) ... Selecting previously unselected package libice6:amd64. Preparing to unpack .../01-libice6_2%3a1.0.9-2_amd64.deb ... Unpacking libice6:amd64 (2:1.0.9-2) ... Selecting previously unselected package libsm6:amd64. Preparing to unpack .../02-libsm6_2%3a1.2.2-1_amd64.deb ... Unpacking libsm6:amd64 (2:1.2.2-1) ... Selecting previously unselected package libonig4:amd64. Preparing to unpack .../03-libonig4_6.7.0-1_amd64.deb ... Unpacking libonig4:amd64 (6.7.0-1) ... Selecting previously unselected package libjq1:amd64. Preparing to unpack .../04-libjq1_1.5+dfsg-2_amd64.deb ... Unpacking libjq1:amd64 (1.5+dfsg-2) ... Selecting previously unselected package jq. Preparing to unpack .../05-jq_1.5+dfsg-2_amd64.deb ... Unpacking jq (1.5+dfsg-2) ... Selecting previously unselected package libqrencode3:amd64. Preparing to unpack .../06-libqrencode3_3.4.4-1build1_amd64.deb ... Unpacking libqrencode3:amd64 (3.4.4-1build1) ... Selecting previously unselected package libxt6:amd64. Preparing to unpack .../07-libxt6_1%3a1.1.5-1_amd64.deb ... Unpacking libxt6:amd64 (1:1.1.5-1) ... Selecting previously unselected package libxmu6:amd64. Preparing to unpack .../08-libxmu6_2%3a1.1.2-2_amd64.deb ... Unpacking libxmu6:amd64 (2:1.1.2-2) ... Selecting previously unselected package gnupg2. Preparing to unpack .../09-gnupg2_2.2.4-1ubuntu1.2_all.deb ... Unpacking gnupg2 (2.2.4-1ubuntu1.2) ... Selecting previously unselected package tree. Preparing to unpack .../10-tree_1.7.0-5_amd64.deb ... Unpacking tree (1.7.0-5) ... Selecting previously unselected package pass. Preparing to unpack .../11-pass_1.7.1-3_all.deb ... Unpacking pass (1.7.1-3) ... Selecting previously unselected package qrencode. Preparing to unpack .../12-qrencode_3.4.4-1build1_amd64.deb ... Unpacking qrencode (3.4.4-1build1) ... Selecting previously unselected package rng-tools. Preparing to unpack .../13-rng-tools_5-0ubuntu4_amd64.deb ... Unpacking rng-tools (5-0ubuntu4) ... Selecting previously unselected package xclip. Preparing to unpack .../14-xclip_0.12+svn84-4build1_amd64.deb ... Unpacking xclip (0.12+svn84-4build1) ... Setting up tree (1.7.0-5) ... Setting up libonig4:amd64 (6.7.0-1) ... Setting up pass (1.7.1-3) ... Setting up libqrencode3:amd64 (3.4.4-1build1) ... Setting up rng-tools (5-0ubuntu4) ... Setting up libjq1:amd64 (1.5+dfsg-2) ... Setting up gnupg2 (2.2.4-1ubuntu1.2) ... Setting up x11-common (1:7.7+19ubuntu7.1) ... update-rc.d: warning: start and stop actions are no longer supported; falling back to defaults Setting up qrencode (3.4.4-1build1) ... Setting up jq (1.5+dfsg-2) ... Setting up libice6:amd64 (2:1.0.9-2) ... Setting up libsm6:amd64 (2:1.2.2-1) ... Setting up libxt6:amd64 (1:1.1.5-1) ... Setting up libxmu6:amd64 (2:1.1.2-2) ... Setting up xclip (0.12+svn84-4build1) ... Processing triggers for systemd (237-3ubuntu10.42) ... Processing triggers for man-db (2.8.3-2ubuntu0.1) ... Processing triggers for ureadahead (0.100.0-21) ... Processing triggers for libc-bin (2.27-3ubuntu1.2) ... --2020-09-15 22:10:46-- https://github.com/docker/docker-credential-helpers/releases/download/v0.6.3/docker-credential-pass-v0.6.3-amd64.tar.gz Resolving github.com (github.com)... 140.82.114.4 Connecting to github.com (github.com)|140.82.114.4|:443... connected. HTTP request sent, awaiting response... 302 Found Location: https://github-production-release-asset-2e65be.s3.amazonaws.com/51309425/d50f9700-a88b-11e9-8807-ac23deaea4f0?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIWNJYAX4CSVEH53A%2F20200915%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20200915T221047Z&X-Amz-Expires=300&X-Amz-Signature=44165cc88f45df3685cc8e4b93bd906b44a37a6aa0f5a4a978c360107b44d210&X-Amz-SignedHeaders=host&actor_id=0&key_id=0&repo_id=51309425&response-content-disposition=attachment%3B%20filename%3Ddocker-credential-pass-v0.6.3-amd64.tar.gz&response-content-type=application%2Foctet-stream [following] --2020-09-15 22:10:47-- https://github-production-release-asset-2e65be.s3.amazonaws.com/51309425/d50f9700-a88b-11e9-8807-ac23deaea4f0?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIWNJYAX4CSVEH53A%2F20200915%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20200915T221047Z&X-Amz-Expires=300&X-Amz-Signature=44165cc88f45df3685cc8e4b93bd906b44a37a6aa0f5a4a978c360107b44d210&X-Amz-SignedHeaders=host&actor_id=0&key_id=0&repo_id=51309425&response-content-disposition=attachment%3B%20filename%3Ddocker-credential-pass-v0.6.3-amd64.tar.gz&response-content-type=application%2Foctet-stream Resolving github-production-release-asset-2e65be.s3.amazonaws.com (github-production-release-asset-2e65be.s3.amazonaws.com)... 52.216.112.195 Connecting to github-production-release-asset-2e65be.s3.amazonaws.com (github-production-release-asset-2e65be.s3.amazonaws.com)|52.216.112.195|:443... connected. HTTP request sent, awaiting response... 200 OK Length: 1486371 (1.4M) [application/octet-stream] Saving to: ‘docker-credential-pass-v0.6.3-amd64.tar.gz’ docker-credential-pass-v0.6 100%[=========================================>] 1.42M --.-KB/s in 0.04s 2020-09-15 22:10:47 (34.4 MB/s) - ‘docker-credential-pass-v0.6.3-amd64.tar.gz’ saved [1486371/1486371] /home/mr-robot/.docker/config.json created with defaults /home/mr-robot/.docker/config.json: { "credsStore": "pass" } Generating GPG key, accept defaults but consider key size to 2048, supply user info gpg: WARNING: unsafe ownership on homedir '/home/mr-robot/.gnupg' gpg (GnuPG) 2.2.4; Copyright (C) 2017 Free Software Foundation, Inc. This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. gpg: keybox '/home/mr-robot/.gnupg/pubring.kbx' created Please select what kind of key you want: (1) RSA and RSA (default) (2) DSA and Elgamal (3) DSA (sign only) (4) RSA (sign only) Your selection? RSA keys may be between 1024 and 4096 bits long. What keysize do you want? (3072) 2048 Requested keysize is 2048 bits Please specify how long the key should be valid. 0 = key does not expire= key expires in n days w = key expires in n weeks m = key expires in n months y = key expires in n years Key is valid for? (0) Key does not expire at all Is this correct? (y/N) y GnuPG needs to construct a user ID to identify your key. Real name: Robot Email address: robot@domain.com Comment: You selected this USER-ID: "Robot " Change (N)ame, (C)omment, (E)mail or (O)kay/(Q)uit? O We need to generate a lot of random bytes. It is a good idea to perform some other action (type on the keyboard, move the mouse, utilize the disks) during the prime generation; this gives the random number generator a better chance to gain enough entropy. We need to generate a lot of random bytes. It is a good idea to perform some other action (type on the keyboard, move the mouse, utilize the disks) during the prime generation; this gives the random number generator a better chance to gain enough entropy. gpg: /home/mr-robot/.gnupg/trustdb.gpg: trustdb created gpg: key AA4FD1A5B14B84B6 marked as ultimately trusted gpg: directory '/home/mr-robot/.gnupg/openpgp-revocs.d' created gpg: revocation certificate stored as '/home/mr-robot/.gnupg/openpgp-revocs.d/0421A82FFC8D8B20A483E807AA4FD1A5B14B84B6.rev' public and secret key created and signed. pub rsa2048 2020-09-15 [SC] 0421A82FFC8D8B20A483E807AA4FD1A5B14B84B6 uid Robot sub rsa2048 2020-09-15 [E] Adjusting permissions gpg: checking the trustdb gpg: marginals needed: 3 completes needed: 1 trust model: pgp gpg: depth: 0 valid: 1 signed: 0 trust: 0-, 0q, 0n, 0m, 0f, 1u /home/mr-robot/.gnupg/pubring.kbx --------------------------------- pub rsa2048 2020-09-15 [SC] 0421A82FFC8D8B20A483E807AA4FD1A5B14B84B6 uid [ultimate] Robot sub rsa2048 2020-09-15 [E] Initializing pass with key 0421A82FFC8D8B20A483E807AA4FD1A5B14B84B6 mkdir: created directory '/home/mr-robot/.password-store/' Password store initialized for 0421A82FFC8D8B20A483E807AA4FD1A5B14B84B6 Do not set a passphrase for this step (*IMPORTANT*) mkdir: created directory '/home/mr-robot/.password-store/docker-credential-helpers' Enter password for docker-credential-helpers/docker-pass-initialized-check: Retype password for docker-credential-helpers/docker-pass-initialized-check: Docker credential password list (empty initially): {} Done. Ready to test. Run: docker login After login run: docker-credential-pass list; cat ~/.docker/config.json; pass show
Next it was time to test the setup with some steps, starting with logging into a private registry requiring authentication.
mr-robot@ubun2:~$ sudo docker login theworldisahoax.azurecr.io
Username: theworldisahoax Password: Login Succeeded
Next up, listing docker credential pass authentications.
mr-robot@ubun2:~$ sudo docker-credential-pass list
{"theworldisahoax.azurecr.io":"theworldisahoax"}
Verifying the Docker daemon config:
mr-robot@ubun2:~$ sudo cat ~/.docker/config.json
{ "auths": { "theworldisahoax.azurecr.io": {} }, "HttpHeaders": { "User-Agent": "Docker-Client/19.03.12 (linux)" }, "credsStore": "pass" }
Listing the names of passwords with tree:
mr-robot@ubun2:~$ sudo pass ls
Password Store └── docker-credential-helpers ├── dGhld29ybGRpc2Fob2F4LmF6dXJlY3IuaW8= │ └── theworldisahoax └── docker-pass-initialized-check
The password can then be retrieved with the following which will first securely prompt for the password used when generating the key during setup.
mr-robot@ubun2:~$ sudo pass show docker-credential-helpers/dGhld29ybGRpc2Fob2F4LmF6dXJlY3IuaW8=/theworldisahoax
9GBTLOiig7lMuzWYwb5mdw/7Ut8PpFvu
I’m not an expert in Linux or security so there’s probably room for improvement with this.