Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docker: fix a bug where auth for private registries wasn't parsed correctly #24215

Merged
merged 16 commits into from
Oct 16, 2024
3 changes: 3 additions & 0 deletions .changelog/24215.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:bug
docker: Fix incorrect auth parsing for private registries
```
5 changes: 5 additions & 0 deletions drivers/docker/driver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2481,6 +2481,7 @@ func TestDockerDriver_AuthConfiguration(t *testing.T) {
{
Repo: "redis:7",
AuthConfig: &registry.AuthConfig{
Auth: "eyJ1c2VybmFtZSI6InRlc3QiLCJwYXNzd29yZCI6IjEyMzQifQ==",
Username: "test",
Password: "1234",
Email: "",
Expand All @@ -2490,6 +2491,7 @@ func TestDockerDriver_AuthConfiguration(t *testing.T) {
{
Repo: "quay.io/redis:7",
AuthConfig: &registry.AuthConfig{
Auth: "eyJ1c2VybmFtZSI6InRlc3QiLCJwYXNzd29yZCI6IjU2NzgifQ==",
Username: "test",
Password: "5678",
Email: "",
Expand All @@ -2499,6 +2501,7 @@ func TestDockerDriver_AuthConfiguration(t *testing.T) {
{
Repo: "other.io/redis:7",
AuthConfig: &registry.AuthConfig{
Auth: "eyJ1c2VybmFtZSI6InRlc3QiLCJwYXNzd29yZCI6ImFiY2QifQ==",
Username: "test",
Password: "abcd",
Email: "",
Expand Down Expand Up @@ -2535,6 +2538,7 @@ func TestDockerDriver_AuthFromTaskConfig(t *testing.T) {
ServerAddr: "www.foobar.com",
},
AuthConfig: &registry.AuthConfig{
Auth: "eyJ1c2VybmFtZSI6ImZvbyIsInBhc3N3b3JkIjoiYmFyIn0=",
Username: "foo",
Password: "bar",
Email: "[email protected]",
Expand All @@ -2549,6 +2553,7 @@ func TestDockerDriver_AuthFromTaskConfig(t *testing.T) {
ServerAddr: "www.foobar.com",
},
AuthConfig: &registry.AuthConfig{
Auth: "eyJ1c2VybmFtZSI6ImZvbyIsInBhc3N3b3JkIjoiYmFyIn0=",
Username: "foo",
Password: "bar",
ServerAddress: "www.foobar.com",
Expand Down
35 changes: 33 additions & 2 deletions drivers/docker/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package docker

import (
"encoding/base64"
"encoding/json"
"fmt"
"os"
Expand Down Expand Up @@ -102,12 +103,19 @@ func authFromTaskConfig(driverConfig *TaskConfig) authBackend {
if len(driverConfig.Auth.Username) == 0 && len(driverConfig.Auth.Password) == 0 && len(driverConfig.Auth.Email) == 0 && len(driverConfig.Auth.ServerAddr) == 0 {
return nil, nil
}
return &registrytypes.AuthConfig{

authConfig := &registrytypes.AuthConfig{
Username: driverConfig.Auth.Username,
Password: driverConfig.Auth.Password,
Email: driverConfig.Auth.Email,
ServerAddress: driverConfig.Auth.ServerAddr,
}, nil
}

if err := encodeAuth(authConfig); err != nil {
return nil, err
}

return authConfig, nil
}
}

Expand Down Expand Up @@ -140,6 +148,11 @@ func authFromDockerConfig(file string) authBackend {
IdentityToken: dockerAuthConfig.IdentityToken,
RegistryToken: dockerAuthConfig.RegistryToken,
}

if err := encodeAuth(auth); err != nil {
return nil, err
}

if authIsEmpty(auth) {
return nil, nil
}
Expand Down Expand Up @@ -187,6 +200,9 @@ func authFromHelper(helperName string) authBackend {
Username: response["Username"],
Password: response["Secret"],
}
if err := encodeAuth(auth); err != nil {
return nil, err
}

if authIsEmpty(auth) {
return nil, nil
Expand All @@ -195,6 +211,21 @@ func authFromHelper(helperName string) authBackend {
}
}

// some docker api calls require a base64 encoded basic auth string
func encodeAuth(cfg *registrytypes.AuthConfig) error {
auth := &registrytypes.AuthConfig{
Username: cfg.Username,
Password: cfg.Password,
}
encodedJSON, err := json.Marshal(auth)
if err != nil {
return fmt.Errorf("error encoding basic auth: %v", err)
}

cfg.Auth = base64.URLEncoding.EncodeToString(encodedJSON)
return nil
}

// authIsEmpty returns if auth is nil or an empty structure
func authIsEmpty(auth *registrytypes.AuthConfig) bool {
if auth == nil {
Expand Down
5 changes: 5 additions & 0 deletions e2e/docker/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

// Package docker contains test cases related to the docker task driver.
package docker
125 changes: 125 additions & 0 deletions e2e/docker/docker_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package docker

import (
"fmt"
"strconv"
"testing"
"time"

"github.com/hashicorp/nomad/e2e/e2eutil"
"github.com/hashicorp/nomad/e2e/v3/cluster3"
"github.com/hashicorp/nomad/e2e/v3/jobs3"
"github.com/shoenig/test/must"
)

const (
registryService = "registry"
)

func TestDocker(t *testing.T) {
cluster3.Establish(t,
cluster3.Leader(),
cluster3.LinuxClients(1),
)

runRegistry(t)

t.Run("testRedis", testRedis)
t.Run("testAuthBasic", testAuthBasic)
t.Run("testAuthFileStatic", testAuthFileStatic)
t.Run("testAuthHelper", testAuthHelper)
}

func findService(t *testing.T, name string) (string, int) {
services, _, err := e2eutil.NomadClient(t).Services().Get(name, nil)
must.NoError(t, err, must.Sprintf("failed to find %q service", name))
must.Len(t, 1, services, must.Sprintf("expected 1 %q service", name))
return services[0].Address, services[0].Port
}

func runRegistry(t *testing.T) {
_, regCleanup := jobs3.Submit(t,
"../docker_registry/registry.hcl",
jobs3.Timeout(40*time.Second), // pulls an image
)
t.Cleanup(regCleanup)

// lookup registry address
addr, port := findService(t, registryService)
address := fmt.Sprintf("%s:%d", addr, port)

t.Logf("Setting up insecure private registry at %v", address)

// run the sed job to fixup the auth.json file with correct address and make
// sure the registry is marked as insecure for docker, otherwise pulls will
// fail
_, sedCleanup := jobs3.Submit(t,
"./input/registry-auths.hcl",
jobs3.Var("registry_address", address),
jobs3.Var("user", "root"),
jobs3.Var("helper_dir", "/usr/local/bin"),
jobs3.Var("auth_dir", "/etc"),
jobs3.Var("docker_conf_dir", "/etc/docker"),
jobs3.WaitComplete("create-files"),
jobs3.Timeout(20*time.Second),
)
t.Cleanup(sedCleanup)
}

func testRedis(t *testing.T) {
job, cleanup := jobs3.Submit(t, "./input/redis.hcl")
t.Cleanup(cleanup)

logs := job.TaskLogs("cache", "redis")
must.StrContains(t, logs.Stdout, "oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👻

}

func testAuthBasic(t *testing.T) {
// find the private registry service
regAddr, regPort := findService(t, "registry")

// run the private bash image
bashJob, bashCleanup := jobs3.Submit(t, "./input/auth_basic.hcl",
jobs3.Var("registry_address", regAddr),
jobs3.Var("registry_port", strconv.Itoa(regPort)),
jobs3.WaitComplete("basic"),
)
t.Cleanup(bashCleanup)
logs := bashJob.TaskLogs("basic", "echo")
must.StrContains(t, logs.Stdout, "The auth basic test is OK!")
}

func testAuthFileStatic(t *testing.T) {
// find the private registry service
regAddr, regPort := findService(t, "registry")

// run the private _static bash image
bashJob, bashCleanup := jobs3.Submit(t, "./input/auth_static.hcl",
jobs3.Var("registry_address", regAddr),
jobs3.Var("registry_port", strconv.Itoa(regPort)),
jobs3.WaitComplete("static"),
)
t.Cleanup(bashCleanup)
logs := bashJob.TaskLogs("static", "echo")
must.StrContains(t, logs.Stdout, "The static auth test is OK!")
}

func testAuthHelper(t *testing.T) {
// find the private registry service
regAddr, regPort := findService(t, "registry")

t.Log("registry", regAddr, regPort)

// run the private _helper bash image
bashJob, bashCleanup := jobs3.Submit(t, "./input/auth_helper.hcl",
jobs3.Var("registry_address", regAddr),
jobs3.Var("registry_port", strconv.Itoa(regPort)),
jobs3.WaitComplete("helper"),
)
t.Cleanup(bashCleanup)
logs := bashJob.TaskLogs("helper", "echo")
must.StrContains(t, logs.Stdout, "The credentials helper auth test is OK!")
}
75 changes: 75 additions & 0 deletions e2e/docker/input/auth_basic.hcl
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Copyright (c) HashiCorp, Inc.
# SPDX-License-Identifier: BUSL-1.1

# This job runs a docker task using a container stored in a private registry
# configured with basic authentication. The registry.hcl job should be running
# and healthy before running this job. The registry_address and registry_port
# HCL variables must be provided.

variable "registry_address" {
type = string
description = "The HTTP address of the local registry"
default = "localhost"
}

variable "registry_port" {
type = number
description = "The HTTP port of the local registry"
default = "7511"
}

variable "registry_username" {
type = string
description = "The Basic Auth username of the local registry"
default = "auth_basic_user"
}

variable "registry_password" {
type = string
description = "The Basic Auth password of the local registry"
default = "auth_basic_pass"
}

locals {
registry_auth = base64encode("${var.registry_username}:${var.registry_password}")
}

job "auth_basic" {
type = "batch"

constraint {
attribute = "${attr.kernel.name}"
value = "linux"
}

group "basic" {
reschedule {
attempts = 0
unlimited = false
}

network {
mode = "host"
}

task "echo" {
driver = "docker"

config {
image = "${var.registry_address}:${var.registry_port}/docker.io/library/bash_auth_basic:private"
args = ["echo", "The auth basic test is OK!"]
auth_soft_fail = true

auth {
username = "${var.registry_username}"
password = "${var.registry_password}"
}
}

resources {
cpu = 100
memory = 64
}
}
}
}
55 changes: 55 additions & 0 deletions e2e/docker/input/auth_helper.hcl
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Copyright (c) HashiCorp, Inc.
# SPDX-License-Identifier: BUSL-1.1

# This job runs a docker task using a container stored in a private registry
# configured with credentials helper authentication. The registry.hcl job should
# be running and healthy before running this job.

variable "registry_address" {
type = string
description = "The HTTP address of the local registry"
default = "localhost"
}

variable "registry_port" {
type = number
description = "The HTTP port of the local registry"
default = "7511"
}

job "auth_static" {
type = "batch"

constraint {
attribute = "${attr.kernel.name}"
value = "linux"
}

group "helper" {
reschedule {
attempts = 0
unlimited = false
}

network {
mode = "host"
}

task "echo" {
driver = "docker"

config {
image = "${var.registry_address}:${var.registry_port}/docker.io/library/bash_auth_helper:private"
args = ["echo", "The credentials helper auth test is OK!"]

# usename and password come from [docker-credential-]test.sh found on
# $PATH as specified by "helper=test.sh" in plugin config
}

resources {
cpu = 100
memory = 64
}
}
}
}
Loading