Repository: containers/image
Pull request #1193 (2021-03-30 11:14:57)

registries.conf: configure credential helpers
Allow for configuring credential helpers in `registries.conf` files.

Credential helpers are configured as a top-level field
`credential-helpers`.  This is an array of strings.  Items listed in the
array are consulted in the specified order when looking up or removing
credentials.

Note that there is a built-in credential helpers `containers-auth.json`
for using auth files.  If no global defaults are specified, we will
fallback to using auth files.  This assures backwards compat and a
working default setting.

The traces of the disabled "keyring" functionality have largely been
removed.  If we ever want to re-enable support, we can follow the
example of auth files and specify a new built-in keyring helper.

Using a built-in helper simplifies the code quite a bit since the code
structure boilds down to conditionally dispatching helpers; everything's
a credential helper with some special values for built-in helpers.

Make sure that the execution paths are properly logged (debug level).

Signed-off-by: Qi Wang <qiwan@redhat.com>
Signed-off-by: Valentin Rothberg <rothberg@redhat.com>


vrothberg: Do not merge. We need to iron out the default behaviors and fallbacks etc.
vrothberg: @rhatdan PTAL
rhatdan: LGTM
@QiWang19 @mtrmac PTAL
mtrmac: > Registry-specific credential helpers are configured as part of a
registry object and allow for overriding the global defaults.

BTW, random idle thought, do we need this? IIRC the global configuration in the config file was originally wanted so that we can system-wide default to a credential helper instead of using plaintext files (but users can revert); that doesn’t imply the need to do it per-registry.

(It might be a good idea to have the per-registry (per-reference) _API_, that doesn’t strictly require supporting per-registry _configuration_. Or, uh, in theory, even the sysregistriesv2 API could be private and we could just support accessing credentials via pkg/docker only.)
vrothberg: I am cool to keep it simple and only use "global" cred helpers and ditch the registry-specific ones. @rhatdan WDYT?
rhatdan: I am fine with Global also.  Not sure where the registry specific ones came from (Hopefully there is no message where I requested it.)  Simple is better.
mtrmac: The flip side is that per-registry helpers are already possible via `.docker/config.json`=`auth.json` , and it feels a bit cumbersome to have a `registries conf` helper list point to a config that points to helpers.

But then again, do we care? Is there a _good_ reason to use a helper for one single registry? (Some kind of integration with a specific cloud provider based on other credentials in the environment, maybe?)

I don’t really have an opinion either way, I just want to make sure we know why we are adding a ~permanent feature before we are committed to maintaining it.
rhatdan: I would leave that to the auth.json file.  I don't see people using different mechanisms for this.
vrothberg: Less code to maintain is always good.  I took over the design and assumed there was consensus on it.  I'll clean it up now and repush.

Patch:
diff --git a/docs/containers-registries.conf.5.md b/docs/containers-registries.conf.5.md
index f0a99956..9a884e3d 100644
--- a/docs/containers-registries.conf.5.md
+++ b/docs/containers-registries.conf.5.md
@@ -16,6 +16,9 @@ Container engines will use the `$HOME/.config/containers/registries.conf` if it
 `unqualified-search-registries`
 : An array of _host_[`:`_port_] registries to try when pulling an unqualified image, in order.
 
+`credential-helpers`
+: An array of default credential helpers used as external credential stores.  Note that "containers-auth.json" is a reserved value to use auth files as specified in containers-auth.json(5).  The credential helpers are set to `["containers-auth.json"]` if none are specified.
+
 ### NAMESPACED `[[registry]]` SETTINGS
 
 The bulk of the configuration is represented as an array of `[[registry]]`
@@ -270,7 +273,7 @@ the destination registry is unambiguous. Pulling by digest
 tags.
 
 # SEE ALSO
-  containers-certs.d(5)
+ containers-auth.json(5) containers-certs.d(5)
 
 # HISTORY
 Dec 2019, Warning added for unqualified image names by Tom Sweeney <tsweeney@redhat.com>
diff --git a/go.mod b/go.mod
index 71832687..f43a6432 100644
--- a/go.mod
+++ b/go.mod
@@ -17,6 +17,7 @@ require (
 	github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 // indirect
 	github.com/ghodss/yaml v1.0.0
 	github.com/gorilla/mux v1.7.4 // indirect
+	github.com/hashicorp/go-multierror v1.1.1
 	github.com/imdario/mergo v0.3.11
 	github.com/klauspost/compress v1.11.13
 	github.com/klauspost/pgzip v1.2.5
diff --git a/go.sum b/go.sum
index ca6fcfa9..afd591d7 100644
--- a/go.sum
+++ b/go.sum
@@ -46,6 +46,7 @@ github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tT
 github.com/Microsoft/go-winio v0.4.16-0.20201130162521-d1ffc52c7331 h1:3YnB7Hpmh1lPecPE8doMOtYCrMdrpedZOvxfuNES/Vk=
 github.com/Microsoft/go-winio v0.4.16-0.20201130162521-d1ffc52c7331/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0=
 github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0=
+github.com/Microsoft/go-winio v0.4.17-0.20210211115548-6eac466e5fa3 h1:mw6pDQqv38/WGF1cO/jF5t/jyAJ2yi7CmtFLLO5tGFI=
 github.com/Microsoft/go-winio v0.4.17-0.20210211115548-6eac466e5fa3/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84=
 github.com/Microsoft/hcsshim v0.8.6/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg=
 github.com/Microsoft/hcsshim v0.8.7-0.20190325164909-8abdbb8205e4/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg=
@@ -53,6 +54,7 @@ github.com/Microsoft/hcsshim v0.8.7/go.mod h1:OHd7sQqRFrYd3RmSgbgji+ctCwkbq2wbEY
 github.com/Microsoft/hcsshim v0.8.9/go.mod h1:5692vkUqntj1idxauYlpoINNKeqCiG6Sg38RRsjT5y8=
 github.com/Microsoft/hcsshim v0.8.14 h1:lbPVK25c1cu5xTLITwpUcxoA9vKrKErASPYygvouJns=
 github.com/Microsoft/hcsshim v0.8.14/go.mod h1:NtVKoYxQuTLx6gEq0L96c9Ju4JbRJ4nY2ow3VK6a9Lg=
+github.com/Microsoft/hcsshim v0.8.15 h1:Aof83YILRs2Vx3GhHqlvvfyx1asRJKMFIMeVlHsZKtI=
 github.com/Microsoft/hcsshim v0.8.15/go.mod h1:x38A4YbHbdxJtc0sF6oIz+RG0npwSCAvn69iY6URG00=
 github.com/Microsoft/hcsshim/test v0.0.0-20201218223536-d3e5debf77da/go.mod h1:5hlzMzRKMLyo42nCZ9oml8AdTlq/0cvIaBv6tK1RehU=
 github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
@@ -86,6 +88,7 @@ github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd/go.mod h1:2oa8n
 github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50=
 github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
 github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 github.com/checkpoint-restore/go-criu/v4 v4.0.2/go.mod h1:xUQBLp4RLc5zJtWY++yjOoMoB5lihDt7fai+75m+rGw=
 github.com/checkpoint-restore/go-criu/v4 v4.1.0/go.mod h1:xUQBLp4RLc5zJtWY++yjOoMoB5lihDt7fai+75m+rGw=
@@ -109,6 +112,7 @@ github.com/containerd/cgroups v0.0.0-20190919134610-bf292b21730f/go.mod h1:OApqh
 github.com/containerd/cgroups v0.0.0-20200531161412-0dbf7f05ba59 h1:qWj4qVYZ95vLWwqyNJCQg7rDsG5wPdze0UaPolH7DUk=
 github.com/containerd/cgroups v0.0.0-20200531161412-0dbf7f05ba59/go.mod h1:pA0z1pT8KYB3TCXK/ocprsh7MAkoW8bZVzPdih9snmM=
 github.com/containerd/cgroups v0.0.0-20200710171044-318312a37340/go.mod h1:s5q4SojHctfxANBDvMeIaIovkq29IP48TKAxnhYRxvo=
+github.com/containerd/cgroups v0.0.0-20200824123100-0b889c03f102 h1:Qf4HiqfvmB7zS6scsmNgTLmByHbq8n9RTF39v+TzP7A=
 github.com/containerd/cgroups v0.0.0-20200824123100-0b889c03f102/go.mod h1:s5q4SojHctfxANBDvMeIaIovkq29IP48TKAxnhYRxvo=
 github.com/containerd/console v0.0.0-20180822173158-c12b1e7919c1/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw=
 github.com/containerd/console v0.0.0-20181022165439-0650fd9eeb50/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw=
@@ -122,6 +126,7 @@ github.com/containerd/containerd v1.3.1-0.20191213020239-082f7e3aed57/go.mod h1:
 github.com/containerd/containerd v1.3.2 h1:ForxmXkA6tPIvffbrDAcPUIB32QgXkt2XFj+F0UxetA=
 github.com/containerd/containerd v1.3.2/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
 github.com/containerd/containerd v1.4.0-beta.2.0.20200729163537-40b22ef07410/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
+github.com/containerd/containerd v1.5.0-beta.1 h1:IK6yirB4X7wpKyFSikWiT++nZsyIxGAAgNEv3fEGuls=
 github.com/containerd/containerd v1.5.0-beta.1/go.mod h1:5HfvG1V2FsKesEGQ17k5/T7V960Tmcumvqn8Mc+pCYQ=
 github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc h1:TP+534wVlf61smEIq1nwLLAjQVEK2EADoW3CX9AuT+8=
 github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
@@ -164,7 +169,9 @@ github.com/containers/storage v1.26.1-0.20210219111608-5bd1a7d801b6 h1:q51Y6hvr/
 github.com/containers/storage v1.26.1-0.20210219111608-5bd1a7d801b6/go.mod h1:o7PtlRZpFleYVu0TRAFSb/dPJHZnEk5GMFbVLsf0NOI=
 github.com/containers/storage v1.27.0 h1:3r4yWPNCoqZa8ptvVhljoDJyBYKrJq/tmHyODD7Lv2Y=
 github.com/containers/storage v1.27.0/go.mod h1:o7PtlRZpFleYVu0TRAFSb/dPJHZnEk5GMFbVLsf0NOI=
+github.com/containers/storage v1.28.0 h1:lA/9i9BIjfmIRxCI8GuzasYHmU4IUXVcfZZiDceD0Eg=
 github.com/containers/storage v1.28.0/go.mod h1:ixAwO7Bj31cigqPEG7aCz+PYmxkDxbIFdUFioYdxbzI=
+github.com/containers/storage v1.28.1 h1:axYBD+c0N0YkHelDoqzdLQXfY3fgb8pqIMsRHqUNGts=
 github.com/containers/storage v1.28.1/go.mod h1:5bwiMh2LkrN3AWIfDFMH7A/xbVNLcve+oeXYvHvW8cc=
 github.com/coreos/go-iptables v0.4.5/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU=
 github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
@@ -263,12 +270,14 @@ github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5
 github.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
 github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls=
 github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
+github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
 github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
 github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY=
 github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
 github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
@@ -314,6 +323,7 @@ github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hf
 github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
 github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
 github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
 github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
@@ -336,6 +346,7 @@ github.com/hashicorp/go-multierror v0.0.0-20161216184304-ed905158d874/go.mod h1:
 github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
 github.com/hashicorp/go-multierror v1.1.0 h1:B9UzwGQJehnUY1yNrnwREHc3fGbC2xefo8g4TbElacI=
 github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
+github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
 github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
 github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
 github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU=
@@ -370,7 +381,9 @@ github.com/klauspost/compress v1.11.7 h1:0hzRabrMN4tSTvMfnL3SCv1ZGeAP23ynzodBgaH
 github.com/klauspost/compress v1.11.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
 github.com/klauspost/compress v1.11.9 h1:5OCMOdde1TCT2sookEuVeEZzA8bmRSFV3AwPDZAG8AA=
 github.com/klauspost/compress v1.11.9/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
+github.com/klauspost/compress v1.11.12 h1:famVnQVu7QwryBN4jNseQdUKES71ZAOnB6UQQJPZvqk=
 github.com/klauspost/compress v1.11.12/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
+github.com/klauspost/compress v1.11.13 h1:eSvu8Tmq6j2psUJqJrLcWH6K3w5Dwc+qipbaA6eVEN4=
 github.com/klauspost/compress v1.11.13/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
 github.com/klauspost/pgzip v1.2.5 h1:qnWYvvKqedOF2ulHpMG72XQol4ILEJ8k2wwRl/Km8oE=
 github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
@@ -409,11 +422,13 @@ github.com/mattn/go-shellwords v1.0.11 h1:vCoR9VPpsk/TZFW2JwK5I9S0xdrtUq2bph6/Yj
 github.com/mattn/go-shellwords v1.0.11/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
 github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
 github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
+github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI=
 github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
 github.com/miekg/pkcs11 v1.0.3 h1:iMwmD7I5225wv84WxIG/bmxz9AXjWvTWIbM/TYHvWtw=
 github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
 github.com/mistifyio/go-zfs v2.1.1+incompatible h1:gAMO1HM9xBRONLHHYnu5iFsOJUiJdNZo6oqSENd4eW8=
 github.com/mistifyio/go-zfs v2.1.1+incompatible/go.mod h1:8AuVvqP/mXw1px98n46wfvcGfQ4ci2FwoAjKYxuo3Z4=
+github.com/mistifyio/go-zfs v2.1.2-0.20190413222219-f784269be439+incompatible h1:aKW/4cBs+yK6gpqU3K/oIwk9Q/XICqd3zOX/UFuvqmk=
 github.com/mistifyio/go-zfs v2.1.2-0.20190413222219-f784269be439+incompatible/go.mod h1:8AuVvqP/mXw1px98n46wfvcGfQ4ci2FwoAjKYxuo3Z4=
 github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
 github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f/go.mod h1:OkQIRizQZAeMln+1tSwduZz7+Af5oFlKirV/MSYes2A=
@@ -421,6 +436,7 @@ github.com/moby/sys/mountinfo v0.1.3 h1:KIrhRO14+AkwKvG/g2yIpNMOUVZ02xNhOw8KY1Ws
 github.com/moby/sys/mountinfo v0.1.3/go.mod h1:w2t2Avltqx8vE7gX5l+QiBKxODu2TX0+Syr3h52Tw4o=
 github.com/moby/sys/mountinfo v0.4.0 h1:1KInV3Huv18akCu58V7lzNlt+jFmqlu1EaErnEHE/VM=
 github.com/moby/sys/mountinfo v0.4.0/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A=
+github.com/moby/sys/mountinfo v0.4.1 h1:1O+1cHA1aujwEwwVMa2Xm2l+gIpUHyd3+D+d7LZh1kM=
 github.com/moby/sys/mountinfo v0.4.1/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A=
 github.com/moby/sys/symlink v0.1.0/go.mod h1:GGDODQmbFOjFsXvfLVn3+ZRxkch54RkSiGqsZeMYowQ=
 github.com/moby/term v0.0.0-20200312100748-672ec06f55cd/go.mod h1:DdlQx2hp0Ss5/fLikoLlEeIYiATotOjgB//nb973jeo=
@@ -503,6 +519,7 @@ github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXP
 github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
 github.com/prometheus/client_golang v1.1.0 h1:BQ53HtBmfOitExawJ6LokA4x8ov/z0SYYb0+HxJfRI8=
 github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g=
+github.com/prometheus/client_golang v1.7.1 h1:NTGy1Ja9pByO+xAeH/qiWnLrKtr3hJPNjaVUwnjpdpA=
 github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
 github.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
 github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
@@ -510,11 +527,13 @@ github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx
 github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM=
 github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
 github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 github.com/prometheus/common v0.0.0-20180110214958-89604d197083/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
 github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
 github.com/prometheus/common v0.6.0 h1:kRhiuYSXR3+uv2IbVbZhUxK5zVD/2pp3Gd2PpvPkpEo=
 github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc=
+github.com/prometheus/common v0.10.0 h1:RyRA7RzGXQZiW+tGMr7sxa85G1z0yOpM1qq5c8lNawc=
 github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
 github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
 github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
@@ -525,6 +544,7 @@ github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDa
 github.com/prometheus/procfs v0.0.5/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
 github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
 github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
+github.com/prometheus/procfs v0.2.0 h1:wH4vA7pcjKuZzjF7lM8awk4fnuJO6idemZXoKnULUx4=
 github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
 github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
 github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
@@ -548,6 +568,7 @@ github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM
 github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
 github.com/sirupsen/logrus v1.8.0 h1:nfhvjKcUMhBMVqbKHJlk5RPrrfYr/NMo3692g0dwfWU=
 github.com/sirupsen/logrus v1.8.0/go.mod h1:4GuYW9TZmE769R5STWrRakJc4UqQ3+QQ95fyz7ENv1A=
+github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
 github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
 github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
 github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
@@ -596,6 +617,7 @@ github.com/vbatts/tar-split v0.11.1 h1:0Odu65rhcZ3JZaPHxl7tCI3V/C/Q9Zf82UFravl02
 github.com/vbatts/tar-split v0.11.1/go.mod h1:LEuURwDEiWjRjwu46yU3KVGuUdVv/dcnpcEPSzR8z6g=
 github.com/vbauerster/mpb/v6 v6.0.2 h1:DWFnBOcgHi9GUNduC1MbQ936Z7B77wvOnZexP9Hjzcw=
 github.com/vbauerster/mpb/v6 v6.0.2/go.mod h1:JDNVbdx4oAMMxZNXodDH2DeDY5xBJC8bDGHNFZwRqQM=
+github.com/vbauerster/mpb/v6 v6.0.3 h1:j+twHHhSUe8aXWaT/27E98G5cSBeqEuJSVCMjmLg0PI=
 github.com/vbauerster/mpb/v6 v6.0.3/go.mod h1:5luBx4rDLWxpA4t6I5sdeeQuZhqDxc+wr5Nqf35+tnM=
 github.com/vishvananda/netlink v0.0.0-20181108222139-023a6dafdcdf/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk=
 github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=
@@ -628,6 +650,7 @@ go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
 go.opencensus.io v0.22.0 h1:C9hSCOW830chIVkdja34wa6Ky+IzWllkUinR+BtRZd4=
 go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
 go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+go.opencensus.io v0.22.3 h1:8sGtKOrtQqkN1bp2AtX+misvLIlOmsEsNd+9NIcPEm8=
 go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
 go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
 go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
@@ -645,6 +668,7 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de h1:ikNHVSjEfnvz6sxdSPCaPt572qowuyMDMJLLm3Db3ig=
 golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0 h1:hb9wdF1z5waM+dSIICn1l0DkLVDT3hqhhQsDNUmHPRE=
 golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -725,6 +749,7 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ
 golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a h1:WXEvlFVvvGxCJLG6REjsT03iWnKLEWinaScsxF2Vm2o=
 golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck=
 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -788,6 +813,7 @@ golang.org/x/sys v0.0.0-20201202213521-69691e467435/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20210113181707-4bcb84eeeb78 h1:nVuTkr9L6Bq62qpUqKo/RnZCFfzDBL0bYo6w9OJUqZY=
 golang.org/x/sys v0.0.0-20210113181707-4bcb84eeeb78/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210324051608-47abb6519492 h1:Paq34FxTluEPvVyayQqMPgHm+vTOrIifmcYxFBx9TLg=
 golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -798,6 +824,7 @@ golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc=
 golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -888,6 +915,7 @@ google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfG
 google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
 google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 h1:+kGHl1aib/qcwaRi1CbqBZ1rk19r85MNUf8HaBghugY=
 google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
+google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a h1:pOwg4OoaRYScjmR4LlLgdtnyoHYTSAVhhqe5uPdpII8=
 google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
 google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
diff --git a/pkg/docker/config/config.go b/pkg/docker/config/config.go
index 983df41d..b84aac6e 100644
--- a/pkg/docker/config/config.go
+++ b/pkg/docker/config/config.go
@@ -6,14 +6,17 @@ import (
 	"fmt"
 	"io/ioutil"
 	"os"
+	"os/exec"
 	"path/filepath"
 	"runtime"
 	"strings"
 
+	"github.com/containers/image/v5/pkg/sysregistriesv2"
 	"github.com/containers/image/v5/types"
 	"github.com/containers/storage/pkg/homedir"
 	helperclient "github.com/docker/docker-credential-helpers/client"
 	"github.com/docker/docker-credential-helpers/credentials"
+	"github.com/hashicorp/go-multierror"
 	"github.com/pkg/errors"
 	"github.com/sirupsen/logrus"
 )
@@ -41,12 +44,6 @@ var (
 	dockerLegacyHomePath    = ".dockercfg"
 	nonLinuxAuthFilePath    = filepath.FromSlash(".config/containers/auth.json")
 
-	// Note that the keyring support has been disabled as it was causing
-	// regressions. Before enabling, please revisit TODO(keyring) comments
-	// which need to be addressed if the need remerged to support the
-	// kernel keyring.
-	enableKeyring = false
-
 	// ErrNotLoggedIn is returned for users not logged into a registry
 	// that they are trying to logout of
 	ErrNotLoggedIn = errors.New("not logged in")
@@ -54,72 +51,114 @@ var (
 	ErrNotSupported = errors.New("not supported")
 )
 
-// SetAuthentication stores the username and password in the auth.json file
+// SetAuthentication stores the username and password in the credential helper or file
 func SetAuthentication(sys *types.SystemContext, registry, username, password string) error {
-	return modifyJSON(sys, func(auths *dockerConfigFile) (bool, error) {
-		if ch, exists := auths.CredHelpers[registry]; exists {
-			return false, setAuthToCredHelper(ch, registry, username, password)
-		}
+	helpers, err := sysregistriesv2.CredentialHelpers(sys)
+	if err != nil {
+		return err
+	}
 
-		// Set the credentials to kernel keyring if enableKeyring is true.
-		// The keyring might not work in all environments (e.g., missing capability) and isn't supported on all platforms.
-		// Hence, we want to fall-back to using the authfile in case the keyring failed.
-		// However, if the enableKeyring is false, we want adhere to the user specification and not use the keyring.
-		if enableKeyring {
-			err := setAuthToKernelKeyring(registry, username, password)
-			if err == nil {
-				logrus.Debugf("credentials for (%s, %s) were stored in the kernel keyring\n", registry, username)
-				return false, nil
-			}
-			logrus.Debugf("failed to authenticate with the kernel keyring, falling back to authfiles. %v", err)
+	// Make sure to collect all errors.
+	var multiErr error
+	for _, helper := range helpers {
+		var err error
+		switch helper {
+		// Special-case the built-in helpers for auth files.
+		case sysregistriesv2.AuthenticationFileHelper:
+			err = modifyJSON(sys, func(auths *dockerConfigFile) (bool, error) {
+				if ch, exists := auths.CredHelpers[registry]; exists {
+					return false, setAuthToCredHelper(ch, registry, username, password)
+				}
+				creds := base64.StdEncoding.EncodeToString([]byte(username + ":" + password))
+				newCreds := dockerAuthConfig{Auth: creds}
+				auths.AuthConfigs[registry] = newCreds
+				return true, nil
+			})
+		// External helpers.
+		default:
+			err = setAuthToCredHelper(helper, registry, username, password)
+		}
+		if err != nil {
+			multiErr = multierror.Append(multiErr, err)
+			logrus.Debugf("Error storing credentials for %s in credential helper %s: %v", registry, helper, err)
+			continue
 		}
-		creds := base64.StdEncoding.EncodeToString([]byte(username + ":" + password))
-		newCreds := dockerAuthConfig{Auth: creds}
-		auths.AuthConfigs[registry] = newCreds
-		return true, nil
-	})
+		logrus.Debugf("Stored credentials for %s in credential helper %s", registry, helper)
+		return nil
+	}
+	return multiErr
 }
 
 // GetAllCredentials returns the registry credentials for all registries stored
-// in either the auth.json file or the docker/config.json.
+// in any of the configured credential helpers.
 func GetAllCredentials(sys *types.SystemContext) (map[string]types.DockerAuthConfig, error) {
-	// Note: we need to read the auth files in the inverse order to prevent
-	// a priority inversion when writing to the map.
-	authConfigs := make(map[string]types.DockerAuthConfig)
-	paths := getAuthFilePaths(sys, homedir.Get())
-	for i := len(paths) - 1; i >= 0; i-- {
-		path := paths[i]
-		// readJSONFile returns an empty map in case the path doesn't exist.
-		auths, err := readJSONFile(path.path, path.legacyFormat)
-		if err != nil {
-			return nil, errors.Wrapf(err, "error reading JSON file %q", path.path)
-		}
+	// To keep things simple, let's first extract all registries from all
+	// possible sources, and then call `GetCredentials` on them.  That
+	// prevents us from having to reverse engineer the logic in
+	// `GetCredentials`.
+	allRegistries := make(map[string]bool)
+	addRegistry := func(s string) {
+		allRegistries[s] = true
+	}
 
-		for registry, data := range auths.AuthConfigs {
-			conf, err := decodeDockerAuth(data)
-			if err != nil {
-				return nil, err
+	helpers, err := sysregistriesv2.CredentialHelpers(sys)
+	if err != nil {
+		return nil, err
+	}
+	for _, helper := range helpers {
+		switch helper {
+		// Special-case the built-in helper for auth files.
+		case sysregistriesv2.AuthenticationFileHelper:
+			for _, path := range getAuthFilePaths(sys, homedir.Get()) {
+				// readJSONFile returns an empty map in case the path doesn't exist.
+				auths, err := readJSONFile(path.path, path.legacyFormat)
+				if err != nil {
+					return nil, errors.Wrapf(err, "error reading JSON file %q", path.path)
+				}
+				// Credential helpers in the auth file have a
+				// direct mapping to a registry, so we can just
+				// walk the map.
+				for registry := range auths.CredHelpers {
+					addRegistry(registry)
+				}
+				for registry := range auths.AuthConfigs {
+					addRegistry(registry)
+				}
 			}
-			authConfigs[normalizeRegistry(registry)] = conf
-		}
-
-		// Credential helpers may override credentials from the auth file.
-		for registry, credHelper := range auths.CredHelpers {
-			username, password, err := getAuthFromCredHelper(credHelper, registry)
+		// External helpers.
+		default:
+			creds, err := listAuthsFromCredHelper(helper)
 			if err != nil {
-				if credentials.IsErrCredentialsNotFoundMessage(err.Error()) {
-					continue
+				logrus.Debugf("Error listing credentials stored in credential helper %s: %v", helper, err)
+			}
+			switch errors.Cause(err) {
+			case nil:
+				for registry := range creds {
+					addRegistry(registry)
 				}
+			case exec.ErrNotFound:
+				// It's okay if the helper doesn't exist.
+			default:
 				return nil, err
 			}
-
-			conf := types.DockerAuthConfig{Username: username, Password: password}
-			authConfigs[normalizeRegistry(registry)] = conf
 		}
 	}
 
-	// TODO(keyring): if we ever re-enable the keyring support, we had to
-	// query all credentials from the keyring here.
+	// Now use `GetCredentials` to the specific auth configs for each
+	// previously listed registry.
+	authConfigs := make(map[string]types.DockerAuthConfig)
+	for registry := range allRegistries {
+		authConf, err := GetCredentials(sys, registry)
+		if err != nil {
+			if credentials.IsErrCredentialsNotFoundMessage(err.Error()) {
+				// Ignore if the credentials could not be found (anymore).
+				continue
+			}
+			// Note: we rely on the logging in `GetCredentials`.
+			return nil, err
+		}
+		authConfigs[registry] = authConf
+	}
 
 	return authConfigs, nil
 }
@@ -159,7 +198,9 @@ func getAuthFilePaths(sys *types.SystemContext, homeDir string) []authPath {
 	return paths
 }
 
-// GetCredentials returns the registry credentials stored in either auth.json
+// GetCredentials returns the registry credentials stored in the
+// registry-specific credential helpers or in the default global credentials
+// helpers with falling back to using either auth.json
 // file or .docker/config.json, including support for OAuth2 and IdentityToken.
 // If an entry is not found, an empty struct is returned.
 func GetCredentials(sys *types.SystemContext, registry string) (types.DockerAuthConfig, error) {
@@ -170,41 +211,65 @@ func GetCredentials(sys *types.SystemContext, registry string) (types.DockerAuth
 // it exists only to allow testing it with an artificial home directory.
 func getCredentialsWithHomeDir(sys *types.SystemContext, registry, homeDir string) (types.DockerAuthConfig, error) {
 	if sys != nil && sys.DockerAuthConfig != nil {
-		logrus.Debug("Returning credentials from DockerAuthConfig")
+		logrus.Debugf("Returning credentials for %s from DockerAuthConfig", registry)
 		return *sys.DockerAuthConfig, nil
 	}
 
-	if enableKeyring {
-		username, password, err := getAuthFromKernelKeyring(registry)
-		if err == nil {
-			logrus.Debug("returning credentials from kernel keyring")
-			return types.DockerAuthConfig{
-				Username: username,
-				Password: password,
-			}, nil
+	// Anonymous function to query credentials from auth files.
+	getCredentialsFromAuthFiles := func() (types.DockerAuthConfig, error) {
+		for _, path := range getAuthFilePaths(sys, homeDir) {
+			authConfig, err := findAuthentication(registry, path.path, path.legacyFormat)
+			if err != nil {
+				return types.DockerAuthConfig{}, err
+			}
+
+			if (authConfig.Username != "" && authConfig.Password != "") || authConfig.IdentityToken != "" {
+				return authConfig, nil
+			}
 		}
+		return types.DockerAuthConfig{}, nil
+	}
+
+	helpers, err := sysregistriesv2.CredentialHelpers(sys)
+	if err != nil {
+		return types.DockerAuthConfig{}, err
 	}
 
-	for _, path := range getAuthFilePaths(sys, homeDir) {
-		authConfig, err := findAuthentication(registry, path.path, path.legacyFormat)
+	var multiErr error
+	for _, helper := range helpers {
+		var creds types.DockerAuthConfig
+		var err error
+		switch helper {
+		// Special-case the built-in helper for auth files.
+		case sysregistriesv2.AuthenticationFileHelper:
+			creds, err = getCredentialsFromAuthFiles()
+		// External helpers.
+		default:
+			creds, err = getAuthFromCredHelper(helper, registry)
+		}
 		if err != nil {
-			logrus.Debugf("Credentials not found")
-			return types.DockerAuthConfig{}, err
+			logrus.Debugf("Error looking up credentials for %s in credential helper %s: %v", registry, helper, err)
+			multiErr = multierror.Append(multiErr, err)
+			continue
 		}
-
-		if (authConfig.Username != "" && authConfig.Password != "") || authConfig.IdentityToken != "" {
-			logrus.Debugf("Returning credentials from %s", path.path)
-			return authConfig, nil
+		if len(creds.Username)+len(creds.Password)+len(creds.IdentityToken) == 0 {
+			continue
 		}
+		logrus.Debugf("Found credentials for %s in credential helper %s", registry, helper)
+		return creds, nil
+	}
+	if multiErr != nil {
+		return types.DockerAuthConfig{}, multiErr
 	}
 
-	logrus.Debugf("Credentials not found")
+	logrus.Debugf("No credentials for %s found", registry)
 	return types.DockerAuthConfig{}, nil
 }
 
-// GetAuthentication returns the registry credentials stored in
-// either auth.json file or .docker/config.json
-// If an entry is not found empty strings are returned for the username and password
+// GetAuthentication returns the registry credentials stored in the
+// registry-specific credential helpers or in the default global credentials
+// helpers with falling back to using either auth.json file or
+// .docker/config.json
 //
 // Deprecated: This API only has support for username and password. To get the
 // support for oauth2 in docker registry authentication, we added the new
@@ -227,53 +292,132 @@ func getAuthenticationWithHomeDir(sys *types.SystemContext, registry, homeDir st
 	return auth.Username, auth.Password, nil
 }
 
-// RemoveAuthentication deletes the credentials stored in auth.json
+// RemoveAuthentication removes credentials for `registry` from all possible
+// sources such as credential helpers and auth files.
 func RemoveAuthentication(sys *types.SystemContext, registry string) error {
-	return modifyJSON(sys, func(auths *dockerConfigFile) (bool, error) {
-		// First try cred helpers.
-		if ch, exists := auths.CredHelpers[registry]; exists {
-			return false, deleteAuthFromCredHelper(ch, registry)
+	helpers, err := sysregistriesv2.CredentialHelpers(sys)
+	if err != nil {
+		return err
+	}
+
+	var multiErr error
+	isLoggedIn := false
+
+	removeFromCredHelper := func(helper string) {
+		err := deleteAuthFromCredHelper(helper, registry)
+		if err == nil {
+			logrus.Debugf("Credentials for %q were deleted from credential helper %s", registry, helper)
+			isLoggedIn = true
+			return
+		}
+		if credentials.IsErrCredentialsNotFoundMessage(err.Error()) {
+			logrus.Debugf("Not logged in to %s with credential helper %s", registry, helper)
+			return
 		}
+		multiErr = multierror.Append(multiErr, errors.Wrapf(err, "error removing credentials for %s from credential helper %s", registry, helper))
+	}
 
-		// Next if keyring is enabled try kernel keyring
-		if enableKeyring {
-			err := deleteAuthFromKernelKeyring(registry)
-			if err == nil {
-				logrus.Debugf("credentials for %s were deleted from the kernel keyring", registry)
-				return false, nil
+	for _, helper := range helpers {
+		var err error
+		switch helper {
+		// Special-case the built-in helper for auth files.
+		case sysregistriesv2.AuthenticationFileHelper:
+			err = modifyJSON(sys, func(auths *dockerConfigFile) (bool, error) {
+				if innerHelper, exists := auths.CredHelpers[registry]; exists {
+					removeFromCredHelper(innerHelper)
+				}
+				if _, ok := auths.AuthConfigs[registry]; ok {
+					isLoggedIn = true
+					delete(auths.AuthConfigs, registry)
+				} else if _, ok := auths.AuthConfigs[normalizeRegistry(registry)]; ok {
+					isLoggedIn = true
+					delete(auths.AuthConfigs, normalizeRegistry(registry))
+				}
+				return true, multiErr
+			})
+			if err != nil {
+				multiErr = multierror.Append(multiErr, err)
 			}
-			logrus.Debugf("failed to delete credentials from the kernel keyring, falling back to authfiles")
+		// External helpers.
+		default:
+			removeFromCredHelper(helper)
 		}
+	}
 
-		if _, ok := auths.AuthConfigs[registry]; ok {
-			delete(auths.AuthConfigs, registry)
-		} else if _, ok := auths.AuthConfigs[normalizeRegistry(registry)]; ok {
-			delete(auths.AuthConfigs, normalizeRegistry(registry))
-		} else {
-			return false, ErrNotLoggedIn
-		}
-		return true, nil
-	})
+	if multiErr != nil {
+		return multiErr
+	}
+	if !isLoggedIn {
+		return ErrNotLoggedIn
+	}
+
+	return nil
 }
 
-// RemoveAllAuthentication deletes all the credentials stored in auth.json and kernel keyring
+// RemoveAllAuthentication deletes all the credentials stored in credential
+// helpers and auth files.
 func RemoveAllAuthentication(sys *types.SystemContext) error {
-	return modifyJSON(sys, func(auths *dockerConfigFile) (bool, error) {
-		if enableKeyring {
-			err := removeAllAuthFromKernelKeyring()
-			if err == nil {
-				logrus.Debugf("removing all credentials from kernel keyring")
-				return false, nil
+	helpers, err := sysregistriesv2.CredentialHelpers(sys)
+	if err != nil {
+		return err
+	}
+
+	var multiErr error
+	for _, helper := range helpers {
+		var err error
+		switch helper {
+		// Special-case the built-in helper for auth files.
+		case sysregistriesv2.AuthenticationFileHelper:
+			err = modifyJSON(sys, func(auths *dockerConfigFile) (bool, error) {
+				for registry, helper := range auths.CredHelpers {
+					// Helpers in auth files are expected
+					// to exist, so no special treatment
+					// for them.
+					if err := deleteAuthFromCredHelper(helper, registry); err != nil {
+						return false, err
+					}
+				}
+				auths.CredHelpers = make(map[string]string)
+				auths.AuthConfigs = make(map[string]dockerAuthConfig)
+				return true, nil
+			})
+		// External helpers.
+		default:
+			var creds map[string]string
+			creds, err = listAuthsFromCredHelper(helper)
+			switch errors.Cause(err) {
+			case nil:
+				for registry := range creds {
+					err = deleteAuthFromCredHelper(helper, registry)
+					if err != nil {
+						break
+					}
+				}
+			case exec.ErrNotFound:
+				// It's okay if the helper doesn't exist.
+				continue
+			default:
+				// fall through
 			}
-			logrus.Debugf("error removing credentials from kernel keyring")
 		}
-		auths.CredHelpers = make(map[string]string)
-		auths.AuthConfigs = make(map[string]dockerAuthConfig)
-		return true, nil
-	})
+		if err != nil {
+			logrus.Debugf("Error removing credentials from credential helper %s: %v", helper, err)
+			multiErr = multierror.Append(multiErr, err)
+			continue
+		}
+		logrus.Debugf("All credentials removed from credential helper %s", helper)
+	}
+
+	return multiErr
+}
+
+func listAuthsFromCredHelper(credHelper string) (map[string]string, error) {
+	helperName := fmt.Sprintf("docker-credential-%s", credHelper)
+	p := helperclient.NewShellProgramFunc(helperName)
+	return helperclient.List(p)
 }
 
-// getPathToAuth gets the path of the auth.json file used for reading and writing credentials
+// getPathToAuth gets the path of the auth.json file used for reading and writting credentials
 // returns the path, and a bool specifies whether the file is in legacy format
 func getPathToAuth(sys *types.SystemContext) (string, bool, error) {
 	return getPathToAuthWithOS(sys, runtime.GOOS)
@@ -387,14 +531,17 @@ func modifyJSON(sys *types.SystemContext, editor func(auths *dockerConfigFile) (
 	return nil
 }
 
-func getAuthFromCredHelper(credHelper, registry string) (string, string, error) {
+func getAuthFromCredHelper(credHelper, registry string) (types.DockerAuthConfig, error) {
 	helperName := fmt.Sprintf("docker-credential-%s", credHelper)
 	p := helperclient.NewShellProgramFunc(helperName)
 	creds, err := helperclient.Get(p, registry)
 	if err != nil {
-		return "", "", err
+		return types.DockerAuthConfig{}, err
 	}
-	return creds.Username, creds.Secret, nil
+	return types.DockerAuthConfig{
+		Username: creds.Username,
+		Password: creds.Secret,
+	}, nil
 }
 
 func setAuthToCredHelper(credHelper, registry, username, password string) error {
@@ -423,15 +570,7 @@ func findAuthentication(registry, path string, legacyFormat bool) (types.DockerA
 
 	// First try cred helpers. They should always be normalized.
 	if ch, exists := auths.CredHelpers[registry]; exists {
-		username, password, err := getAuthFromCredHelper(ch, registry)
-		if err != nil {
-			return types.DockerAuthConfig{}, err
-		}
-
-		return types.DockerAuthConfig{
-			Username: username,
-			Password: password,
-		}, nil
+		return getAuthFromCredHelper(ch, registry)
 	}
 
 	// I'm feeling lucky
diff --git a/pkg/docker/config/config_linux.go b/pkg/docker/config/config_linux.go
index 1531d694..5bbfb450 100644
--- a/pkg/docker/config/config_linux.go
+++ b/pkg/docker/config/config_linux.go
@@ -9,9 +9,13 @@ import (
 	"github.com/sirupsen/logrus"
 )
 
-const keyDescribePrefix = "container-registry-login:"
+// NOTE: none of the functions here are currently used.  If we ever want to
+// reenable keyring support, we should introduce a similar built-in credential
+// helpers as for `sysregistriesv2.AuthenticationFileHelper`.
 
-func getAuthFromKernelKeyring(registry string) (string, string, error) {
+const keyDescribePrefix = "container-registry-login:" // nolint
+
+func getAuthFromKernelKeyring(registry string) (string, string, error) { // nolint
 	userkeyring, err := keyctl.UserKeyring()
 	if err != nil {
 		return "", "", err
@@ -31,7 +35,7 @@ func getAuthFromKernelKeyring(registry string) (string, string, error) {
 	return parts[0], parts[1], nil
 }
 
-func deleteAuthFromKernelKeyring(registry string) error {
+func deleteAuthFromKernelKeyring(registry string) error { // nolint
 	userkeyring, err := keyctl.UserKeyring()
 
 	if err != nil {
@@ -44,7 +48,7 @@ func deleteAuthFromKernelKeyring(registry string) error {
 	return key.Unlink()
 }
 
-func removeAllAuthFromKernelKeyring() error {
+func removeAllAuthFromKernelKeyring() error { // nolint
 	keys, err := keyctl.ReadUserKeyring()
 	if err != nil {
 		return err
@@ -77,7 +81,7 @@ func removeAllAuthFromKernelKeyring() error {
 	return nil
 }
 
-func setAuthToKernelKeyring(registry, username, password string) error {
+func setAuthToKernelKeyring(registry, username, password string) error { // nolint
 	keyring, err := keyctl.SessionKeyring()
 	if err != nil {
 		return err
@@ -110,6 +114,6 @@ func setAuthToKernelKeyring(registry, username, password string) error {
 	return nil
 }
 
-func genDescription(registry string) string {
+func genDescription(registry string) string { // nolint
 	return fmt.Sprintf("%s%s", keyDescribePrefix, registry)
 }
diff --git a/pkg/sysregistriesv2/system_registries_v2.go b/pkg/sysregistriesv2/system_registries_v2.go
index 41f7c968..b5fd3e8c 100644
--- a/pkg/sysregistriesv2/system_registries_v2.go
+++ b/pkg/sysregistriesv2/system_registries_v2.go
@@ -37,6 +37,11 @@ var systemRegistriesConfDirPath = builtinRegistriesConfDirPath
 // DO NOT change this, instead see systemRegistriesConfDirectoryPath above.
 const builtinRegistriesConfDirPath = "/etc/containers/registries.conf.d"
 
+// AuthenticationFileHelper is a special key for credential helpers indicating
+// the usage of consulting containers-auth.json files instead of a credential
+// helper.
+const AuthenticationFileHelper = "containers-auth.json"
+
 // Endpoint describes a remote location of a registry.
 type Endpoint struct {
 	// The endpoint's remote location.
@@ -154,6 +159,14 @@ type V2RegistriesConf struct {
 	Registries []Registry `toml:"registry"`
 	// An array of host[:port] (not prefix!) entries to use for resolving unqualified image references
 	UnqualifiedSearchRegistries []string `toml:"unqualified-search-registries"`
+	// An array of global credential helpers to use for authentication
+	// (e.g., ["pass", "secretservice"]).  The helpers are consulted in the
+	// specified order.  Note that "containers-auth.json" is a reserved
+	// value for consulting auth files as specified in
+	// containers-auth.json(5).
+	//
+	// If empty, CredentialHelpers defaults to  ["containers-auth.json"].
+	CredentialHelpers []string `toml:"credential-helpers"`
 
 	// ShortNameMode defines how short-name resolution should be handled by
 	// _consumers_ of this package.  Depending on the mode, the user should
@@ -601,6 +614,10 @@ func tryUpdatingCache(ctx *types.SystemContext, wrapper configWrapper) (*parsedC
 		config.shortNameMode = defaultShortNameMode
 	}
 
+	if len(config.partialV2.CredentialHelpers) == 0 {
+		config.partialV2.CredentialHelpers = []string{AuthenticationFileHelper}
+	}
+
 	// populate the cache
 	configCache[wrapper] = config
 	return config, nil
@@ -663,6 +680,15 @@ func GetShortNameMode(ctx *types.SystemContext) (types.ShortNameMode, error) {
 	return config.shortNameMode, err
 }
 
+// CredentialHelpers returns the global top-level credential helpers.
+func CredentialHelpers(sys *types.SystemContext) ([]string, error) {
+	config, err := getConfig(sys)
+	if err != nil {
+		return nil, err
+	}
+	return config.partialV2.CredentialHelpers, nil
+}
+
 // refMatchesPrefix returns true iff ref,
 // which is a registry, repository namespace, repository or image reference (as formatted by
 // reference.Domain(), reference.Named.Name() or reference.Reference.String()
@@ -700,6 +726,12 @@ func FindRegistry(ctx *types.SystemContext, ref string) (*Registry, error) {
 		return nil, err
 	}
 
+	return findRegistryWithParsedConfig(config, ref)
+}
+
+// findRegistryWithParsedConfig implements `FindRegistry` with a pre-loaded
+// parseConfig.
+func findRegistryWithParsedConfig(config *parsedConfig, ref string) (*Registry, error) {
 	reg := Registry{}
 	prefixLen := 0
 	for _, r := range config.partialV2.Registries {
@@ -825,6 +857,11 @@ func (c *parsedConfig) updateWithConfigurationFrom(updates *parsedConfig) {
 		c.unqualifiedSearchRegistriesOrigin = updates.unqualifiedSearchRegistriesOrigin
 	}
 
+	// == Merge credential helpers:
+	if updates.partialV2.CredentialHelpers != nil {
+		c.partialV2.CredentialHelpers = updates.partialV2.CredentialHelpers
+	}
+
 	// == Merge shortNameMode:
 	// We don’t maintain c.partialV2.ShortNameMode.
 	if updates.shortNameMode != types.ShortNameModeInvalid {


Test patch:
diff --git a/pkg/docker/config/config_test.go b/pkg/docker/config/config_test.go
index 13ffeec0..5f7a8e0e 100644
--- a/pkg/docker/config/config_test.go
+++ b/pkg/docker/config/config_test.go
@@ -1,16 +1,15 @@
 package config
 
 import (
-	"encoding/json"
 	"fmt"
 	"io/ioutil"
 	"os"
 	"path/filepath"
 	"runtime"
+	"strings"
 	"testing"
 
 	"github.com/containers/image/v5/types"
-	"github.com/pkg/errors"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )
@@ -97,6 +96,17 @@ func TestGetAuth(t *testing.T) {
 		os.Setenv("XDG_RUNTIME_DIR", origXDG)
 	}()
 
+	// override PATH for executing credHelper
+	curtDir, err := os.Getwd()
+	require.NoError(t, err)
+	origPath := os.Getenv("PATH")
+	newPath := fmt.Sprintf("%s:%s", filepath.Join(curtDir, "testdata"), origPath)
+	os.Setenv("PATH", newPath)
+	t.Logf("using PATH: %q", newPath)
+	defer func() {
+		os.Setenv("PATH", origPath)
+	}()
+
 	tmpHomeDir, err := ioutil.TempDir("", "test_docker_client_get_auth")
 	if err != nil {
 		t.Fatal(err)
@@ -237,6 +247,18 @@ func TestGetAuth(t *testing.T) {
 				hostname: "https://localhost:5000",
 				path:     filepath.Join("testdata", "empty.json"),
 			},
+			{
+				name:     "credhelper from registries.conf",
+				hostname: "registry-a.com",
+				sys: &types.SystemContext{
+					SystemRegistriesConfPath:    filepath.Join("testdata", "cred-helper.conf"),
+					SystemRegistriesConfDirPath: filepath.Join("testdata", "IdoNotExist"),
+				},
+				expected: types.DockerAuthConfig{
+					Username: "foo",
+					Password: "bar",
+				},
+			},
 		} {
 			t.Run(tc.name, func(t *testing.T) {
 				if err := os.RemoveAll(configPath); err != nil {
@@ -439,7 +461,7 @@ func TestGetAuthFailsOnBadInput(t *testing.T) {
 	if err == nil {
 		t.Fatalf("got unexpected non-error: username=%q, password=%q", auth.Username, auth.Password)
 	}
-	if _, ok := errors.Cause(err).(*json.SyntaxError); !ok {
+	if !strings.Contains(err.Error(), "error unmarshaling JSON") {
 		t.Fatalf("expected JSON syntax error, not: %#+v", err)
 	}
 
@@ -460,7 +482,7 @@ func TestGetAuthFailsOnBadInput(t *testing.T) {
 	if err == nil {
 		t.Fatalf("got unexpected non-error: username=%q, password=%q", auth.Username, auth.Password)
 	}
-	if _, ok := errors.Cause(err).(*json.SyntaxError); !ok {
+	if !strings.Contains(err.Error(), "error unmarshaling JSON") {
 		t.Fatalf("expected JSON syntax error, not: %#+v", err)
 	}
 }
@@ -474,12 +496,29 @@ func TestGetAllCredentials(t *testing.T) {
 	err = tmpFile.Close()
 	require.NoError(t, err)
 	authFilePath := tmpFile.Name()
-	sys := types.SystemContext{AuthFilePath: authFilePath}
+	// override PATH for executing credHelper
+	path, err := os.Getwd()
+	require.NoError(t, err)
+	origPath := os.Getenv("PATH")
+	newPath := fmt.Sprintf("%s:%s", filepath.Join(path, "testdata"), origPath)
+	os.Setenv("PATH", newPath)
+	t.Logf("using PATH: %q", newPath)
+	defer func() {
+		os.Setenv("PATH", origPath)
+	}()
+	err = os.Chmod(filepath.Join(path, "testdata", "docker-credential-helper-registry"), os.ModePerm)
+	require.NoError(t, err)
+	sys := types.SystemContext{
+		AuthFilePath:                authFilePath,
+		SystemRegistriesConfPath:    filepath.Join("testdata", "cred-helper-with-auth-files.conf"),
+		SystemRegistriesConfDirPath: filepath.Join("testdata", "IdoNotExist"),
+	}
 
 	data := []struct {
 		server   string
 		username string
 		password string
+		noStore  bool
 	}{
 		{
 			server:   "example.org",
@@ -496,10 +535,19 @@ func TestGetAllCredentials(t *testing.T) {
 			username: "local-user",
 			password: "local-password",
 		},
+		{
+			server:   "registry-a.com",
+			username: "foo",
+			password: "bar",
+			noStore:  true,
+		},
 	}
 
 	// Write the credentials to the authfile.
 	for _, d := range data {
+		if d.noStore {
+			continue
+		}
 		err := SetAuthentication(&sys, d.server, d.username, d.password)
 		require.NoError(t, err)
 	}
@@ -508,12 +556,13 @@ func TestGetAllCredentials(t *testing.T) {
 	// servers and the correct credentials.
 	authConfigs, err := GetAllCredentials(&sys)
 	require.NoError(t, err)
-	assert.Equal(t, len(data), len(authConfigs))
+	require.Equal(t, len(data), len(authConfigs))
+
 	for _, d := range data {
 		conf, exists := authConfigs[d.server]
-		assert.True(t, exists)
-		assert.Equal(t, d.username, conf.Username)
-		assert.Equal(t, d.password, conf.Password)
+		require.True(t, exists, "%v", d)
+		require.Equal(t, d.username, conf.Username, "%v", d)
+		require.Equal(t, d.password, conf.Password, "%v", d)
 	}
 
 }
diff --git a/pkg/docker/config/testdata/cred-helper-with-auth-files.conf b/pkg/docker/config/testdata/cred-helper-with-auth-files.conf
new file mode 100644
index 00000000..51da42d1
--- /dev/null
+++ b/pkg/docker/config/testdata/cred-helper-with-auth-files.conf
@@ -0,0 +1,1 @@
+credential-helpers = [ "containers-auth.json", "helper-registry" ]
diff --git a/pkg/docker/config/testdata/cred-helper.conf b/pkg/docker/config/testdata/cred-helper.conf
new file mode 100644
index 00000000..680faabb
--- /dev/null
+++ b/pkg/docker/config/testdata/cred-helper.conf
@@ -0,0 +1,1 @@
+credential-helpers = [ "helper-registry" ]
diff --git a/pkg/docker/config/testdata/docker-credential-helper-registry b/pkg/docker/config/testdata/docker-credential-helper-registry
new file mode 100755
index 00000000..7a97c2f2
--- /dev/null
+++ b/pkg/docker/config/testdata/docker-credential-helper-registry
@@ -0,0 +1,26 @@
+#!/bin/bash
+
+case "${1}" in
+    get)
+        read REGISTRY
+        if [[ "${REGISTRY}" = "registry-a.com" ]]; then
+	        echo "{\"ServerURL\":\"${REGISTRY}\",\"Username\":\"foo\",\"Secret\":\"bar\"}"
+	else
+		echo "{}"
+	fi
+        exit 0
+    ;;
+    store)
+        read UNUSED
+        exit 0
+    ;;
+    list)
+        read UNUSED
+        echo "{\"registry-a.com\":\"foo\"}"
+        exit 0
+    ;;
+    *)
+        echo "not implemented"
+        exit 1
+    ;;
+esac
diff --git a/pkg/sysregistriesv2/system_registries_v2_test.go b/pkg/sysregistriesv2/system_registries_v2_test.go
index 347acebc..f300ee55 100644
--- a/pkg/sysregistriesv2/system_registries_v2_test.go
+++ b/pkg/sysregistriesv2/system_registries_v2_test.go
@@ -636,3 +636,43 @@ func TestGetShortNameMode(t *testing.T) {
 		assert.Equal(t, test.mode, mode, "%s", test.path)
 	}
 }
+
+func TestCredentialHelpers(t *testing.T) {
+	tests := []struct {
+		confPath    string
+		confDirPath string
+		helpers     []string
+	}{
+		{
+			confPath:    "testdata/cred-helper.conf",
+			confDirPath: "testdata/this-does-not-exist",
+			helpers:     []string{"helper-1", "helper-2"},
+		},
+		{
+			confPath:    "testdata/empty.conf",
+			confDirPath: "testdata/this-does-not-exist",
+			helpers:     []string{"containers-auth.json"},
+		},
+		{
+			confPath:    "testdata/cred-helper.conf",
+			confDirPath: "testdata/registries.conf.d-empty-helpers",
+			helpers:     []string{"containers-auth.json"},
+		},
+		{
+			confPath:    "testdata/cred-helper.conf",
+			confDirPath: "testdata/registries.conf.d",
+			helpers:     []string{"dropin-1", "dropin-2"},
+		},
+	}
+
+	for _, test := range tests {
+		ctx := &types.SystemContext{
+			SystemRegistriesConfPath:    test.confPath,
+			SystemRegistriesConfDirPath: test.confDirPath,
+		}
+
+		helpers, err := CredentialHelpers(ctx)
+		require.NoError(t, err)
+		require.Equal(t, test.helpers, helpers, "%v", test)
+	}
+}
diff --git a/pkg/sysregistriesv2/testdata/cred-helper.conf b/pkg/sysregistriesv2/testdata/cred-helper.conf
new file mode 100644
index 00000000..fbc5e97b
--- /dev/null
+++ b/pkg/sysregistriesv2/testdata/cred-helper.conf
@@ -0,0 +1,10 @@
+credential-helpers = ["helper-1", "helper-2"]
+[[registry]]
+location = "registry-a.com"
+
+[[registry]]
+location = "registry-b.com"
+
+[[registry]]
+location = "registry-c.com/foo"
+prefix = "registry-c.com"
diff --git a/pkg/sysregistriesv2/testdata/registries.conf.d-empty-helpers/empty.conf b/pkg/sysregistriesv2/testdata/registries.conf.d-empty-helpers/empty.conf
new file mode 100644
index 00000000..3be4372d
--- /dev/null
+++ b/pkg/sysregistriesv2/testdata/registries.conf.d-empty-helpers/empty.conf
@@ -0,0 +1,1 @@
+credential-helpers=[]
diff --git a/pkg/sysregistriesv2/testdata/registries.conf.d/config-1.conf b/pkg/sysregistriesv2/testdata/registries.conf.d/config-1.conf
index f02e618a..b52add55 100644
--- a/pkg/sysregistriesv2/testdata/registries.conf.d/config-1.conf
+++ b/pkg/sysregistriesv2/testdata/registries.conf.d/config-1.conf
@@ -1,4 +1,5 @@
 unqualified-search-registries = ["example-overwrite.com"]
+credential-helpers = ["we", "will", "be", "overridden", "later"]
 
 [[registry]]
 location = "1.com"
diff --git a/pkg/sysregistriesv2/testdata/registries.conf.d/config-2.conf b/pkg/sysregistriesv2/testdata/registries.conf.d/config-2.conf
index 7ec82c75..695b80b7 100644
--- a/pkg/sysregistriesv2/testdata/registries.conf.d/config-2.conf
+++ b/pkg/sysregistriesv2/testdata/registries.conf.d/config-2.conf
@@ -1,5 +1,10 @@
 short-name-mode="permissive"
 
+credential-helpers=["dropin-1", "dropin-2"]
+
+[[registry]]
+location = "1.com"
+
 [[registry]]
 location = "2.com"