Skip to content

feat(cmd/auth) New command to support Workload Identity on GKE #288

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

Merged
merged 7 commits into from
Jul 6, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions cmd/registry/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"github.com/spf13/cobra"

"github.com/falcosecurity/falcoctl/cmd/registry/auth/basic"
"github.com/falcosecurity/falcoctl/cmd/registry/auth/gcp"
"github.com/falcosecurity/falcoctl/cmd/registry/auth/oauth"
commonoptions "github.com/falcosecurity/falcoctl/pkg/options"
)
Expand All @@ -36,6 +37,7 @@ func NewAuthCmd(ctx context.Context, opt *commonoptions.CommonOptions) *cobra.Co

cmd.AddCommand(basic.NewBasicCmd(ctx, opt))
cmd.AddCommand(oauth.NewOauthCmd(ctx, opt))
cmd.AddCommand(gcp.NewGcpCmd(ctx, opt))

return cmd
}
16 changes: 16 additions & 0 deletions cmd/registry/auth/gcp/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Copyright 2023 The Falco Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package gcp defines the logic to authenticate against an Artifact registry using GCP credentials.
package gcp
82 changes: 82 additions & 0 deletions cmd/registry/auth/gcp/gcp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Copyright 2023 The Falco Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package gcp

import (
"context"
"fmt"

"github.com/spf13/cobra"

"github.com/falcosecurity/falcoctl/internal/config"
"github.com/falcosecurity/falcoctl/internal/login/gcp"
"github.com/falcosecurity/falcoctl/pkg/options"
)

const (
longGcp = `Register a registry to use GCE Metadata server or gcloud Application Default credentials to connect to it.

Example
falcoctl registry gcp europe-docker.pkg.dev
`
)

// RegistryGcpOptions contains the options for the registry gcp command.
type RegistryGcpOptions struct {
*options.CommonOptions
}

// NewGcpCmd returns the gcp command.
func NewGcpCmd(ctx context.Context, opt *options.CommonOptions) *cobra.Command {
Copy link
Member

Choose a reason for hiding this comment

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

We need to add the new command to the README.md: https://github.com/falcosecurity/falcoctl#falcoctl-registry.
Feel free to add examples too.

o := RegistryGcpOptions{
CommonOptions: opt,
}

cmd := &cobra.Command{
Use: "gcp [REGISTRY]",
DisableFlagsInUseLine: true,
Short: "Register an OCI registry to log in using GCP common credentials",
Long: longGcp,
Args: cobra.ExactArgs(1),
SilenceErrors: true,
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
return o.RunGcp(ctx, args)
},
}

return cmd
}

// RunGcp executes the business logic for the gcp command.
func (o *RegistryGcpOptions) RunGcp(ctx context.Context, args []string) error {
var err error
reg := args[0]
if err = gcp.Login(ctx, reg); err != nil {
return err
}
o.Printer.Success.Printfln("GCP authentication successful for %q", reg)

o.Printer.Verbosef("Adding new gcp entry to configuration file %q", o.ConfigFile)
if err = config.AddGcp([]config.GcpAuth{{
Registry: reg,
}}, o.ConfigFile); err != nil {
return fmt.Errorf("index entry %q: %w", reg, err)
}

o.Printer.Success.Printfln("GCP authentication entry for %q successfully added in configuration file", reg)

return nil
}
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ require (
atomicgo.dev/cursor v0.1.1 // indirect
atomicgo.dev/keyboard v0.2.9 // indirect
atomicgo.dev/schedule v0.0.2 // indirect
cloud.google.com/go/compute v1.19.0 // indirect
cloud.google.com/go/compute/metadata v0.2.3 // indirect
github.com/Shopify/logrus-bugsnag v0.0.0-20230117174420-439a4b8ba167 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bshuster-repo/logrus-logstash-hook v1.1.0 // indirect
Expand Down Expand Up @@ -96,6 +98,7 @@ require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/mattn/go-isatty v0.0.19
github.com/stretchr/testify v1.8.4
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1
golang.org/x/net v0.10.0 // indirect
golang.org/x/oauth2 v0.8.0
golang.org/x/sys v0.8.0 // indirect
Expand Down
7 changes: 6 additions & 1 deletion go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvf
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/compute v1.19.0 h1:+9zda3WGgW1ZSTlVppLCYFIr48Pa35q1uG2N1itbCEQ=
cloud.google.com/go/compute v1.19.0/go.mod h1:rikpw2y+UMidAe9tISo04EHNOIf42RLYF/q8Bs93scU=
cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
Expand Down Expand Up @@ -388,7 +392,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E=
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
Expand Down
88 changes: 88 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ const (
RegistryAuthOauthKey = "registry.auth.oauth"
// RegistryAuthBasicKey is the Viper key for basic authentication configuration.
RegistryAuthBasicKey = "registry.auth.basic"
// RegistryAuthGcpKey is the Viper key for gcp authentication configuration.
RegistryAuthGcpKey = "registry.auth.gcp"
// IndexesKey is the Viper key for indexes configuration.
IndexesKey = "indexes"
// ArtifactFollowEveryKey is the Viper key for follower "every" configuration.
Expand Down Expand Up @@ -127,6 +129,11 @@ type BasicAuth struct {
Password string `mapstructure:"password"`
}

// GcpAuth represents a Gcp activation setting.
type GcpAuth struct {
Registry string `mapstructure:"registry"`
}

// Follow represents the follower configuration.
type Follow struct {
Every time.Duration `mapstructure:"every"`
Expand Down Expand Up @@ -216,6 +223,17 @@ func Indexes() ([]Index, error) {
return indexes, nil
}

// Gcps retrieves the gcp auth section of the config file.
func Gcps() ([]GcpAuth, error) {
var auths []GcpAuth

if err := viper.UnmarshalKey(RegistryAuthGcpKey, &auths, viper.DecodeHook(gcpAuthListHookFunc())); err != nil {
return nil, fmt.Errorf("unable to get gcpAuths: %w", err)
}

return auths, nil
}

// indexListHookFunc returns a DecodeHookFunc that converts
// strings to string slices, when the target type is DotSeparatedStringList.
// when passed as env should be in the following format:
Expand Down Expand Up @@ -393,6 +411,45 @@ func oathAuthListHookFunc() mapstructure.DecodeHookFuncType {
}
}

// oauthAuthListHookFunc returns a DecodeHookFunc that converts
// strings to string slices, when the target type is DotSeparatedStringList.
// when passed as env should be in the following format:
// "registry;registry1".
func gcpAuthListHookFunc() mapstructure.DecodeHookFuncType {
Copy link
Member

Choose a reason for hiding this comment

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

We need to add the new ENV variable to the README.md: https://github.com/falcosecurity/falcoctl#falcoctl-environment-variables.

return func(f reflect.Type, t reflect.Type, data interface{}) (interface{}, error) {
if f.Kind() != reflect.String && f.Kind() != reflect.Slice {
return data, nil
}

if t != reflect.TypeOf([]GcpAuth{}) {
return data, fmt.Errorf("unable to decode data since destination variable is not of type %T", []GcpAuth{})
}

switch f.Kind() {
case reflect.String:
if !SemicolonSeparatedRegexp.MatchString(data.(string)) {
return data, fmt.Errorf("env variable not correctly set, should match %q, got %q", SemicolonSeparatedRegexp.String(), data.(string))
}
tokens := strings.Split(data.(string), ";")
auths := make([]GcpAuth, len(tokens))
for i, token := range tokens {
auths[i] = GcpAuth{
Registry: token,
}
}
return auths, nil
case reflect.Slice:
var auths []GcpAuth
if err := mapstructure.WeakDecode(data, &auths); err != nil {
return err, nil
}
return auths, nil
default:
return nil, nil
}
}
}

// Follower retrieves the follower section of the config file.
func Follower() (Follow, error) {
// with Follow we can just use nested keys.
Expand Down Expand Up @@ -550,3 +607,34 @@ func findIndexInSlice(slice []Index, val *Index) (int, bool) {
}
return -1, false
}

// AddGcp appends the provided gcps to a configuration file if not present.
func AddGcp(gcps []GcpAuth, configFile string) error {
var currGcps []GcpAuth
var err error

// Retrieve the current gcps from configuration.
if currGcps, err = Gcps(); err != nil {
return err
}
for i, gcp := range gcps {
if _, ok := findGcpInSlice(currGcps, &gcps[i]); !ok {
currGcps = append(currGcps, gcp)
}
}

if err := UpdateConfigFile(RegistryAuthGcpKey, currGcps, configFile); err != nil {
return fmt.Errorf("unable to update gcps list in the config file %q: %w", configFile, err)
}

return nil
}

func findGcpInSlice(slice []GcpAuth, val *GcpAuth) (int, bool) {
for i, item := range slice {
if item.Registry == val.Registry {
return i, true
}
}
return -1, false
}
16 changes: 16 additions & 0 deletions internal/login/gcp/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Copyright 2023 The Falco Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package gcp implements gcp credentials login functionality.
package gcp
54 changes: 54 additions & 0 deletions internal/login/gcp/gcp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Copyright 2023 The Falco Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package gcp

import (
"context"
"fmt"

"golang.org/x/oauth2"
"golang.org/x/oauth2/google"

"github.com/falcosecurity/falcoctl/pkg/oci/registry"
)

// Login checks if passed gcp credentials are correct.
func Login(ctx context.Context, reg string) error {
// Check that we can find a valid token source using GCE or ApplicationDefault.
ts, err := google.DefaultTokenSource(ctx)
if err != nil {
return fmt.Errorf("wrong GCP token source, unable to find a valid source: %w", err)
}

// Check that we can retrieve token.
_, err = ts.Token()
if err != nil {
return fmt.Errorf("wrong GCP credentials, unable to retrieve token: %w", err)
}

// Check connection to the registry
client := oauth2.NewClient(ctx, ts)

r, err := registry.NewRegistry(reg, registry.WithClient(client))
if err != nil {
return err
}

if err := r.CheckConnection(ctx); err != nil {
return fmt.Errorf("unable to connect to registry %q: %w", reg, err)
}

return nil
}
26 changes: 26 additions & 0 deletions internal/login/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (

"github.com/falcosecurity/falcoctl/internal/config"
"github.com/falcosecurity/falcoctl/internal/login/basic"
"github.com/falcosecurity/falcoctl/internal/login/gcp"
"github.com/falcosecurity/falcoctl/internal/login/oauth"
)

Expand Down Expand Up @@ -51,6 +52,16 @@ func PerformAuthsFromConfigWithMap(ctx context.Context, client *auth.Client, cre
}
}

// Perform authentications using gcp auth.
gcpAuths, err := config.Gcps()
if err != nil {
return err
}

if err := PerformGcpAuthsLogin(ctx, gcpAuths, registrySet); err != nil {
return err
}

// Perform authentications using oauth auth.
oauthAuths, err := config.OauthAuths()
if err != nil {
Expand Down Expand Up @@ -92,3 +103,18 @@ func PerformOauthAuths(ctx context.Context, auths []config.OauthAuth, registrySe

return nil
}

// PerformGcpAuthsLogin logins to the registries.
func PerformGcpAuthsLogin(
ctx context.Context, auths []config.GcpAuth, registrySet map[string]bool,
) error {
for _, gcpAuth := range auths {
if _, exists := registrySet[gcpAuth.Registry]; exists {
if err := gcp.Login(ctx, gcpAuth.Registry); err != nil {
return err
}
}
}

return nil
}
Loading