Add "environment" credential helper
This commit is contained in:
parent
e328007bc1
commit
a8633cced4
|
|
@ -49,6 +49,9 @@ RUN go install github.com/awslabs/amazon-ecr-credential-helper/ecr-login/cli/doc
|
|||
# Get ACR docker env credential helper
|
||||
RUN go install github.com/chrismellard/docker-credential-acr-env
|
||||
|
||||
# Get docker generic environment credential helper
|
||||
RUN go install github.com/isometry/docker-credential-env
|
||||
|
||||
RUN \
|
||||
--mount=type=cache,target=/root/.cache/go-build \
|
||||
--mount=type=cache,target=/go/pkg \
|
||||
|
|
@ -78,6 +81,7 @@ FROM kaniko-base-slim AS kaniko-base
|
|||
COPY --from=builder --chown=0:0 /usr/local/bin/docker-credential-gcr /kaniko/docker-credential-gcr
|
||||
COPY --from=builder --chown=0:0 /usr/local/bin/docker-credential-ecr-login /kaniko/docker-credential-ecr-login
|
||||
COPY --from=builder --chown=0:0 /usr/local/bin/docker-credential-acr-env /kaniko/docker-credential-acr-env
|
||||
COPY --from=builder --chown=0:0 /usr/local/bin/docker-credential-env /kaniko/docker-credential-env
|
||||
|
||||
COPY --from=builder /kaniko/.docker /kaniko/.docker
|
||||
|
||||
|
|
|
|||
1
go.mod
1
go.mod
|
|
@ -166,6 +166,7 @@ require (
|
|||
github.com/google/s2a-go v0.1.7 // indirect
|
||||
github.com/google/subcommands v1.2.0 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/isometry/docker-credential-env v1.3.0 // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
|
|
|
|||
2
go.sum
2
go.sum
|
|
@ -312,6 +312,8 @@ github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
|||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/isometry/docker-credential-env v1.3.0 h1:0YCSPhbtJ096HwFKagF+sxveynKzgfnxui6UHhghtBE=
|
||||
github.com/isometry/docker-credential-env v1.3.0/go.mod h1:k7IkbTjh/x63jnYMHY9IGZ1al/gPfWJGcr8GI4mHGXY=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
||||
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
|
||||
|
|
|
|||
|
|
@ -25,4 +25,5 @@ import (
|
|||
_ "github.com/GoogleCloudPlatform/docker-credential-gcr/v2"
|
||||
_ "github.com/awslabs/amazon-ecr-credential-helper/ecr-login/cli/docker-credential-ecr-login"
|
||||
_ "github.com/chrismellard/docker-credential-acr-env"
|
||||
_ "github.com/isometry/docker-credential-env"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,2 @@
|
|||
dist
|
||||
docker-credential-env
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
before:
|
||||
hooks:
|
||||
- go mod tidy
|
||||
builds:
|
||||
- env:
|
||||
- CGO_ENABLED=0
|
||||
mod_timestamp: "{{ .CommitTimestamp }}"
|
||||
flags:
|
||||
- -trimpath
|
||||
ldflags:
|
||||
- "-s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}"
|
||||
goos:
|
||||
- freebsd
|
||||
- windows
|
||||
- linux
|
||||
- darwin
|
||||
goarch:
|
||||
- amd64
|
||||
- "386"
|
||||
- arm
|
||||
- arm64
|
||||
binary: docker-credential-env
|
||||
archives:
|
||||
- format: zip
|
||||
name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
|
||||
snapshot:
|
||||
name_template: "{{ .Tag }}-next"
|
||||
checksum:
|
||||
name_template: "{{ .ProjectName }}_{{ .Version }}_SHA256SUMS"
|
||||
algorithm: sha256
|
||||
release:
|
||||
draft: false
|
||||
changelog:
|
||||
sort: asc
|
||||
filters:
|
||||
exclude:
|
||||
- "^docs:"
|
||||
- "^test:"
|
||||
brews:
|
||||
- tap:
|
||||
owner: isometry
|
||||
name: homebrew-tap
|
||||
token: "{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}"
|
||||
folder: Formula
|
||||
description: Environment-driven Docker credential helper
|
||||
homepage: https://just.breathe.io/project/docker-credential-env/
|
||||
test: |
|
||||
system "#{bin}/docker-credential-env --version"
|
||||
install: |
|
||||
bin.install "docker-credential-env"
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright © 2021 Robin Breathe <robin@breathe.io>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
# Docker Credentials from the Environment
|
||||
|
||||
A [Docker credential helper](https://docs.docker.com/engine/reference/commandline/login/#credential-helpers) to streamline repository interactions in scenarios where the cacheing of credentials to `~/.docker/config.json` is undesirable, including CI/CD pipelines, or anywhere ephemeral credentials are used.
|
||||
|
||||
All OCI registry clients that support `~/.docker/config.json` are supported, including [`oras`](https://oras.land/), [`crane`](https://github.com/google/go-containerregistry/blob/main/cmd/crane/README.md), [`grype`](https://github.com/anchore/grype), etc.
|
||||
|
||||
In addition to handling basic username:password credentials, the credential helper also includes special support for:
|
||||
|
||||
* Amazon Elastic Container Registry (ECR) repositories using [standard AWS credentials](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html), including automatic cross-account role assumption.
|
||||
* [GitHub Packages](https://ghcr.io/) via the common `GITHUB_TOKEN` environment variable.
|
||||
|
||||
## Environment Variables
|
||||
|
||||
For the docker repository `https://repo.example.com/v1`, the credential helper expects to retrieve credentials from the following environment variables:
|
||||
|
||||
* `DOCKER_repo_example_com_USR` containing the repository username
|
||||
* `DOCKER_repo_example_com_PSW` containing the repository password, token or secret.
|
||||
|
||||
If no environment variables for the target repository's FQDN is found, then:
|
||||
|
||||
1. The helper will remove DNS labels from the FQDN one-at-a-time from the right, and look again, for example:
|
||||
`DOCKER_repo_example_com_USR` => `DOCKER_example_com_USR` => `DOCKER_com_USR` => `DOCKER__USR`.
|
||||
2. If the target repository is a private AWS ECR repository (FQDN matches the regex `^[0-9]+\.dkr\.ecr\.[-a-z0-9]+\.amazonaws\.com$`), it will attempt to exchange local AWS credentials (most likely exposed through `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` environment variables) for short-lived ECR login credentials, including automatic sts:AssumeRole if `role_arn` is specified (e.g. via `AWS_ROLE_ARN`).
|
||||
|
||||
Hyphens within DNS labels are transformed to underscores (`s/-/_/g`) for the purposes of credential lookup.
|
||||
|
||||
## Configuration
|
||||
|
||||
The `docker-credential-env` binary must be installed to `$PATH`, and is enabled via `~/.docker/config.json`:
|
||||
|
||||
* Handle all docker authentication:
|
||||
|
||||
```json
|
||||
{
|
||||
"credsStore": "env"
|
||||
}
|
||||
```
|
||||
|
||||
* Handle docker authentication for specific repositories:
|
||||
|
||||
```json
|
||||
{
|
||||
"credHelpers": {
|
||||
"artifactory.example.com": "env"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
By default, attempts to explicitly `docker {login,logout}` will generate an error. To ignore these errors, set the environment variable `IGNORE_DOCKER_LOGIN=1`.
|
||||
|
||||
## Example Usage
|
||||
|
||||
### Jenkins
|
||||
|
||||
```groovy
|
||||
stages {
|
||||
stage('Push Image to Artifactory') {
|
||||
environment {
|
||||
DOCKER_artifactory_example_com = credentials('jenkins.artifactory') // (Vault) Username-Password credential
|
||||
}
|
||||
steps {
|
||||
sh 'docker push artifactory.example.com/example/example-image:1.0'
|
||||
}
|
||||
}
|
||||
|
||||
stage('Push Image to Docker Hub') {
|
||||
environment {
|
||||
DOCKER_docker_com = credentials('hub.docker.com') // Username-Password credential, exploiting domain search
|
||||
}
|
||||
steps {
|
||||
sh 'docker push hub.docker.com/example/example-image:1.0'
|
||||
}
|
||||
}
|
||||
|
||||
stage('Push Image to AWS-ECR') {
|
||||
environment {
|
||||
// any standard AWS authentication mechanisms are supported
|
||||
AWS_ROLE_ARN = 'arn:aws:iam::123456789:role/jenkins-user' // triggers automatic sts:AssumeRole
|
||||
// AWS_CONFIG_FILE = file('AWS_CONFIG')
|
||||
// AWS_PROFILE = 'jenkins'
|
||||
AWS_ACCESS_KEY_ID = credentials('AWS_ACCESS_KEY_ID') // String credential
|
||||
AWS_SECRET_ACCESS_KEY = credentials('AWS_SECRET_ACCESS_KEY') // String credential
|
||||
}
|
||||
steps {
|
||||
sh 'docker push 123456789.dkr.ecr.us-east-1.amazonaws.com/example/example-image:1.0'
|
||||
}
|
||||
}
|
||||
|
||||
stage('Push Image to GHCR') {
|
||||
environment {
|
||||
GITHUB_TOKEN = credentials('github') // String credential
|
||||
}
|
||||
steps {
|
||||
sh 'docker push ghcr.io/example/example-image:1.0'
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
|
@ -0,0 +1,190 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/config"
|
||||
"github.com/aws/aws-sdk-go-v2/credentials/stscreds"
|
||||
"github.com/aws/aws-sdk-go-v2/service/ecr"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sts"
|
||||
docker_credentials "github.com/docker/docker-credential-helpers/credentials"
|
||||
)
|
||||
|
||||
var ecrHostname = regexp.MustCompile(`^[0-9]+\.dkr\.ecr\.[-a-z0-9]+\.amazonaws\.com$`)
|
||||
var ghcrHostname = regexp.MustCompile(`^ghcr\.io$`)
|
||||
|
||||
const (
|
||||
defaultScheme = "https://"
|
||||
envPrefix = "DOCKER"
|
||||
envUsernameSuffix = "USR"
|
||||
envPasswordSuffix = "PSW"
|
||||
envSeparator = "_"
|
||||
envIgnoreLogin = "IGNORE_DOCKER_LOGIN"
|
||||
)
|
||||
|
||||
type NotSupportedError struct{}
|
||||
|
||||
func (m *NotSupportedError) Error() string {
|
||||
return "not supported"
|
||||
}
|
||||
|
||||
// Env implements the Docker credentials Helper interface.
|
||||
type Env struct{}
|
||||
|
||||
// Add implements the set verb
|
||||
func (*Env) Add(*docker_credentials.Credentials) error {
|
||||
switch {
|
||||
case os.Getenv(envIgnoreLogin) != "":
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("add: %w", &NotSupportedError{})
|
||||
}
|
||||
}
|
||||
|
||||
// Delete implements the erase verb
|
||||
func (*Env) Delete(string) error {
|
||||
switch {
|
||||
case os.Getenv(envIgnoreLogin) != "":
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("delete: %w", &NotSupportedError{})
|
||||
}
|
||||
}
|
||||
|
||||
// List implements the list verb
|
||||
func (*Env) List() (map[string]string, error) {
|
||||
return nil, fmt.Errorf("list: %w", &NotSupportedError{})
|
||||
}
|
||||
|
||||
// Get implements the get verb
|
||||
func (e *Env) Get(serverURL string) (username string, password string, err error) {
|
||||
var (
|
||||
hostname string
|
||||
ok bool
|
||||
)
|
||||
|
||||
hostname, err = getHostname(serverURL)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if username, password, ok = getEnvCredentials(hostname); ok {
|
||||
return
|
||||
}
|
||||
|
||||
if ecrHostname.MatchString(hostname) {
|
||||
// This is an AWS ECR Docker Registry: <account-id>.dkr.ecr.<region>.amazonaws.com
|
||||
username, password, err = getEcrToken()
|
||||
return
|
||||
}
|
||||
|
||||
if ghcrHostname.MatchString(hostname) {
|
||||
// This is a GitHub Container Registry: ghcr.io
|
||||
if token, found := os.LookupEnv("GITHUB_TOKEN"); found {
|
||||
username = "github"
|
||||
password = token
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func getHostname(serverURL string) (hostname string, err error) {
|
||||
var server *url.URL
|
||||
server, err = url.Parse(defaultScheme + strings.TrimPrefix(serverURL, defaultScheme))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
hostname = server.Hostname()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func getEnvVariables(labels []string, offset int) (envUsername, envPassword string) {
|
||||
if offset < 0 {
|
||||
offset = 0
|
||||
} else if offset > len(labels) {
|
||||
offset = len(labels)
|
||||
}
|
||||
|
||||
envHostname := strings.Join(labels[offset:], envSeparator)
|
||||
envUsername = strings.Join([]string{envPrefix, envHostname, envUsernameSuffix}, envSeparator)
|
||||
envPassword = strings.Join([]string{envPrefix, envHostname, envPasswordSuffix}, envSeparator)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func getEnvCredentials(hostname string) (username, password string, found bool) {
|
||||
hostname = strings.ReplaceAll(hostname, "-", "_")
|
||||
labels := strings.Split(hostname, ".")
|
||||
|
||||
for i := 0; i <= len(labels); i++ {
|
||||
envUsername, envPassword := getEnvVariables(labels, i)
|
||||
|
||||
if username, found = os.LookupEnv(envUsername); found {
|
||||
if password, found = os.LookupEnv(envPassword); found {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func getEcrToken() (username, password string, err error) {
|
||||
ctx := context.TODO()
|
||||
cfg, err := config.LoadDefaultConfig(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if roleArn := getRoleArn(cfg.ConfigSources...); roleArn != "" {
|
||||
stsSvc := sts.NewFromConfig(cfg)
|
||||
creds := stscreds.NewAssumeRoleProvider(stsSvc, roleArn)
|
||||
cfg.Credentials = aws.NewCredentialsCache(creds)
|
||||
}
|
||||
|
||||
client := ecr.NewFromConfig(cfg)
|
||||
|
||||
output, err := client.GetAuthorizationToken(ctx, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, authData := range output.AuthorizationData {
|
||||
// authData.AuthorizationToken is a base64-encoded username:password string,
|
||||
// where the username is always expected to be "AWS".
|
||||
var tokenBytes []byte
|
||||
tokenBytes, err = base64.StdEncoding.DecodeString(*authData.AuthorizationToken)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
token := bytes.SplitN(tokenBytes, []byte{':'}, 2)
|
||||
username, password = string(token[0]), string(token[1])
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func getRoleArn(configSources ...interface{}) (roleARN string) {
|
||||
for _, x := range configSources {
|
||||
switch impl := x.(type) {
|
||||
case config.EnvConfig:
|
||||
if impl.RoleARN != "" {
|
||||
return strings.TrimSpace(impl.RoleARN)
|
||||
}
|
||||
case config.SharedConfig:
|
||||
if impl.RoleARN != "" {
|
||||
return strings.TrimSpace(impl.RoleARN)
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
// docker-credentials-env is a Docker credentials helper that reads
|
||||
// credentials from the process environment.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
docker_credentials "github.com/docker/docker-credential-helpers/credentials"
|
||||
)
|
||||
|
||||
func main() {
|
||||
docker_credentials.Serve(&Env{})
|
||||
}
|
||||
|
|
@ -818,6 +818,9 @@ github.com/hashicorp/hcl/json/token
|
|||
# github.com/inconshreveable/mousetrap v1.1.0
|
||||
## explicit; go 1.18
|
||||
github.com/inconshreveable/mousetrap
|
||||
# github.com/isometry/docker-credential-env v1.3.0
|
||||
## explicit; go 1.22
|
||||
github.com/isometry/docker-credential-env
|
||||
# github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99
|
||||
## explicit
|
||||
github.com/jbenet/go-context/io
|
||||
|
|
|
|||
Loading…
Reference in New Issue