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 all 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
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,8 @@ registry:
clientsecret: "999999"
clientid: "000000"
tokenurl: http://myregistry.example.com:9096/token
gcp:
- registry: europe-docker.pkg.dev
```

## `~/.config/falcoctl/`
Expand Down Expand Up @@ -296,6 +298,19 @@ The `registry auth basic` command authenticates a user to a given OCI registry u
#### Falcoctl registry auth oauth
The `registry auth oauth` command retrieves access and refresh tokens for OAuth2.0 client credentials flow authentication. Run the command in advance for any private registries.

#### Falcoctl registry auth gcp
The `registry auth gcp` command retrieves access tokens using [Application Default Credentials](https://cloud.google.com/docs/authentication/application-default-credentials). In particular, it supports access token retrieval using Google Compute Engine metadata server and Workload Identity, useful to authenticate your deployed Falco workloads. Run the command in advance for Artifact Registry authentication.

Two typical use cases:

1. You are manipulating some rules or plugins and use `falcoctl` to pull or push to an Artifact Registry:
1. run `gcloud auth application-default login` to generate a JSON credential file that will be used by applications.
2. run `falcoctl registry auth gcp europe-docker.pkg.dev` for instance to use Application Default Credentials to connect to any repository hosted at `europe-docker.pkg.dev`.
2. You have a Falco instance with Falcoctl as a side car, running in a GKE cluster with Workload Identity enabled:
1. Workload Identity is correctly set up for the Falco instance (see the [documentation](https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity)).
2. Add an environment variable like `FALCOCTL_REGISTRY_AUTH_GCP=europe-docker.pkg.dev` to enable GCP authentication for the `europe-docker.pkg.dev` registry.
3. The Falcoctl instance will get access tokens from the metadata server and use them to authenticate to the registry and download your rules.

### Falcoctl registry push
It pushes local files and references the artifact uniquely. The following command shows how to push a local file to a remote registry:
```bash
Expand Down Expand Up @@ -329,6 +344,7 @@ This is the list of the environment variable that `falcoctl` will use:
| ------ | ---------- |
| `FALCOCTL_REGISTRY_AUTH_BASIC` | `registry,username,password;registry1,username1,password1` |
| `FALCOCTL_REGISTRY_AUTH_OAUTH` | `registry,client-id,client-secret,token-url;registry1` |
| `FALCOCTL_REGISTRY_AUTH_GCP` | `registry;registry1` |
| `FALCOCTL_INDEXES` | `index-name,https://falcosecurity.github.io/falcoctl/index.yaml` |
| `FALCOCTL_ARTIFACT_FOLLOW_EVERY` | `6h0m0s` |
| `FALCOCTL_ARTIFACT_FOLLOW_CRON` | `cron-formatted-string` |
Expand Down
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
84 changes: 84 additions & 0 deletions cmd/registry/auth/gcp/gcp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// 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 an Artifact Registry to use GCP Application Default credentials to connect to it.

In particular, it can use Workload Identity or GCE metadata server to authenticate.

Example
falcoctl registry auth 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 Artifact Registry to log in using GCP Application Default 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
}
Loading