Skip to content

Commit

Permalink
ci-9739 - workload identity/OIDC support for GAR/GCR (#413)
Browse files Browse the repository at this point in the history
* adds support for oidc access tokens gar/gcr
  • Loading branch information
eoinmcafee00 authored Oct 23, 2023
1 parent c354cd6 commit 757a756
Show file tree
Hide file tree
Showing 6 changed files with 314 additions and 41 deletions.
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,26 @@ steps:
from_secret: gcr_json_key
```
### GAR (Google Artifact Registry) using workload identity (OIDC)
```yaml
steps:
- name: push-to-gar
image: plugins/gcr
pull: never
settings:
tag: latest
repo: project-id/repo/image-name
registry_type: GAR
location: europe
project_number: project-number
pool_id: workload identity pool id
provider_id: workload identity provider id
service_account_email: service account email
oidc_token_id:
from_secret: token
```
## Developer Notes
- When updating the base image, you will need to update for each architecture and OS.
Expand Down
16 changes: 11 additions & 5 deletions cmd/drone-docker/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,11 @@ func main() {
Usage: "registry type",
EnvVar: "PLUGIN_REGISTRY_TYPE",
},
cli.StringFlag{
Name: "access-token",
Usage: "access token",
EnvVar: "ACCESS_TOKEN",
},
}

if err := app.Run(os.Args); err != nil {
Expand All @@ -309,11 +314,12 @@ func run(c *cli.Context) error {
Dryrun: c.Bool("dry-run"),
Cleanup: c.BoolT("docker.purge"),
Login: docker.Login{
Registry: c.String("docker.registry"),
Username: c.String("docker.username"),
Password: c.String("docker.password"),
Email: c.String("docker.email"),
Config: c.String("docker.config"),
Registry: c.String("docker.registry"),
Username: c.String("docker.username"),
Password: c.String("docker.password"),
Email: c.String("docker.email"),
Config: c.String("docker.config"),
AccessToken: c.String("access-token"),
},
CardPath: c.String("drone-card-path"),
ArtifactFile: c.String("artifact-file"),
Expand Down
121 changes: 99 additions & 22 deletions cmd/drone-gcr/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,15 @@ import (
"strconv"
"strings"

docker "github.com/drone-plugins/drone-docker"

"github.com/joho/godotenv"
"github.com/sirupsen/logrus"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"

docker "github.com/drone-plugins/drone-docker"
"google.golang.org/api/iamcredentials/v1"
"google.golang.org/api/option"
"google.golang.org/api/sts/v1"
)

type Config struct {
Expand All @@ -25,11 +29,21 @@ type Config struct {
WorkloadIdentity bool
Username string
RegistryType string
AccessToken string
}

type staticTokenSource struct {
token *oauth2.Token
}

func (s *staticTokenSource) Token() (*oauth2.Token, error) {
return s.token, nil
}

func loadConfig() Config {
// Default username
username := "_json_key"
var config Config

// Load env-file if it exists
if env := os.Getenv("PLUGIN_ENV_FILE"); env != "" {
Expand All @@ -38,18 +52,36 @@ func loadConfig() Config {
}
}

idToken := getenv("PLUGIN_OIDC_TOKEN_ID")
projectId := getenv("PLUGIN_PROJECT_NUMBER")
poolId := getenv("PLUGIN_POOL_ID")
providerId := getenv("PLUGIN_PROVIDER_ID")
serviceAccountEmail := getenv("PLUGIN_SERVICE_ACCOUNT_EMAIL")

if idToken != "" && projectId != "" && poolId != "" && providerId != "" && serviceAccountEmail != "" {
federalToken, err := getFederalToken(idToken, projectId, poolId, providerId)
if err != nil {
logrus.Fatalf("Error (getFederalToken): %s", err)
}
accessToken, err := getGoogleCloudAccessToken(federalToken, serviceAccountEmail)
if err != nil {
logrus.Fatalf("Error (getGoogleCloudAccessToken): %s", err)
}
config.AccessToken = accessToken
} else {
password := getenv(
"PLUGIN_JSON_KEY",
"GCR_JSON_KEY",
"GOOGLE_CREDENTIALS",
"TOKEN",
)
config.WorkloadIdentity = parseBoolOrDefault(false, getenv("PLUGIN_WORKLOAD_IDENTITY"))
config.Username, config.Password = setUsernameAndPassword(username, password, config.WorkloadIdentity)
}

location := getenv("PLUGIN_LOCATION")
repo := getenv("PLUGIN_REPO")

password := getenv(
"PLUGIN_JSON_KEY",
"GCR_JSON_KEY",
"GOOGLE_CREDENTIALS",
"TOKEN",
)
workloadIdentity := parseBoolOrDefault(false, getenv("PLUGIN_WORKLOAD_IDENTITY"))
username, password = setUsernameAndPassword(username, password, workloadIdentity)

registryType := getenv("PLUGIN_REGISTRY_TYPE")
if registryType == "" {
registryType = "GCR"
Expand All @@ -73,24 +105,23 @@ func loadConfig() Config {
if !strings.HasPrefix(repo, registry) {
repo = path.Join(registry, repo)
}

return Config{
Repo: repo,
Registry: registry,
Password: password,
WorkloadIdentity: workloadIdentity,
Username: username,
RegistryType: registryType,
}
config.Repo = repo
config.Registry = registry
config.RegistryType = registryType
return config
}

func main() {
config := loadConfig()
if config.AccessToken != "" {
os.Setenv("ACCESS_TOKEN", config.AccessToken)
} else if config.Username != "" && config.Password != "" {
os.Setenv("DOCKER_USERNAME", config.Username)
os.Setenv("DOCKER_PASSWORD", config.Password)
}

os.Setenv("PLUGIN_REPO", config.Repo)
os.Setenv("PLUGIN_REGISTRY", config.Registry)
os.Setenv("DOCKER_USERNAME", config.Username)
os.Setenv("DOCKER_PASSWORD", config.Password)
os.Setenv("PLUGIN_REGISTRY_TYPE", config.RegistryType)

// invoke the base docker plugin binary
Expand Down Expand Up @@ -152,3 +183,49 @@ func getenv(key ...string) (s string) {
}
return
}

func getFederalToken(idToken, projectNumber, poolId, providerId string) (string, error) {
ctx := context.Background()
stsService, err := sts.NewService(ctx, option.WithoutAuthentication())
if err != nil {
return "", err
}
audience := fmt.Sprintf("//iam.googleapis.com/projects/%s/locations/global/workloadIdentityPools/%s/providers/%s", projectNumber, poolId, providerId)
tokenRequest := &sts.GoogleIdentityStsV1ExchangeTokenRequest{
GrantType: "urn:ietf:params:oauth:grant-type:token-exchange",
SubjectToken: idToken,
Audience: audience,
Scope: "https://www.googleapis.com/auth/cloud-platform",
RequestedTokenType: "urn:ietf:params:oauth:token-type:access_token",
SubjectTokenType: "urn:ietf:params:oauth:token-type:id_token",
}
tokenResponse, err := stsService.V1.Token(tokenRequest).Do()
if err != nil {
return "", err
}

return tokenResponse.AccessToken, nil
}

func getGoogleCloudAccessToken(federatedToken string, serviceAccountEmail string) (string, error) {
ctx := context.Background()
tokenSource := &staticTokenSource{
token: &oauth2.Token{AccessToken: federatedToken},
}
service, err := iamcredentials.NewService(ctx, option.WithTokenSource(tokenSource))
if err != nil {
return "", err
}

name := "projects/-/serviceAccounts/" + serviceAccountEmail
rb := &iamcredentials.GenerateAccessTokenRequest{
Scope: []string{"https://www.googleapis.com/auth/cloud-platform"},
}

resp, err := service.Projects.ServiceAccounts.GenerateAccessToken(name, rb).Do()
if err != nil {
return "", err
}

return resp.AccessToken, nil
}
38 changes: 31 additions & 7 deletions docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,12 @@ type (

// Login defines Docker login parameters.
Login struct {
Registry string // Docker registry address
Username string // Docker registry username
Password string // Docker registry password
Email string // Docker registry email
Config string // Docker Auth Config
Registry string // Docker registry address
Username string // Docker registry username
Password string // Docker registry password
Email string // Docker registry email
Config string // Docker Auth Config
AccessToken string // External Access Token
}

// Build defines Docker build parameters.
Expand Down Expand Up @@ -113,7 +114,6 @@ type (

// Exec executes the plugin step
func (p Plugin) Exec() error {

// start the Docker daemon server
if !p.Daemon.Disabled {
p.startDaemon()
Expand Down Expand Up @@ -143,6 +143,8 @@ func (p Plugin) Exec() error {
fmt.Println("Detected registry credentials")
case p.Login.Config != "":
fmt.Println("Detected registry credentials file")
case p.Login.AccessToken != "":
fmt.Println("Detected access token")
default:
fmt.Println("Registry credentials or Docker config not provided. Guest mode enabled.")
}
Expand All @@ -166,7 +168,18 @@ func (p Plugin) Exec() error {
out := string(raw)
out = strings.Replace(out, "WARNING! Using --password via the CLI is insecure. Use --password-stdin.", "", -1)
fmt.Println(out)
return fmt.Errorf("Error authenticating: exit status 1")
return fmt.Errorf("error authenticating: exit status 1")
}
} else if p.Login.AccessToken != "" {
cmd := commandLoginAccessToken(p.Login, p.Login.AccessToken)
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("error logging in to Docker registry: %s", err)
}
if strings.Contains(string(output), "Login Succeeded") {
fmt.Println("Login successful")
} else {
return fmt.Errorf("login did not succeed")
}
}

Expand Down Expand Up @@ -270,6 +283,17 @@ func commandLogin(login Login) *exec.Cmd {
)
}

func commandLoginAccessToken(login Login, accessToken string) *exec.Cmd {
cmd := exec.Command(dockerExe,
"login",
"-u",
"oauth2accesstoken",
"--password-stdin",
login.Registry)
cmd.Stdin = strings.NewReader(accessToken)
return cmd
}

// helper to check if args match "docker pull <image>"
func isCommandPull(args []string) bool {
return len(args) > 2 && args[1] == "pull"
Expand Down
26 changes: 19 additions & 7 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,31 @@ require (
github.com/joho/godotenv v1.3.0
github.com/sirupsen/logrus v1.9.0
github.com/urfave/cli v1.22.2
golang.org/x/oauth2 v0.8.0
golang.org/x/oauth2 v0.13.0
)

require (
cloud.google.com/go/compute/metadata v0.2.0 // indirect
cloud.google.com/go/compute v1.23.1 // indirect
cloud.google.com/go/compute/metadata v0.2.3 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/s2a-go v0.1.7 // indirect
github.com/google/uuid v1.3.1 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.1 // indirect
github.com/googleapis/gax-go/v2 v2.12.0 // indirect
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
golang.org/x/net v0.10.0 // indirect
golang.org/x/sys v0.8.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.28.0 // indirect
go.opencensus.io v0.24.0 // indirect
golang.org/x/crypto v0.14.0 // indirect
golang.org/x/net v0.17.0 // indirect
golang.org/x/sys v0.13.0 // indirect
golang.org/x/text v0.13.0 // indirect
google.golang.org/api v0.146.0 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20231016165738-49dd2c1f3d0b // indirect
google.golang.org/grpc v1.59.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/yaml.v2 v2.2.8 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
Expand Down
Loading

0 comments on commit 757a756

Please sign in to comment.