diff --git a/integrations/kube-agent-updater/cmd/teleport-kube-agent-updater/main.go b/integrations/kube-agent-updater/cmd/teleport-kube-agent-updater/main.go index a6ad8b3f540e7..30e2f485cffc0 100644 --- a/integrations/kube-agent-updater/cmd/teleport-kube-agent-updater/main.go +++ b/integrations/kube-agent-updater/cmd/teleport-kube-agent-updater/main.go @@ -64,6 +64,7 @@ func main() { var versionServer string var versionChannel string var insecureNoVerify bool + var insecureNoResolve bool var disableLeaderElection bool flag.StringVar(&agentName, "agent-name", "", "The name of the agent that should be updated. This is mandatory.") @@ -71,7 +72,8 @@ func main() { flag.StringVar(&metricsAddr, "metrics-addr", ":8080", "The address the metric endpoint binds to.") flag.StringVar(&probeAddr, "healthz-addr", ":8081", "The address the probe endpoint binds to.") flag.DurationVar(&syncPeriod, "sync-period", 10*time.Hour, "Operator sync period (format: https://pkg.go.dev/time#ParseDuration)") - flag.BoolVar(&insecureNoVerify, "insecure-no-verify-image", false, "Disable image signature verification.") + flag.BoolVar(&insecureNoVerify, "insecure-no-verify-image", false, "Disable image signature verification. The image tag is still resolved and image must exist.") + flag.BoolVar(&insecureNoResolve, "insecure-no-resolve-image", false, "Disable image signature verification AND resolution. The updater can update to non-existing images.") flag.BoolVar(&disableLeaderElection, "disable-leader-election", false, "Disable leader election, used when running the kube-agent-updater outside of Kubernetes.") flag.StringVar(&versionServer, "version-server", "https://updates.releases.teleport.dev/v1/", "URL of the HTTP server advertising target version and critical maintenances. Trailing slash is optional.") flag.StringVar(&versionChannel, "version-channel", "cloud/stable", "Version channel to get updates from.") @@ -130,10 +132,14 @@ func main() { } var imageValidators img.Validators - if insecureNoVerify { + switch { + case insecureNoResolve: + ctrl.Log.Info("INSECURE: Image validation and resolution disabled") + imageValidators = append(imageValidators, img.NewNopValidator("insecure no resolution")) + case insecureNoVerify: ctrl.Log.Info("INSECURE: Image validation disabled") - imageValidators = append(imageValidators, img.NewInsecureValidator("insecure always verify")) - } else { + imageValidators = append(imageValidators, img.NewInsecureValidator("insecure always verified")) + default: validator, err := img.NewCosignSingleKeyValidator(teleportProdOCIPubKey, "cosign signature validator") if err != nil { ctrl.Log.Error(err, "failed to build image validator, exiting") diff --git a/integrations/kube-agent-updater/pkg/img/insecure.go b/integrations/kube-agent-updater/pkg/img/insecure.go index d8d435712e963..a02cc17e2473e 100644 --- a/integrations/kube-agent-updater/pkg/img/insecure.go +++ b/integrations/kube-agent-updater/pkg/img/insecure.go @@ -55,7 +55,8 @@ func (v *insecureValidator) ValidateAndResolveDigest(ctx context.Context, image } // NewInsecureValidator returns an img.Validator that only resolves the image -// but does not check its signature. +// but does not check its signature. This must not be confused with +// NewNopValidator that returns a validator that always validate without resolving. func NewInsecureValidator(name string) Validator { return &insecureValidator{ name: name, diff --git a/integrations/kube-agent-updater/pkg/img/nop.go b/integrations/kube-agent-updater/pkg/img/nop.go new file mode 100644 index 0000000000000..da95a364be91d --- /dev/null +++ b/integrations/kube-agent-updater/pkg/img/nop.go @@ -0,0 +1,112 @@ +/* + * Teleport + * Copyright (C) 2023 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package img + +import ( + "context" + + "github.com/distribution/reference" + "github.com/google/go-containerregistry/pkg/name" + "github.com/gravitational/trace" + "github.com/opencontainers/go-digest" +) + +type nopValidator struct { + name string +} + +// Name returns the validator name +func (v *nopValidator) Name() string { + return v.name +} + +// ValidateAndResolveDigest of the nopValidator does not resolve nor +// validate the image. It always says the image is valid, and returns it as-is. +// Using this validator makes you vulnerable in case of image +// registry compromise. +func (v *nopValidator) ValidateAndResolveDigest(_ context.Context, image reference.NamedTagged) (NamedTaggedDigested, error) { + ref, err := name.NewTag(image.String()) + if err != nil { + return nil, trace.Wrap(err) + } + + digestedImage := newUnresolvedImageRef(ref.RegistryStr(), ref.RepositoryStr(), image.Tag()) + return digestedImage, nil +} + +// NewNopValidator returns an img.Validator that only resolves the image +// but does not check its signature. +func NewNopValidator(name string) Validator { + return &nopValidator{ + name: name, + } +} + +// unresolvedImageRef is the insecure version of imageRef. It does not contain the +// digest, which means we cannot enforce that the image ran by Kubernetes is +// the same that was validated. +// This should only be used with the insecure validator. +type unresolvedImageRef struct { + repository struct { + domain string + path string + } + tag string +} + +// String returns the string representation of the image +func (i unresolvedImageRef) String() string { + return i.Name() + ":" + i.tag +} + +// Name returns the image Name (repo domain and image path) +func (i unresolvedImageRef) Name() string { + if i.repository.domain == "" { + return i.repository.path + } + return i.repository.domain + "/" + i.repository.path + +} + +// Tag returns the image tag +func (i unresolvedImageRef) Tag() string { + return i.tag +} + +// Digest returns nothing. It's here to implement the NamedTaggedDigested interface. +func (i unresolvedImageRef) Digest() digest.Digest { + return "" +} + +// newUnresolvedImageRef returns an image reference that both Named, Tagged but not +// Digested. This is insecure because we cannot enforce that the image ran by Kubernetes is +// the same that was validated. +// This should only be used by the nopValidator. +func newUnresolvedImageRef(domain, path, tag string) NamedTaggedDigested { + return unresolvedImageRef{ + repository: struct { + domain string + path string + }{ + domain: domain, + path: path, + }, + tag: tag, + } +} diff --git a/integrations/kube-agent-updater/pkg/img/nop_test.go b/integrations/kube-agent-updater/pkg/img/nop_test.go new file mode 100644 index 0000000000000..fa83a95977a94 --- /dev/null +++ b/integrations/kube-agent-updater/pkg/img/nop_test.go @@ -0,0 +1,46 @@ +/* + * Teleport + * Copyright (C) 2023 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package img + +import ( + "context" + "testing" + + "github.com/distribution/reference" + "github.com/stretchr/testify/require" +) + +func TestNewNopValidator(t *testing.T) { + // Test setup + v := NewNopValidator("test") + ctx := context.Background() + baseImageName := "registry.example.com/teleport-distroless" + baseImage, err := reference.ParseNamed(baseImageName) + require.NoError(t, err) + testVersionTag := "15.1.2" + + image, err := reference.WithTag(baseImage, testVersionTag) + require.NoError(t, err) + + // Test execution: we check that the image is not resolved, validated + // and that the output image is the same as the input one. + result, err := v.ValidateAndResolveDigest(ctx, image) + require.NoError(t, err) + require.Equal(t, image.String(), result.String()) +}