Skip to content

Commit

Permalink
buildctl: Add configured TLS certificate to trust store when making c…
Browse files Browse the repository at this point in the history
…alls to registry auth

Signed-off-by: njucjc <njucjc@gmail.com>
  • Loading branch information
njucjc committed Sep 12, 2023
1 parent 3d44ec2 commit d2b6df8
Show file tree
Hide file tree
Showing 6 changed files with 262 additions and 14 deletions.
19 changes: 14 additions & 5 deletions cmd/buildctl/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ import (

"github.com/containerd/continuity"
"github.com/docker/cli/cli/config"
digest "github.com/opencontainers/go-digest"
"github.com/pkg/errors"
"github.com/urfave/cli"
"golang.org/x/sync/errgroup"

"github.com/moby/buildkit/client"
"github.com/moby/buildkit/client/llb"
"github.com/moby/buildkit/cmd/buildctl/build"
Expand All @@ -24,10 +29,6 @@ import (
spb "github.com/moby/buildkit/sourcepolicy/pb"
"github.com/moby/buildkit/util/bklog"
"github.com/moby/buildkit/util/progress/progresswriter"
digest "github.com/opencontainers/go-digest"
"github.com/pkg/errors"
"github.com/urfave/cli"
"golang.org/x/sync/errgroup"
)

var buildCommand = cli.Command{
Expand Down Expand Up @@ -105,6 +106,10 @@ var buildCommand = cli.Command{
Name: "ref-file",
Usage: "Write build ref to a file",
},
cli.StringSliceFlag{
Name: "registry-auth-tlscontext",
Usage: "Certificate to validate TLS when authenticating with registries, e.g. --registry-auth-tlscontext host=https://myserver:2376,ca=/path/to/my/ca.crt,cert=/path/to/my/cert.crt,key=/path/to/my/key.crt",
},
},
}

Expand Down Expand Up @@ -158,7 +163,11 @@ func buildAction(clicontext *cli.Context) error {
}

dockerConfig := config.LoadDefaultConfigFile(os.Stderr)
attachable := []session.Attachable{authprovider.NewDockerAuthProvider(dockerConfig)}
tlsContexts, err := build.ParseRegistryAuthTLSContext(clicontext.StringSlice("registry-auth-tlscontext"))
if err != nil {
return err
}
attachable := []session.Attachable{authprovider.NewDockerAuthProvider(dockerConfig, tlsContexts)}

if ssh := clicontext.StringSlice("ssh"); len(ssh) > 0 {
configs, err := build.ParseSSH(ssh)
Expand Down
61 changes: 61 additions & 0 deletions cmd/buildctl/build/registryauthtlscontext.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package build

import (
"encoding/csv"
"strings"

"github.com/pkg/errors"

"github.com/moby/buildkit/session/auth/authprovider"
)

func parseRegistryAuthTLSContextCSV(s string) (authprovider.AuthTLSContextEntry, error) {
authTLSContext := authprovider.AuthTLSContextEntry{}
csvReader := csv.NewReader(strings.NewReader(s))
fields, err := csvReader.Read()
if err != nil {
return authTLSContext, err
}
for _, field := range fields {
key, value, ok := strings.Cut(field, "=")
if !ok {
return authTLSContext, errors.Errorf("invalid value %s", field)
}
key = strings.ToLower(key)
switch key {
case "host":
authTLSContext.Host = value
case "ca":
authTLSContext.CA = value
case "cert":
authTLSContext.Cert = value
case "key":
authTLSContext.Key = value
}
}
if authTLSContext.Host == "" {
return authTLSContext, errors.New("--registry-auth-tlscontext requires host=<host>")
}
if authTLSContext.CA == "" {
if authTLSContext.Cert == "" || authTLSContext.Key == "" {
return authTLSContext, errors.New("--registry-auth-tlscontext requires ca=<ca> or cert=<cert>,key=<key>")
}
} else {
if (authTLSContext.Cert != "" && authTLSContext.Key == "") || (authTLSContext.Cert == "" && authTLSContext.Key != "") {
return authTLSContext, errors.New("--registry-auth-tlscontext requires cert=<cert>,key=<key>")
}
}
return authTLSContext, nil
}

func ParseRegistryAuthTLSContext(registryAuthTLSContext []string) ([]authprovider.AuthTLSContextEntry, error) {
var tlsContexts []authprovider.AuthTLSContextEntry
for _, c := range registryAuthTLSContext {
authTLSContext, err := parseRegistryAuthTLSContextCSV(c)
if err != nil {
return nil, err
}
tlsContexts = append(tlsContexts, authTLSContext)
}
return tlsContexts, nil
}
91 changes: 91 additions & 0 deletions cmd/buildctl/build/registryauthtlscontext_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package build

import (
"testing"

"github.com/stretchr/testify/require"

"github.com/moby/buildkit/session/auth/authprovider"
)

func TestParseRegistryAuthTLSContext(t *testing.T) {
type testCase struct {
registryAuthTLSContext []string //--registry-auth-tlscontext
expected []authprovider.AuthTLSContextEntry
expectedErr string
}
testCases := []testCase{
{
registryAuthTLSContext: []string{
"host=tcp://myserver:2376,ca=~/ca-file,cert=~/cert-file,key=~/key-file",
},
expected: []authprovider.AuthTLSContextEntry{
{
Host: "tcp://myserver:2376",
CA: "~/ca-file",
Cert: "~/cert-file",
Key: "~/key-file",
},
},
},
{
registryAuthTLSContext: []string{
"host=tcp://myserver:2376,cert=~/cert-file,key=~/key-file",
},
expected: []authprovider.AuthTLSContextEntry{
{
Host: "tcp://myserver:2376",
Cert: "~/cert-file",
Key: "~/key-file",
},
},
},
{
registryAuthTLSContext: []string{
"host=tcp://myserver:2376,ca=~/ca-file",
},
expected: []authprovider.AuthTLSContextEntry{
{
Host: "tcp://myserver:2376",
CA: "~/ca-file",
},
},
},
{
registryAuthTLSContext: []string{
"host=tcp://myserver:2376,ca=~/ca-file,key=~/key-file",
},
expectedErr: "--registry-auth-tlscontext requires cert=<cert>,key=<key>",
},
{
registryAuthTLSContext: []string{
"host=tcp://myserver:2376,ca=~/ca-file,cert=~/cert-file,key=~/key-file",
"host=https://myserver:2376,ca=/path/to/my/ca.crt,cert=/path/to/my/cert.crt,key=/path/to/my/key.crt",
},
expected: []authprovider.AuthTLSContextEntry{
{
Host: "tcp://myserver:2376",
CA: "~/ca-file",
Cert: "~/cert-file",
Key: "~/key-file",
},
{
Host: "https://myserver:2376",
CA: "/path/to/my/ca.crt",
Cert: "/path/to/my/cert.crt",
Key: "/path/to/my/key.crt",
},
},
},
}

for _, tc := range testCases {
im, err := ParseRegistryAuthTLSContext(tc.registryAuthTLSContext)
if tc.expectedErr == "" {
require.EqualValues(t, tc.expected, im)
} else {
require.Error(t, err)
require.Contains(t, err.Error(), tc.expectedErr)
}
}
}
38 changes: 38 additions & 0 deletions session/auth/authprovider/authconfig.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package authprovider

type AuthConfig struct {
RootCAs []string
KeyPairs []TLSKeyPair
}

type TLSKeyPair struct {
Key string
Certificate string
}

type AuthTLSContextEntry struct {
Host string
CA string
Cert string
Key string
}

func parseAuthConfigs(tlsContexts []AuthTLSContextEntry) map[string]*AuthConfig {
authConfigs := make(map[string]*AuthConfig)
for _, c := range tlsContexts {
_, ok := authConfigs[c.Host]
if !ok {
authConfigs[c.Host] = &AuthConfig{}
}
if c.CA != "" {
authConfigs[c.Host].RootCAs = append(authConfigs[c.Host].RootCAs, c.CA)
}
if c.Cert != "" && c.Key != "" {
authConfigs[c.Host].KeyPairs = append(authConfigs[c.Host].KeyPairs, TLSKeyPair{
Key: c.Key,
Certificate: c.Cert,
})
}
}
return authConfigs
}
62 changes: 55 additions & 7 deletions session/auth/authprovider/authprovider.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@ import (
"crypto/ed25519"
"crypto/hmac"
"crypto/sha256"
"crypto/tls"
"crypto/x509"
"fmt"
"net/http"
"os"
"runtime"
"strconv"
"strings"
"sync"
Expand All @@ -18,26 +21,28 @@ import (
"github.com/docker/cli/cli/config"
"github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/cli/config/types"
"github.com/moby/buildkit/session"
"github.com/moby/buildkit/session/auth"
"github.com/moby/buildkit/util/progress/progresswriter"
"github.com/pkg/errors"
"golang.org/x/crypto/nacl/sign"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"

"github.com/moby/buildkit/session"
"github.com/moby/buildkit/session/auth"
"github.com/moby/buildkit/util/progress/progresswriter"
)

const defaultExpiration = 60
const dockerHubConfigfileKey = "https://index.docker.io/v1/"
const dockerHubRegistryHost = "registry-1.docker.io"

func NewDockerAuthProvider(cfg *configfile.ConfigFile) session.Attachable {
func NewDockerAuthProvider(cfg *configfile.ConfigFile, tlsContexts []AuthTLSContextEntry) session.Attachable {
return &authProvider{
authConfigCache: map[string]*types.AuthConfig{},
config: cfg,
seeds: &tokenSeeds{dir: config.Dir()},
loggerCache: map[string]struct{}{},
tlsConfigs: parseAuthConfigs(tlsContexts),
}
}

Expand All @@ -47,6 +52,7 @@ type authProvider struct {
seeds *tokenSeeds
logger progresswriter.Logger
loggerCache map[string]struct{}
tlsConfigs map[string]*AuthConfig

// The need for this mutex is not well understood.
// Without it, the docker cli on OS X hangs when
Expand Down Expand Up @@ -89,6 +95,12 @@ func (ap *authProvider) FetchToken(ctx context.Context, req *auth.FetchTokenRequ
Secret: creds.Secret,
}

var httpClient = http.DefaultClient
if tc, err := ap.tlsConfig(req.Host); err == nil {
httpClient.Transport = http.DefaultTransport
httpClient.Transport.(*http.Transport).TLSClientConfig = tc
}

if creds.Secret != "" {
done := func(progresswriter.SubLogger) error {
return err
Expand All @@ -103,15 +115,15 @@ func (ap *authProvider) FetchToken(ctx context.Context, req *auth.FetchTokenRequ
}
ap.mu.Unlock()
// credential information is provided, use oauth POST endpoint
resp, err := authutil.FetchTokenWithOAuth(ctx, http.DefaultClient, nil, "buildkit-client", to)
resp, err := authutil.FetchTokenWithOAuth(ctx, httpClient, nil, "buildkit-client", to)
if err != nil {
var errStatus remoteserrors.ErrUnexpectedStatus
if errors.As(err, &errStatus) {
// Registries without support for POST may return 404 for POST /v2/token.
// As of September 2017, GCR is known to return 404.
// As of February 2018, JFrog Artifactory is known to return 401.
if (errStatus.StatusCode == 405 && to.Username != "") || errStatus.StatusCode == 404 || errStatus.StatusCode == 401 {
resp, err := authutil.FetchToken(ctx, http.DefaultClient, nil, to)
resp, err := authutil.FetchToken(ctx, httpClient, nil, to)
if err != nil {
return nil, err
}
Expand All @@ -123,13 +135,49 @@ func (ap *authProvider) FetchToken(ctx context.Context, req *auth.FetchTokenRequ
return toTokenResponse(resp.AccessToken, resp.IssuedAt, resp.ExpiresIn), nil
}
// do request anonymously
resp, err := authutil.FetchToken(ctx, http.DefaultClient, nil, to)
resp, err := authutil.FetchToken(ctx, httpClient, nil, to)
if err != nil {
return nil, errors.Wrap(err, "failed to fetch anonymous token")
}
return toTokenResponse(resp.Token, resp.IssuedAt, resp.ExpiresIn), nil
}

func (ap *authProvider) tlsConfig(host string) (*tls.Config, error) {
c, ok := ap.tlsConfigs[host]
if !ok {
return nil, nil
}
tc := &tls.Config{}
if len(c.RootCAs) > 0 {
systemPool, err := x509.SystemCertPool()
if err != nil {
if runtime.GOOS == "windows" {
systemPool = x509.NewCertPool()
} else {
return nil, errors.Wrapf(err, "unable to get system cert pool")
}
}
tc.RootCAs = systemPool
}

for _, p := range c.RootCAs {
dt, err := os.ReadFile(p)
if err != nil {
return nil, errors.Wrapf(err, "failed to read %s", p)
}
tc.RootCAs.AppendCertsFromPEM(dt)
}

for _, kp := range c.KeyPairs {
cert, err := tls.LoadX509KeyPair(kp.Certificate, kp.Key)
if err != nil {
return nil, errors.Wrapf(err, "failed to load keypair for %s", kp.Certificate)
}
tc.Certificates = append(tc.Certificates, cert)
}
return tc, nil
}

func (ap *authProvider) credentials(host string) (*auth.CredentialsResponse, error) {
ac, err := ap.getAuthConfig(host)
if err != nil {
Expand Down
5 changes: 3 additions & 2 deletions session/auth/authprovider/authprovider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ import (

"github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/cli/config/types"
"github.com/moby/buildkit/session/auth"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/moby/buildkit/session/auth"
)

func TestFetchTokenCaching(t *testing.T) {
Expand All @@ -17,7 +18,7 @@ func TestFetchTokenCaching(t *testing.T) {
dockerHubConfigfileKey: {Username: "user", RegistryToken: "hunter2"},
},
}
p := NewDockerAuthProvider(cfg).(*authProvider)
p := NewDockerAuthProvider(cfg, []AuthTLSContextEntry{}).(*authProvider)
res, err := p.FetchToken(context.Background(), &auth.FetchTokenRequest{Host: dockerHubRegistryHost})
require.NoError(t, err)
assert.Equal(t, "hunter2", res.Token)
Expand Down

0 comments on commit d2b6df8

Please sign in to comment.