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

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

Merged
merged 7 commits into from
Jul 6, 2023
Merged
Show file tree
Hide file tree
Changes from 2 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/gke"
"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(gke.NewGkeCmd(ctx, opt))

return cmd
}
16 changes: 16 additions & 0 deletions cmd/registry/auth/gke/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 oauth defines the logic to authenticate against an OCI registry via OAuth2.0.
package gke
83 changes: 83 additions & 0 deletions cmd/registry/auth/gke/gke.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// 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 gke

import (
"context"
"fmt"

"github.com/spf13/cobra"

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

const (
longGke = `Register a registry to use Workload Identity to connect to it.

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

// RegistryGkeOptions contains the options for the registry gke command.
type RegistryGkeOptions struct {
*options.CommonOptions
registry string
}

// NewGkeCmd returns the gke command.
func NewGkeCmd(ctx context.Context, opt *options.CommonOptions) *cobra.Command {
o := RegistryGkeOptions{
CommonOptions: opt,
}

cmd := &cobra.Command{
Use: "gke [REGISTRY]",
DisableFlagsInUseLine: true,
Short: "Register an OCI registry to log in using Workload identity",
Long: longGke,
Args: cobra.ExactArgs(1),
SilenceErrors: true,
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
return o.RunGke(ctx, args)
},
}

return cmd
}

// RunGke executes the business logic for the gke command.
func (o *RegistryGkeOptions) RunGke(ctx context.Context, args []string) error {
var err error
reg := args[0]
if err = gke.Login(ctx, reg); err != nil {
return err
}
o.Printer.Success.Printfln("GKE source correctly set for %q", o.registry)

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

o.Printer.Success.Printfln("Gke auth entry for %q successfully added", reg)

return nil
}
2 changes: 2 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
5 changes: 5 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,17 @@ cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHOb
cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY=
cloud.google.com/go v0.110.0 h1:Zc8gqp3+a9/Eyph2KDmcGaPtbKRIoqq4YTlL4NMD0Ys=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
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
89 changes: 89 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"
// RegistryAuthGkeKey is the Viper key for gke authentication configuration.
RegistryAuthGkeKey = "registry.auth.gke"
// 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"`
}

// GkeAuth represents a Gke credential.
type GkeAuth 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
}

// Gkes retrieves the indexes section of the config file.
func Gkes() ([]GkeAuth, error) {
var auths []GkeAuth

if err := viper.UnmarshalKey(RegistryAuthGkeKey, &auths, viper.DecodeHook(gkeAuthListHookFunc())); err != nil {
return nil, fmt.Errorf("unable to get gkeAuths: %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,46 @@ 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 gkeAuthListHookFunc() mapstructure.DecodeHookFuncType {
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([]GkeAuth{}) {
return data, fmt.Errorf("unable to decode data since destination variable is not of type %T", []GkeAuth{})
}

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([]GkeAuth, len(tokens))
for i, token := range tokens {

auths[i] = GkeAuth{
Registry: token,
}
}
return auths, nil
case reflect.Slice:
var auths []GkeAuth
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 +608,34 @@ func findIndexInSlice(slice []Index, val *Index) (int, bool) {
}
return -1, false
}

// AddGke appends the provided gkes to a configuration file if not present.
func AddGke(gkes []GkeAuth, configFile string) error {
var currGkes []GkeAuth
var err error

// Retrieve the current gkes from configuration.
if currGkes, err = Gkes(); err != nil {
return err
}
for i, gke := range gkes {
if _, ok := findGkeInSlice(currGkes, &gkes[i]); !ok {
currGkes = append(currGkes, gke)
}
}

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

return nil
}

func findGkeInSlice(slice []GkeAuth, val *GkeAuth) (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/gke/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 oauth implements oauth client credentials login functionality.
package gke
53 changes: 53 additions & 0 deletions internal/login/gke/gke.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// 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 gke

import (
"context"
"fmt"

"github.com/falcosecurity/falcoctl/pkg/oci/registry"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
)

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

// Check that we can retrieve token using ApplicationDefaultCredentials logic.
_, err = ts.Token()
if err != nil {
return fmt.Errorf("wrong gke 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/gke"
"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 gke auth.
gkeAuths, err := config.Gkes()
if err != nil {
return err
}

if err := PerformGkeAuthsLogin(ctx, gkeAuths, 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
}

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

return nil
}
8 changes: 8 additions & 0 deletions pkg/oci/authn/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,14 @@ func WithOAuthCredentials() func(c *Options) {
}
}

// WithGkeCredentials adds the gke source to the client.
func WithGkeCredentials() func(c *Options) {
return func(c *Options) {
gkeStore := NewGkeClientCredentialsStore()
c.CredentialsFuncs = append(c.CredentialsFuncs, gkeStore.Credential)
}
}

// WithCredentials adds a static credential function to the client.
func WithCredentials(cred *auth.Credential) func(c *Options) {
return func(c *Options) {
Expand Down
Loading