Skip to content

Commit dc131aa

Browse files
AkihiroSudadmcgowan
authored andcommitted
support loading certs from a directory
Add `remotes/certutil` functions for loading `ca.crt`, `client.cert`, and `client.key` into `tls.Config` from a directory like `/etc/docker/certs.d/<hostname>. See https://docs.docker.com/engine/security/certificates/ . Client applications including CRI plugin are expected to configure the resolver using these functions. As an example, the `ctr` tool is extended to support `ctr images pull --certs-dir=/etc/docker/certs.d example.com/foo/bar:baz`. Tested with Harbor 1.8. Signed-off-by: Akihiro Suda <akihiro.suda.cz@hco.ntt.co.jp>
1 parent e852da5 commit dc131aa

File tree

3 files changed

+164
-0
lines changed

3 files changed

+164
-0
lines changed

cmd/ctr/commands/commands.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,11 @@ var (
6161
Name: "refresh",
6262
Usage: "refresh token for authorization server",
6363
},
64+
cli.StringFlag{
65+
Name: "certs-dir",
66+
// compatible with "/etc/docker/certs.d"
67+
Usage: "custom certificates directory that contains \"<hostname>/{ca.crt, client.cert, client.key}\"",
68+
},
6469
}
6570

6671
// ContainerFlags are cli flags specifying container options

cmd/ctr/commands/resolver.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,20 @@ import (
2121
gocontext "context"
2222
"crypto/tls"
2323
"fmt"
24+
"io/ioutil"
2425
"net"
2526
"net/http"
27+
"os"
28+
"path/filepath"
2629
"strings"
2730
"time"
2831

2932
"github.com/containerd/console"
3033
"github.com/containerd/containerd/remotes"
34+
"github.com/containerd/containerd/remotes/certutil"
3135
"github.com/containerd/containerd/remotes/docker"
3236
"github.com/pkg/errors"
37+
"github.com/sirupsen/logrus"
3338
"github.com/urfave/cli"
3439
)
3540

@@ -94,6 +99,7 @@ func GetResolver(ctx gocontext.Context, clicontext *cli.Context) (remotes.Resolv
9499
},
95100
ExpectContinueTimeout: 5 * time.Second,
96101
}
102+
loadCertsDir(tr.TLSClientConfig, clicontext.String("certs-dir"))
97103

98104
options.Client = &http.Client{
99105
Transport: tr,
@@ -108,3 +114,30 @@ func GetResolver(ctx gocontext.Context, clicontext *cli.Context) (remotes.Resolv
108114

109115
return docker.NewResolver(options), nil
110116
}
117+
118+
// loadCertsDir loads certs from certsDir like "/etc/docker/certs.d" .
119+
func loadCertsDir(config *tls.Config, certsDir string) {
120+
if certsDir == "" {
121+
return
122+
}
123+
fs, err := ioutil.ReadDir(certsDir)
124+
if err != nil && !os.IsNotExist(err) {
125+
logrus.WithError(err).Errorf("cannot read certs directory %q", certsDir)
126+
return
127+
}
128+
for _, f := range fs {
129+
if !f.IsDir() {
130+
continue
131+
}
132+
// TODO: skip loading if f.Name() is not valid FQDN/IP
133+
hostDir := filepath.Join(certsDir, f.Name())
134+
caCertGlob := filepath.Join(hostDir, "*.crt")
135+
if _, err = certutil.LoadCACerts(config, caCertGlob); err != nil {
136+
logrus.WithError(err).Errorf("cannot load certs from %q", caCertGlob)
137+
}
138+
keyGlob := filepath.Join(hostDir, "*.key")
139+
if _, _, err = certutil.LoadKeyPairs(config, keyGlob, certutil.DockerKeyPairCertLocator); err != nil {
140+
logrus.WithError(err).Errorf("cannot load key pairs from %q", keyGlob)
141+
}
142+
}
143+
}

remotes/certutil/certutil.go

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
/*
2+
Copyright The containerd Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package certutil
18+
19+
import (
20+
"crypto/tls"
21+
"crypto/x509"
22+
"io/ioutil"
23+
"path/filepath"
24+
"runtime"
25+
"sort"
26+
"strings"
27+
28+
"github.com/containerd/containerd/errdefs"
29+
"github.com/pkg/errors"
30+
)
31+
32+
// SystemCertPool returns a copy of the system cert pool,
33+
// returns an error if failed to load or empty pool on windows.
34+
//
35+
// SystemCertPool was ported from Docker 19.03
36+
// https://github.com/docker/engine/blob/v19.03.1/vendor/github.com/docker/go-connections/tlsconfig/certpool_go17.go#L12
37+
func SystemCertPool() (*x509.CertPool, error) {
38+
certpool, err := x509.SystemCertPool()
39+
if err != nil && runtime.GOOS == "windows" {
40+
return x509.NewCertPool(), nil
41+
}
42+
return certpool, err
43+
}
44+
45+
// LoadCACerts loads CA certificates into tlsConfig from glob.
46+
// glob should be like "/etc/docker/certs.d/example.com/*.crt" .
47+
// LoadCACerts returns the paths of the loaded certs.
48+
func LoadCACerts(tlsConfig *tls.Config, glob string) ([]string, error) {
49+
if tlsConfig == nil {
50+
tlsConfig = &tls.Config{}
51+
}
52+
files, err := filepath.Glob(glob)
53+
if err != nil {
54+
return nil, err
55+
}
56+
sort.Strings(files)
57+
if tlsConfig.RootCAs == nil {
58+
systemPool, err := SystemCertPool()
59+
if err != nil {
60+
return nil, errors.Wrap(err, "unable to get system cert poolv")
61+
}
62+
tlsConfig.RootCAs = systemPool
63+
}
64+
var loaded []string
65+
for _, f := range files {
66+
data, err := ioutil.ReadFile(f)
67+
if err != nil {
68+
return loaded, errors.Wrapf(err, "unable to read CA cert %q", f)
69+
}
70+
if !tlsConfig.RootCAs.AppendCertsFromPEM(data) {
71+
return loaded, errors.Errorf("unable to load CA cert %q", f)
72+
}
73+
loaded = append(loaded, f)
74+
}
75+
return loaded, nil
76+
}
77+
78+
// KeyPairCertLocator is used to resolve the cert path from the key path.
79+
type KeyPairCertLocator func(keyPath string) (certPath string, err error)
80+
81+
// DockerKeyPairCertLocator implements the Docker-style convention. ("*.key" -> "*.cert")
82+
var DockerKeyPairCertLocator KeyPairCertLocator = func(keyPath string) (string, error) {
83+
if !strings.HasSuffix(keyPath, ".key") {
84+
return "", errors.Errorf("expected key path with \".key\" suffix, got %q", keyPath)
85+
}
86+
// Docker convention uses *.crt for CA certs, *.cert for keypair certs.
87+
certPath := keyPath[:len(keyPath)-4] + ".cert"
88+
return certPath, nil
89+
}
90+
91+
// LoadKeyPairs loads key pairs into tlsConfig from keyGlob.
92+
// keyGlob should be like "/etc/docker/certs.d/example.com/*.key" .
93+
// certLocator is used to resolve the cert path from the key path.
94+
// Use DockerKeyPairCertLocator for the Docker-style convention. ("*.key" -> "*.cert")
95+
// LoadKeyParis returns the paths of the loaded certs and the loaded keys.
96+
func LoadKeyPairs(tlsConfig *tls.Config, keyGlob string, certLocator KeyPairCertLocator) ([]string, []string, error) {
97+
if tlsConfig == nil {
98+
tlsConfig = &tls.Config{}
99+
}
100+
if certLocator == nil {
101+
return nil, nil, errors.Wrap(errdefs.ErrInvalidArgument, "missing cert locator")
102+
}
103+
keyPaths, err := filepath.Glob(keyGlob)
104+
if err != nil {
105+
return nil, nil, err
106+
}
107+
sort.Strings(keyPaths)
108+
var (
109+
loadedCerts []string
110+
loadedKeys []string
111+
)
112+
for _, keyPath := range keyPaths {
113+
certPath, err := certLocator(keyPath)
114+
if err != nil {
115+
return loadedCerts, loadedKeys, err
116+
}
117+
keyPair, err := tls.LoadX509KeyPair(certPath, keyPath)
118+
if err != nil {
119+
return loadedCerts, loadedKeys, err
120+
}
121+
tlsConfig.Certificates = append(tlsConfig.Certificates, keyPair)
122+
loadedCerts = append(loadedCerts, certPath)
123+
loadedKeys = append(loadedKeys, keyPath)
124+
}
125+
return loadedCerts, loadedKeys, nil
126+
}

0 commit comments

Comments
 (0)