Skip to content

Commit 5e9bda2

Browse files
committed
cli/registry/login: Add the --password-env flag
Adds support for providing registry passwords via environment variables, which is particularly useful in CI pipelines. For example, GitLab stores registry passwords in the `CI_REGISTRY_PASSWORD` environment variable. With this change, authenticating to a registry is as simple as: ```sh docker login --username "${CI_REGISTRY_USER}" --password-env "CI_REGISTRY_PASSWORD" "${CI_REGISTRY}" ``` ## Alternatives Considered * Using `docker login -p "${VAR}"` with warning suppression * Shell history concerns don't apply with variable substitution * Tokens are often short-lived, reducing security concerns * Could introduce a way to suppress warnings via environment variables * Passing passwords via STDIN (current recommended method): ```sh echo "${CI_REGISTRY_PASSWORD}" | docker login --username "${CI_REGISTRY_USER}" --password-stdin "${CI_REGISTRY}" ``` Avoids warnings but adds complexity to command chains
1 parent 30c20d5 commit 5e9bda2

File tree

3 files changed

+80
-11
lines changed

3 files changed

+80
-11
lines changed

cli/command/registry/login.go

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ type loginOptions struct {
2828
user string
2929
password string
3030
passwordStdin bool
31+
passwordEnv string
3132
}
3233

3334
// NewLoginCommand creates a new `docker login` command
@@ -56,31 +57,45 @@ func NewLoginCommand(dockerCLI command.Cli) *cobra.Command {
5657
flags.StringVarP(&opts.user, "username", "u", "", "Username")
5758
flags.StringVarP(&opts.password, "password", "p", "", "Password or Personal Access Token (PAT)")
5859
flags.BoolVar(&opts.passwordStdin, "password-stdin", false, "Take the Password or Personal Access Token (PAT) from stdin")
60+
flags.StringVar(&opts.passwordEnv, "password-env", "", "Take the Password or Personal Access Token (PAT) from an environment variable")
5961

6062
return cmd
6163
}
6264

6365
func verifyLoginOptions(dockerCLI command.Cli, opts *loginOptions) error {
64-
if opts.password != "" {
65-
_, _ = fmt.Fprintln(dockerCLI.Err(), "WARNING! Using --password via the CLI is insecure. Use --password-stdin.")
66-
if opts.passwordStdin {
67-
return errors.New("--password and --password-stdin are mutually exclusive")
66+
switch {
67+
case opts.password != "":
68+
_, _ = fmt.Fprintln(dockerCLI.Err(), "WARNING! Using --password via the CLI is insecure. Use --password-stdin or --password-env.")
69+
if opts.passwordStdin || opts.passwordEnv != "" {
70+
return errors.New("--password, --password-stdin, and --password-env are mutually exclusive")
6871
}
69-
}
7072

71-
if opts.passwordStdin {
73+
case opts.passwordStdin:
7274
if opts.user == "" {
7375
return errors.New("Must provide --username with --password-stdin")
7476
}
7577

78+
if opts.passwordEnv != "" {
79+
return errors.New("--password, --password-stdin, and --password-env are mutually exclusive")
80+
}
81+
7682
contents, err := io.ReadAll(dockerCLI.In())
7783
if err != nil {
7884
return err
7985
}
8086

8187
opts.password = strings.TrimSuffix(string(contents), "\n")
8288
opts.password = strings.TrimSuffix(opts.password, "\r")
89+
90+
case opts.passwordEnv != "":
91+
pw, ok := os.LookupEnv(opts.passwordEnv)
92+
if !ok {
93+
return fmt.Errorf("the environment variable %q is not defined", opts.passwordEnv)
94+
}
95+
96+
opts.password = pw
8397
}
98+
8499
return nil
85100
}
86101

cli/command/registry/login_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ func TestRunLogin(t *testing.T) {
9191
testCases := []struct {
9292
doc string
9393
priorCredentials map[string]configtypes.AuthConfig
94+
env map[string]string
9495
input loginOptions
9596
expectedCredentials map[string]configtypes.AuthConfig
9697
expectedErr string
@@ -286,6 +287,39 @@ func TestRunLogin(t *testing.T) {
286287
},
287288
},
288289
},
290+
// Password from environment
291+
{
292+
doc: "valid password from environment variable",
293+
priorCredentials: map[string]configtypes.AuthConfig{},
294+
env: map[string]string{
295+
"TEST_PASSWORD": "pw0",
296+
},
297+
input: loginOptions{
298+
serverAddress: "reg1",
299+
user: "my-username",
300+
passwordEnv: "TEST_PASSWORD",
301+
},
302+
expectedCredentials: map[string]configtypes.AuthConfig{
303+
"reg1": {
304+
Username: "my-username",
305+
Password: "pw0",
306+
ServerAddress: "reg1",
307+
},
308+
},
309+
},
310+
{
311+
doc: "given environment variable is unset",
312+
priorCredentials: map[string]configtypes.AuthConfig{},
313+
env: map[string]string{
314+
"TEST_PASSWORD": "pw0",
315+
},
316+
input: loginOptions{
317+
serverAddress: "reg1",
318+
user: "my-username",
319+
passwordEnv: "DOES_NOT_EXIST",
320+
},
321+
expectedErr: `the environment variable "DOES_NOT_EXIST" is not defined`,
322+
},
289323
}
290324

291325
for _, tc := range testCases {
@@ -303,6 +337,10 @@ func TestRunLogin(t *testing.T) {
303337
assert.NilError(t, err)
304338
assert.DeepEqual(t, storedCreds, tc.priorCredentials)
305339

340+
for k, v := range tc.env {
341+
t.Setenv(k, v)
342+
}
343+
306344
loginErr := runLogin(context.Background(), cli, tc.input)
307345
if tc.expectedErr != "" {
308346
assert.Error(t, loginErr, tc.expectedErr)

docs/reference/commandline/login.md

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@ Defaults to Docker Hub if no server is specified.
66

77
### Options
88

9-
| Name | Type | Default | Description |
10-
|:---------------------------------------------|:---------|:--------|:------------------------------------------------------------|
11-
| `-p`, `--password` | `string` | | Password or Personal Access Token (PAT) |
12-
| [`--password-stdin`](#password-stdin) | `bool` | | Take the Password or Personal Access Token (PAT) from stdin |
13-
| [`-u`](#username), [`--username`](#username) | `string` | | Username |
9+
| Name | Type | Default | Description |
10+
|:---------------------------------------------|:---------|:--------|:------------------------------------------------------------------------------|
11+
| `-p`, `--password` | `string` | | Password or Personal Access Token (PAT) |
12+
| [`--password-stdin`](#password-stdin) | `bool` | | Take the Password or Personal Access Token (PAT) from stdin |
13+
| [`--password-env`](#password-env) | `string` | | Take the Password or Personal Access Token (PAT) from an environment variable |
14+
| [`-u`](#username), [`--username`](#username) | `string` | | Username |
1415

1516

1617
<!---MARKER_GEN_END-->
@@ -244,6 +245,21 @@ The following example reads a password from a file, and passes it to the
244245
$ cat ~/my_password.txt | docker login --username foo --password-stdin
245246
```
246247

248+
### <a name="password-env"></a> Provide a password using an environment variable (--password-env)
249+
250+
To avoid providing a password or access token on the command line, you can set
251+
the `--password-env` flag to provide a password through an environment
252+
variable. Using an environment variable prevents the password from ending up in
253+
the shell's history, or log-files. This is particularly useful for CI
254+
pipelines, that often get access to secrets through environment variables.
255+
256+
The following example authenticates to a registry using an access token stored
257+
in the `DOCKER_TOKEN` environment variable:
258+
259+
```console
260+
$ docker login --username foo --password-env "DOCKER_TOKEN"
261+
```
262+
247263
## Related commands
248264

249265
* [logout](logout.md)

0 commit comments

Comments
 (0)