Improper path handling in kustomization files allows path traversal
Description
Flux is an open and extensible continuous delivery solution for Kubernetes. Path Traversal in the kustomize-controller via a malicious kustomization.yaml allows an attacker to expose sensitive data from the controller’s pod filesystem and possibly privilege escalation in multi-tenancy deployments. Workarounds include automated tooling in the user's CI/CD pipeline to validate kustomization.yaml files conform with specific policies. This vulnerability is fixed in kustomize-controller v0.24.0 and included in flux2 v0.29.0.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Path traversal in Flux kustomize-controller via malicious kustomization.yaml allows file disclosure and potential privilege escalation in multi-tenancy Kubernetes clusters.
Vulnerability
A path traversal vulnerability exists in the Flux kustomize-controller versions prior to v0.24.0 (and hence in flux2 prior to v0.29.0). By crafting a malicious kustomization.yaml, an attacker can leverage Kustomize's built-in file system operations to read files outside the intended working directory. The insecure path handling permitted references to arbitrary paths on the controller's pod filesystem, bypassing the intended sandbox constraints [1], [2], [4].
Exploitation
An attacker needs write access to a Flux source (e.g., a Git repository or OCI artifact) that is reconciled by the kustomize-controller. By injecting a kustomization.yaml that uses Kustomize features like patches, configMapGenerator, or secretGenerator with file paths such as ../../some/sensitive/file, the controller will read and process those files. In multi-tenancy environments, any user who can push changes to a Flux source can craft this attack without further authentication [4].
Impact
Successful exploitation allows an attacker to read arbitrary files from the kustomize-controller pod's filesystem, including secrets, configuration data, and service account tokens. If the controller's service account has elevated permissions (common in multi-tenancy setups), this can lead to privilege escalation within the cluster. The vulnerability does not directly allow remote code execution, but the information disclosure can be leveraged for further compromise [2], [4].
Mitigation
The vulnerability is fixed in kustomize-controller v0.24.0 and flux2 v0.29.0, released on 2022-04-20. The fix introduces a secure file system implementation (in the fluxcd/pkg/kustomize package) that validates all file paths are within the kustomization's working directory [1], [3]. Users unable to upgrade can employ automated tooling in their CI/CD pipeline to validate that kustomization.yaml files do not reference paths outside allowed directories, thereby blocking exploitation [2], [4].
AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/fluxcd/kustomize-controllerGo | < 0.24.0 | 0.24.0 |
github.com/fluxcd/flux2Go | < 0.29.0 | 0.29.0 |
Affected products
11- osv-coords10 versionspkg:apk/chainguard/flux-kustomize-controllerpkg:apk/chainguard/flux-kustomize-controller-bitnami-compatpkg:apk/chainguard/flux-kustomize-controller-iamguarded-compatpkg:apk/wolfi/flux-kustomize-controllerpkg:apk/wolfi/flux-kustomize-controller-bitnami-compatpkg:apk/wolfi/flux-kustomize-controller-iamguarded-compatpkg:bitnami/fluxpkg:bitnami/kustomizepkg:golang/github.com/fluxcd/flux2pkg:golang/github.com/fluxcd/kustomize-controller
< 0+ 9 more
- (no CPE)range: < 0
- (no CPE)range: < 0
- (no CPE)range: < 0
- (no CPE)range: < 0
- (no CPE)range: < 0
- (no CPE)range: < 0
- (no CPE)range: < 0.29.0
- (no CPE)range: < 0.24.0
- (no CPE)range: < 0.29.0
- (no CPE)range: < 0.24.0
- fluxcd/flux2v5Range: flux2 < v0.29.0
Patches
20ec014baf417kustomize: introduce secure FS implementation
2 files changed · +783 −0
kustomize/filesys/fs_secure.go+232 −0 added@@ -0,0 +1,232 @@ +/* +Copyright 2022 The Flux 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 filesys + +import ( + "fmt" + "os" + "path/filepath" + + "sigs.k8s.io/kustomize/kyaml/filesys" +) + +// MakeFsOnDiskSecure returns a secure file system which asserts any paths it +// handles to be inside root. +func MakeFsOnDiskSecure(root string) (filesys.FileSystem, error) { + unsafeFS := filesys.MakeFsOnDisk() + cleanedAbs, _, err := unsafeFS.CleanedAbs(root) + if err != nil { + return nil, err + } + return fsSecure{root: cleanedAbs, unsafeFS: unsafeFS}, nil +} + +// fsSecure wraps an unsafe FileSystem implementation, and secures it +// by confirming paths are inside root. +type fsSecure struct { + root filesys.ConfirmedDir + unsafeFS filesys.FileSystem +} + +// ConstraintError records an error and the operation and file that +// violated it. +type ConstraintError struct { + Op string + Path string + Err error +} + +func (e *ConstraintError) Error() string { + return "fs-security-constraint " + e.Op + " " + e.Path + ": " + e.Err.Error() +} + +func (e *ConstraintError) Unwrap() error { return e.Err } + +// Create delegates to the embedded unsafe FS after having confirmed the path +// to be inside root. If the provided path violates this constraint, an error +// of type ConstraintError is returned. +func (fs fsSecure) Create(path string) (filesys.File, error) { + if err := isSecurePath(fs.unsafeFS, fs.root, path); err != nil { + return nil, &ConstraintError{Op: "create", Path: path, Err: err} + } + return fs.unsafeFS.Create(path) +} + +// Mkdir delegates to the embedded unsafe FS after having confirmed the path +// to be inside root. If the provided path violates this constraint, an error +// of type ConstraintError is returned. +func (fs fsSecure) Mkdir(path string) error { + if err := isSecurePath(fs.unsafeFS, fs.root, path); err != nil { + return &ConstraintError{Op: "mkdir", Path: path, Err: err} + } + return fs.unsafeFS.Mkdir(path) +} + +// MkdirAll delegates to the embedded unsafe FS after having confirmed the path +// to be inside root. If the provided path violates this constraint, an error +// type ConstraintError is returned. +func (fs fsSecure) MkdirAll(path string) error { + if err := isSecurePath(fs.unsafeFS, fs.root, path); err != nil { + return &ConstraintError{Op: "mkdir", Path: path, Err: err} + } + return fs.unsafeFS.MkdirAll(path) +} + +// RemoveAll delegates to the embedded unsafe FS after having confirmed the +// path to be inside root. If the provided path violates this constraint, an +// error of type ConstraintError is returned. +func (fs fsSecure) RemoveAll(path string) error { + if err := isSecurePath(fs.unsafeFS, fs.root, path); err != nil { + return &ConstraintError{Op: "remove", Path: path, Err: err} + } + return fs.unsafeFS.RemoveAll(path) +} + +// Open delegates to the embedded unsafe FS after having confirmed the path +// to be inside root. If the provided path violates this constraint, an error +// of type ConstraintError is returned. +func (fs fsSecure) Open(path string) (filesys.File, error) { + if err := isSecurePath(fs.unsafeFS, fs.root, path); err != nil { + return nil, &ConstraintError{Op: "open", Path: path, Err: err} + } + return fs.unsafeFS.Open(path) +} + +// IsDir delegates to the embedded unsafe FS after having confirmed the path +// to be inside root. If the provided path violates this constraint, it returns +// false. +func (fs fsSecure) IsDir(path string) bool { + if err := isSecurePath(fs.unsafeFS, fs.root, path); err != nil { + return false + } + return fs.unsafeFS.IsDir(path) +} + +// ReadDir delegates to the embedded unsafe FS after having confirmed the path +// to be inside root. If the provided path violates this constraint, an error +// of type ConstraintError is returned. +func (fs fsSecure) ReadDir(path string) ([]string, error) { + if err := isSecurePath(fs.unsafeFS, fs.root, path); err != nil { + return nil, &ConstraintError{Op: "open", Path: path, Err: err} + } + return fs.unsafeFS.ReadDir(path) +} + +// CleanedAbs delegates to the embedded unsafe FS, but confirms the returned +// result to be within root. If the results violates this constraint, an error +// of type ConstraintError is returned. +// In essence, it functions the same as Kustomize's loader.RestrictionRootOnly, +// but on FS levels, and while allowing file paths. +func (fs fsSecure) CleanedAbs(path string) (filesys.ConfirmedDir, string, error) { + d, f, err := fs.unsafeFS.CleanedAbs(path) + if err != nil { + return d, f, err + } + if !d.HasPrefix(fs.root) { + return "", "", &ConstraintError{Op: "abs", Path: path, Err: rootConstraintErr(path, fs.root.String())} + } + return d, f, err +} + +// Exists delegates to the embedded unsafe FS after having confirmed the path +// to be inside root. If the provided path violates this constraint, it returns +// false. +func (fs fsSecure) Exists(path string) bool { + if err := isSecurePath(fs.unsafeFS, fs.root, path); err != nil { + return false + } + return fs.unsafeFS.Exists(path) +} + +// Glob delegates to the embedded unsafe FS, but filters the returned paths to +// only include items inside root. +func (fs fsSecure) Glob(pattern string) ([]string, error) { + paths, err := fs.unsafeFS.Glob(pattern) + if err != nil { + return nil, err + } + var securePaths []string + for _, p := range paths { + if err := isSecurePath(fs.unsafeFS, fs.root, p); err == nil { + securePaths = append(securePaths, p) + } + } + return securePaths, err +} + +// ReadFile delegates to the embedded unsafe FS after having confirmed the path +// to be inside root. If the provided path violates this constraint, an error +// of type ConstraintError is returned. +func (fs fsSecure) ReadFile(path string) ([]byte, error) { + if err := isSecurePath(fs.unsafeFS, fs.root, path); err != nil { + return nil, &ConstraintError{Op: "read", Path: path, Err: err} + } + return fs.unsafeFS.ReadFile(path) +} + +// WriteFile delegates to the embedded unsafe FS after having confirmed the +// path to be inside root. If the provided path violates this constraint, an +// error of type ConstraintError is returned. +func (fs fsSecure) WriteFile(path string, data []byte) error { + if err := isSecurePath(fs.unsafeFS, fs.root, path); err != nil { + return &ConstraintError{Op: "write", Path: path, Err: err} + } + return fs.unsafeFS.WriteFile(path, data) +} + +// Walk delegates to the embedded unsafe FS, wrapping falkFn in a callback which +// confirms the path to be inside root. If the path violates this constraint, +// an error of type ConstraintError is returned and walkFn is not called. +func (fs fsSecure) Walk(path string, walkFn filepath.WalkFunc) error { + wrapWalkFn := func(path string, info os.FileInfo, err error) error { + if err := isSecurePath(fs.unsafeFS, fs.root, path); err != nil { + return &ConstraintError{Op: "walk", Path: path, Err: err} + } + return walkFn(path, info, err) + } + return fs.unsafeFS.Walk(path, wrapWalkFn) +} + +// isSecurePath confirms the given path is inside root using the provided file +// system. At present, it assumes the file system implementation to be on disk +// and makes use of filepath.EvalSymlinks. +func isSecurePath(fs filesys.FileSystem, root filesys.ConfirmedDir, path string) error { + absRoot, err := filepath.Abs(path) + if err != nil { + return fmt.Errorf("abs path error on '%s': %v", path, err) + } + d := filesys.ConfirmedDir(filepath.Dir(absRoot)) + if fs.Exists(absRoot) { + evaluated, err := filepath.EvalSymlinks(absRoot) + if err != nil { + return fmt.Errorf("evalsymlink failure on '%s': %w", path, err) + } + evaluatedDir := evaluated + if !fs.IsDir(evaluatedDir) { + evaluatedDir = filepath.Dir(evaluatedDir) + } + d = filesys.ConfirmedDir(evaluatedDir) + } + if !d.HasPrefix(root) { + return rootConstraintErr(path, root.String()) + } + return nil +} + +func rootConstraintErr(path, root string) error { + return fmt.Errorf("path '%s' is not in or below '%s'", path, root) +}
kustomize/filesys/fs_secure_test.go+551 −0 added@@ -0,0 +1,551 @@ +/* +Copyright 2022 The Flux 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 filesys + +import ( + "bytes" + "io" + "math/rand" + "os" + "path/filepath" + "strings" + "testing" + + . "github.com/onsi/gomega" + "github.com/onsi/gomega/types" + "sigs.k8s.io/kustomize/kyaml/filesys" +) + +func Test_fsSecure_Create(t *testing.T) { + g := NewWithT(t) + + root := t.TempDir() + fs, err := MakeFsOnDiskSecure(root) + g.Expect(err).ToNot(HaveOccurred()) + + t.Run("secure create", func(t *testing.T) { + g := NewWithT(t) + + path := filepath.Join(root, "file.txt") + got, err := fs.Create(path) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(got).ToNot(BeNil()) + g.Expect(got.Close()).To(Succeed()) + g.Expect(fs.Exists(path)).To(BeTrue()) + }) + + t.Run("illegal create", func(t *testing.T) { + g := NewWithT(t) + + path := filepath.Join(root, "../file.txt") + got, err := fs.Create("/file.txt") + g.Expect(err).To(HaveOccurred()) + g.Expect(got).To(BeNil()) + g.Expect(fs.Exists(path)).To(BeFalse()) + }) +} + +func Test_fsSecure_Mkdir(t *testing.T) { + g := NewWithT(t) + + root := t.TempDir() + fs, err := MakeFsOnDiskSecure(root) + g.Expect(err).ToNot(HaveOccurred()) + + t.Run("secure mkdir", func(t *testing.T) { + g := NewWithT(t) + + path := filepath.Join(root, "secure") + g.Expect(fs.Mkdir(path)).To(Succeed()) + g.Expect(path).To(BeADirectory()) + }) + + t.Run("illegal mkdir", func(t *testing.T) { + g := NewWithT(t) + + path := filepath.Join(os.TempDir(), "illegal") + g.Expect(fs.Mkdir(path)).To(HaveOccurred()) + g.Expect(path).ToNot(BeADirectory()) + g.Expect(path).ToNot(BeAnExistingFile()) + }) +} + +func Test_fsSecure_MkdirAll(t *testing.T) { + g := NewWithT(t) + + root := t.TempDir() + fs, err := MakeFsOnDiskSecure(root) + g.Expect(err).ToNot(HaveOccurred()) + + t.Run("secure mkdir all", func(t *testing.T) { + g := NewWithT(t) + + path := filepath.Join(root, "secure", "subdir") + g.Expect(fs.MkdirAll(path)).To(Succeed()) + g.Expect(path).To(BeADirectory()) + }) + + t.Run("illegal mkdir all", func(t *testing.T) { + g := NewWithT(t) + + path := filepath.Join(root, "..", "..", "subdir") + g.Expect(fs.MkdirAll(path)).To(HaveOccurred()) + g.Expect(path).ToNot(BeADirectory()) + g.Expect(path).ToNot(BeAnExistingFile()) + }) +} + +func Test_fsSecure_RemoveAll(t *testing.T) { + g := NewWithT(t) + + tmpDir := t.TempDir() + root := filepath.Join(tmpDir, "workdir") + + g.Expect(os.MkdirAll(filepath.Join(root, "subdir"), 0o700)).To(Succeed()) + g.Expect(os.WriteFile(filepath.Join(root, "subdir", "file.txt"), []byte(""), 0o644)).To(Succeed()) + g.Expect(os.WriteFile(filepath.Join(tmpDir, "file.txt"), []byte(""), 0o644)).To(Succeed()) + + fs, err := MakeFsOnDiskSecure(root) + g.Expect(err).ToNot(HaveOccurred()) + + t.Run("secure remove all", func(t *testing.T) { + g := NewWithT(t) + + path := filepath.Join(root, "subdir") + g.Expect(fs.RemoveAll(path)).To(Succeed()) + g.Expect(path).NotTo(BeADirectory()) + }) + + t.Run("illegal remove all", func(t *testing.T) { + g := NewWithT(t) + + path := filepath.Join(tmpDir, "file.txt") + g.Expect(fs.RemoveAll(path)).To(HaveOccurred()) + g.Expect(path).To(BeAnExistingFile()) + }) +} + +func Test_fsSecure_Open(t *testing.T) { + g := NewWithT(t) + + tmpDir := t.TempDir() + + root := filepath.Join(tmpDir, "workdir") + g.Expect(os.Mkdir(root, 0o700)).To(Succeed()) + g.Expect(os.WriteFile(filepath.Join(root, "file.txt"), []byte("secure"), 0o644)).To(Succeed()) + g.Expect(os.WriteFile(filepath.Join(tmpDir, "file.txt"), []byte("illegal"), 0o644)).To(Succeed()) + + fs, err := MakeFsOnDiskSecure(root) + g.Expect(err).ToNot(HaveOccurred()) + + t.Run("secure open", func(t *testing.T) { + g := NewWithT(t) + + path := filepath.Join(root, "file.txt") + f, err := fs.Open(path) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(f).ToNot(BeNil()) + var b bytes.Buffer + _, err = io.Copy(&b, f) + g.Expect(err).To(Succeed()) + g.Expect(b.String()).To(Equal("secure")) + }) + + t.Run("illegal open", func(t *testing.T) { + g := NewWithT(t) + + path := filepath.Join(tmpDir, "file.txt") + f, err := fs.Open(path) + g.Expect(err).To(HaveOccurred()) + g.Expect(f).To(BeNil()) + }) +} + +func Test_fsSecure_IsDir(t *testing.T) { + g := NewWithT(t) + + tmpDir := t.TempDir() + + root := filepath.Join(tmpDir, "workdir") + g.Expect(os.Mkdir(root, 0o700)).To(Succeed()) + g.Expect(os.Mkdir(filepath.Join(tmpDir, "illegal"), 0o700)).To(Succeed()) + + fs, err := MakeFsOnDiskSecure(root) + g.Expect(err).ToNot(HaveOccurred()) + + t.Run("secure is dir", func(t *testing.T) { + g := NewWithT(t) + + path := filepath.Join(root, "") + g.Expect(fs.IsDir(path)).To(BeTrue()) + }) + + t.Run("illegal is dir", func(t *testing.T) { + g := NewWithT(t) + + path := filepath.Join(tmpDir, "illegal") + g.Expect(fs.IsDir(path)).To(BeFalse()) + }) +} + +func Test_fsSecure_ReadDir(t *testing.T) { + g := NewWithT(t) + + tmpDir := t.TempDir() + + root := filepath.Join(tmpDir, "workdir") + g.Expect(os.Mkdir(root, 0o700)).To(Succeed()) + g.Expect(os.WriteFile(filepath.Join(root, "file.txt"), []byte("secure"), 0o644)).To(Succeed()) + g.Expect(os.Mkdir(filepath.Join(tmpDir, "illegal"), 0o700)).To(Succeed()) + g.Expect(os.WriteFile(filepath.Join(tmpDir, "illegal", "file.txt"), []byte("illegal"), 0o644)).To(Succeed()) + + fs, err := MakeFsOnDiskSecure(root) + g.Expect(err).ToNot(HaveOccurred()) + + t.Run("secure read dir", func(t *testing.T) { + g := NewWithT(t) + + path := filepath.Join(root, "") + files, err := fs.ReadDir(path) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(files).To(HaveLen(1)) + g.Expect(files).To(ContainElement("file.txt")) + }) + + t.Run("illegal is dir", func(t *testing.T) { + g := NewWithT(t) + + path := filepath.Join(tmpDir, "illegal") + files, err := fs.ReadDir(path) + g.Expect(err).To(HaveOccurred()) + g.Expect(files).To(HaveLen(0)) + }) +} + +func Test_fsSecure_CleanedAbs(t *testing.T) { + g := NewWithT(t) + + tmpDir := t.TempDir() + + root := filepath.Join(tmpDir, "workdir") + g.Expect(os.Mkdir(root, 0o700)).To(Succeed()) + + fs, err := MakeFsOnDiskSecure(root) + g.Expect(err).ToNot(HaveOccurred()) + + t.Run("secure cleaned abs", func(t *testing.T) { + g := NewWithT(t) + + d, f, err := fs.CleanedAbs(filepath.Join(root, "../workdir")) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(d).To(Equal(filesys.ConfirmedDir(root))) + g.Expect(f).To(BeEmpty()) + }) + + t.Run("illegal cleaned abs", func(t *testing.T) { + g := NewWithT(t) + + d, f, err := fs.CleanedAbs(filepath.Join(root, "../../workdir")) + g.Expect(err).To(HaveOccurred()) + g.Expect(d).To(BeEmpty()) + g.Expect(f).To(BeEmpty()) + }) +} + +func Test_fsSecure_Exists(t *testing.T) { + g := NewWithT(t) + + tmpDir := t.TempDir() + + root := filepath.Join(tmpDir, "workdir") + g.Expect(os.Mkdir(root, 0o700)).To(Succeed()) + + fs, err := MakeFsOnDiskSecure(root) + g.Expect(err).ToNot(HaveOccurred()) + + t.Run("secure exists", func(t *testing.T) { + g := NewWithT(t) + + g.Expect(fs.Exists(root)).To(BeTrue()) + }) + + t.Run("illegal exists", func(t *testing.T) { + g := NewWithT(t) + + g.Expect(fs.Exists(tmpDir)).To(BeFalse()) + }) +} + +func Test_fsSecure_Glob(t *testing.T) { + g := NewWithT(t) + + tmpDir := t.TempDir() + + root := filepath.Join(tmpDir, "workdir") + g.Expect(os.Mkdir(root, 0o700)).To(Succeed()) + + g.Expect(os.WriteFile(filepath.Join(root, "file.txt"), []byte("secure"), 0o644)).To(Succeed()) + g.Expect(os.WriteFile(filepath.Join(tmpDir, "file.txt"), []byte("illegal"), 0o644)).To(Succeed()) + + fs, err := MakeFsOnDiskSecure(root) + g.Expect(err).ToNot(HaveOccurred()) + + files, err := fs.Glob(filepath.Join(tmpDir, "*/*.txt")) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(files).To(ContainElement(filepath.Join(root, "file.txt"))) + g.Expect(files).ToNot(ContainElement(filepath.Join(tmpDir, "file.txt"))) +} + +func Test_fsSecure_ReadFile(t *testing.T) { + g := NewWithT(t) + + tmpDir := t.TempDir() + + root := filepath.Join(tmpDir, "workdir") + g.Expect(os.Mkdir(root, 0o700)).To(Succeed()) + g.Expect(os.WriteFile(filepath.Join(root, "file.txt"), []byte("secure"), 0o644)).To(Succeed()) + g.Expect(os.WriteFile(filepath.Join(tmpDir, "file.txt"), []byte("illegal"), 0o644)).To(Succeed()) + + fs, err := MakeFsOnDiskSecure(root) + g.Expect(err).ToNot(HaveOccurred()) + + t.Run("secure read file", func(t *testing.T) { + g := NewWithT(t) + + path := filepath.Join(root, "file.txt") + b, err := fs.ReadFile(path) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(b).To(Equal([]byte("secure"))) + }) + + t.Run("illegal read file", func(t *testing.T) { + g := NewWithT(t) + + path := filepath.Join(tmpDir, "file.txt") + b, err := fs.ReadFile(path) + g.Expect(err).To(HaveOccurred()) + g.Expect(b).To(BeNil()) + }) +} + +func Test_fsSecure_WriteFile(t *testing.T) { + g := NewWithT(t) + + tmpDir := t.TempDir() + + root := filepath.Join(tmpDir, "workdir") + g.Expect(os.Mkdir(root, 0o700)).To(Succeed()) + + fs, err := MakeFsOnDiskSecure(root) + g.Expect(err).ToNot(HaveOccurred()) + + t.Run("secure write file", func(t *testing.T) { + g := NewWithT(t) + + path := filepath.Join(root, "file.txt") + data := []byte("secure") + err := fs.WriteFile(path, data) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(path).To(BeAnExistingFile()) + b, err := fs.ReadFile(path) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(b).To(Equal(data)) + }) + + t.Run("illegal write file", func(t *testing.T) { + g := NewWithT(t) + + path := filepath.Join(tmpDir, "file.txt") + err := fs.WriteFile(path, []byte("illegal")) + g.Expect(err).To(HaveOccurred()) + g.Expect(path).ToNot(BeAnExistingFile()) + }) +} + +func Test_fsSecure_Walk(t *testing.T) { + g := NewWithT(t) + + tmpDir := t.TempDir() + + root := filepath.Join(tmpDir, "workdir") + g.Expect(os.Mkdir(root, 0o700)).To(Succeed()) + g.Expect(os.WriteFile(filepath.Join(root, "file.txt"), []byte("secure"), 0o644)).To(Succeed()) + g.Expect(os.WriteFile(filepath.Join(tmpDir, "file.txt"), []byte("illegal"), 0o644)).To(Succeed()) + + fs, err := MakeFsOnDiskSecure(root) + g.Expect(err).ToNot(HaveOccurred()) + + t.Run("secure walk", func(t *testing.T) { + g := NewWithT(t) + + var walkedPaths []string + walk := func(path string, info os.FileInfo, err error) error { + walkedPaths = append(walkedPaths, path) + return nil + } + g.Expect(fs.Walk(root, walk)).To(Succeed()) + g.Expect(walkedPaths).To(Equal([]string{root, filepath.Join(root, "file.txt")})) + }) + + t.Run("illegal walk", func(t *testing.T) { + g := NewWithT(t) + + var walkedPaths []string + walk := func(path string, info os.FileInfo, err error) error { + walkedPaths = append(walkedPaths, path) + return nil + } + g.Expect(fs.Walk(tmpDir, walk)).To(HaveOccurred()) + g.Expect(walkedPaths).To(BeEmpty()) + }) +} + +func Test_isSecurePath(t *testing.T) { + type file struct { + name string + symlink string + } + tests := []struct { + name string + fs filesys.FileSystem + rootSuffix string + files []file + path string + wantErr types.GomegaMatcher + }{ + { + name: "secure non existing path", + fs: filesys.MakeFsOnDisk(), + path: "<root>/filepath", + wantErr: Succeed(), + }, + { + name: "illegal relative path", + fs: filesys.MakeFsOnDisk(), + rootSuffix: "subdir", + path: "../", + wantErr: HaveOccurred(), + }, + { + name: "illegal absolute path", + fs: filesys.MakeFsOnDisk(), + rootSuffix: "subdir", + path: "<root>", + wantErr: HaveOccurred(), + }, + { + name: "relative symlink", + fs: filesys.MakeFsOnDisk(), + rootSuffix: "subdir", + files: []file{ + {name: "subdir/file.txt"}, + {name: "subdir/subsubdir/symlink", symlink: "../file.txt"}, + }, + path: "<root>/subdir/subsubdir/symlink", + wantErr: Succeed(), + }, + { + name: "absolute symlink", + fs: filesys.MakeFsOnDisk(), + rootSuffix: "subdir", + files: []file{ + {name: "subdir/file.txt"}, + {name: "subdir/subsubdir/symlink", symlink: "<root>/subdir/file.txt"}, + }, + path: "<root>/subdir/subsubdir/symlink", + wantErr: Succeed(), + }, + { + name: "illegal relative symlink", + fs: filesys.MakeFsOnDisk(), + rootSuffix: "subdir", + files: []file{ + {name: "file.txt"}, + {name: "subdir/symlink", symlink: "../file.txt"}, + }, + path: "<root>/subdir/symlink", + wantErr: HaveOccurred(), + }, + { + name: "illegal absolute symlink", + fs: filesys.MakeFsOnDisk(), + rootSuffix: "subdir", + files: []file{ + {name: "file.txt"}, + {name: "subdir/symlink", symlink: "<root>/file.txt"}, + }, + path: "<root>/subdir/symlink", + wantErr: HaveOccurred(), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + + root := newTemp() + realRoot := filesys.ConfirmedDir(filepath.Join(root, tt.rootSuffix)) + g.Expect(tt.fs.MkdirAll(realRoot.String())).To(Succeed()) + t.Cleanup(func() { + g.Expect(tt.fs.RemoveAll(root)).To(Succeed()) + }) + + for _, f := range tt.files { + fPath := filepath.Join(root, f.name) + dir, base := filepath.Split(fPath) + g.Expect(tt.fs.MkdirAll(dir)).To(Succeed()) + + if symlink := f.symlink; symlink != "" { + if strings.HasPrefix(symlink, "<root>") { + symlink = strings.Replace(symlink, "<root>", root, 1) + } + g.Expect(os.Symlink(symlink, fPath)).To(Succeed()) + continue + } + + if base != "" { + file, err := tt.fs.Create(fPath) + g.Expect(err).ToNot(HaveOccurred()) + _, err = file.Write([]byte(f.name + " data")) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(file.Close()).To(Succeed()) + } + } + + path := tt.path + if strings.HasPrefix(path, "<root>") { + path = strings.Replace(path, "<root>", root, 1) + } + + err := isSecurePath(tt.fs, realRoot, path) + g.Expect(err).To(tt.wantErr) + }) + } +} + +func newTemp() string { + return filepath.Join(os.TempDir(), "securefs-"+randStringBytes(5)) +} + +func randStringBytes(n int) string { + const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + + b := make([]byte, n) + for i := range b { + b[i] = letterBytes[rand.Int63()%int64(len(letterBytes))] + } + return string(b) +}
f4528fb25d61controllers: use own Kustomize FS implementation
5 files changed · +58 −44
controllers/kustomization_controller.go+9 −12 modified@@ -52,7 +52,6 @@ import ( "sigs.k8s.io/controller-runtime/pkg/predicate" "sigs.k8s.io/controller-runtime/pkg/reconcile" "sigs.k8s.io/controller-runtime/pkg/source" - "sigs.k8s.io/kustomize/kyaml/filesys" apiacl "github.com/fluxcd/pkg/apis/acl" "github.com/fluxcd/pkg/apis/meta" @@ -357,7 +356,7 @@ func (r *KustomizationReconciler) reconcile( } // generate kustomization.yaml if needed - err = r.generate(kustomization, dirPath) + err = r.generate(kustomization, tmpDir, dirPath) if err != nil { return kustomizev1.KustomizationNotReady( kustomization, @@ -629,8 +628,8 @@ func (r *KustomizationReconciler) getSource(ctx context.Context, kustomization k return source, nil } -func (r *KustomizationReconciler) generate(kustomization kustomizev1.Kustomization, dirPath string) error { - gen := NewGenerator(kustomization) +func (r *KustomizationReconciler) generate(kustomization kustomizev1.Kustomization, workDir string, dirPath string) error { + gen := NewGenerator(workDir, kustomization) return gen.WriteFile(dirPath) } @@ -641,19 +640,17 @@ func (r *KustomizationReconciler) build(ctx context.Context, workDir string, kus } defer cleanup() - // import OpenPGP keys if any + // Import decryption keys if err := dec.ImportKeys(ctx); err != nil { return nil, err } - fs := filesys.MakeFsOnDisk() - // decrypt .env files before building kustomization - if kustomization.Spec.Decryption != nil { - if err = dec.DecryptEnvSources(dirPath); err != nil { - return nil, fmt.Errorf("error decrypting .env file: %w", err) - } + // Decrypt Kustomize EnvSources files before build + if err = dec.DecryptEnvSources(dirPath); err != nil { + return nil, fmt.Errorf("error decrypting env sources: %w", err) } - m, err := buildKustomization(fs, dirPath) + + m, err := secureBuildKustomization(workDir, dirPath) if err != nil { return nil, fmt.Errorf("kustomize build failed: %w", err) }
controllers/kustomization_decryptor.go+1 −1 modified@@ -358,7 +358,7 @@ func (d *KustomizeDecryptor) DecryptResource(res *resource.Resource) (*resource. // outside the working directory of the decryptor, but returns any decryption // error. func (d *KustomizeDecryptor) DecryptEnvSources(path string) error { - if d.kustomization.Spec.Decryption.Provider != DecryptionProviderSOPS { + if d.kustomization.Spec.Decryption == nil || d.kustomization.Spec.Decryption.Provider != DecryptionProviderSOPS { return nil }
controllers/kustomization_generator.go+23 −9 modified@@ -29,19 +29,22 @@ import ( "sigs.k8s.io/kustomize/api/provider" "sigs.k8s.io/kustomize/api/resmap" kustypes "sigs.k8s.io/kustomize/api/types" - "sigs.k8s.io/kustomize/kyaml/filesys" "sigs.k8s.io/yaml" - kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta2" "github.com/fluxcd/pkg/apis/kustomize" + securefs "github.com/fluxcd/pkg/kustomize/filesys" + + kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta2" ) type KustomizeGenerator struct { + root string kustomization kustomizev1.Kustomization } -func NewGenerator(kustomization kustomizev1.Kustomization) *KustomizeGenerator { +func NewGenerator(root string, kustomization kustomizev1.Kustomization) *KustomizeGenerator { return &KustomizeGenerator{ + root: root, kustomization: kustomization, } } @@ -127,7 +130,10 @@ func checkKustomizeImageExists(images []kustypes.Image, imageName string) (bool, } func (kg *KustomizeGenerator) generateKustomization(dirPath string) error { - fs := filesys.MakeFsOnDisk() + fs, err := securefs.MakeFsOnDiskSecure(kg.root) + if err != nil { + return err + } // Determine if there already is a Kustomization file at the root, // as this means we do not have to generate one. @@ -234,11 +240,19 @@ func adaptSelector(selector *kustomize.Selector) (output *kustypes.Selector) { // TODO: remove mutex when kustomize fixes the concurrent map read/write panic var kustomizeBuildMutex sync.Mutex -// buildKustomization wraps krusty.MakeKustomizer with the following settings: -// - load files from outside the kustomization.yaml root -// - disable plugins except for the builtin ones -func buildKustomization(fs filesys.FileSystem, dirPath string) (resmap.ResMap, error) { - // temporary workaround for concurrent map read and map write bug +// secureBuildKustomization wraps krusty.MakeKustomizer with the following settings: +// - secure on-disk FS denying operations outside root +// - load files from outside the kustomization dir path +// (but not outside root) +// - disable plugins except for the builtin ones +func secureBuildKustomization(root, dirPath string) (resmap.ResMap, error) { + // Create secure FS for root + fs, err := securefs.MakeFsOnDiskSecure(root) + if err != nil { + return nil, err + } + + // Temporary workaround for concurrent map read and map write bug // https://github.com/kubernetes-sigs/kustomize/issues/3659 kustomizeBuildMutex.Lock() defer kustomizeBuildMutex.Unlock()
go.mod+8 −7 modified@@ -16,6 +16,7 @@ require ( github.com/fluxcd/pkg/apis/acl v0.0.3 github.com/fluxcd/pkg/apis/kustomize v0.3.2 github.com/fluxcd/pkg/apis/meta v0.12.1 + github.com/fluxcd/pkg/kustomize v0.2.0 github.com/fluxcd/pkg/runtime v0.13.3 github.com/fluxcd/pkg/ssa v0.15.1 github.com/fluxcd/pkg/testserver v0.2.0 @@ -29,14 +30,13 @@ require ( go.mozilla.org/sops/v3 v3.7.2 golang.org/x/net v0.0.0-20220225172249-27dd8689420f google.golang.org/grpc v1.45.0 - k8s.io/api v0.23.4 - k8s.io/apiextensions-apiserver v0.23.4 - k8s.io/apimachinery v0.23.4 - k8s.io/client-go v0.23.4 + k8s.io/api v0.23.5 + k8s.io/apiextensions-apiserver v0.23.5 + k8s.io/apimachinery v0.23.5 + k8s.io/client-go v0.23.5 sigs.k8s.io/cli-utils v0.29.3 - sigs.k8s.io/controller-runtime v0.11.1 + sigs.k8s.io/controller-runtime v0.11.2 sigs.k8s.io/kustomize/api v0.11.4 - sigs.k8s.io/kustomize/kyaml v0.13.6 sigs.k8s.io/yaml v1.3.0 ) @@ -199,11 +199,12 @@ require ( gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect k8s.io/cli-runtime v0.23.2 // indirect - k8s.io/component-base v0.23.4 // indirect + k8s.io/component-base v0.23.5 // indirect k8s.io/klog/v2 v2.50.0 // indirect k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65 // indirect k8s.io/kubectl v0.23.2 // indirect k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9 // indirect sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2 // indirect + sigs.k8s.io/kustomize/kyaml v0.13.6 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect )
go.sum+17 −15 modified@@ -276,6 +276,8 @@ github.com/fluxcd/pkg/apis/kustomize v0.3.2 h1:ULoAwOOekHf5cy6mYIwL+K6v8/cfcNVVb github.com/fluxcd/pkg/apis/kustomize v0.3.2/go.mod h1:p8iAH5TeqMBnnxkkpCNNDvWYfKlNRx89a6WKOo+hJHA= github.com/fluxcd/pkg/apis/meta v0.12.1 h1:m5PfKAqbqWBvGp9+JRj1sv+xNkGsHwUVf+3rJ8wm6SE= github.com/fluxcd/pkg/apis/meta v0.12.1/go.mod h1:f8YVt70/KAhqzZ7xxhjvqyzKubOYx2pAbakb/FfCEg8= +github.com/fluxcd/pkg/kustomize v0.2.0 h1:twiGAFJctt2tyH8vHxL1uqb6BlU3B9ZqG8uSlluuioM= +github.com/fluxcd/pkg/kustomize v0.2.0/go.mod h1:Qczvl7vNY9NJBpyaFrldsxfGjj6uaMcMmKGsSJ6hcxc= github.com/fluxcd/pkg/runtime v0.13.3 h1:k0Xun+RoEC/F6iuAPTA6rQb+I4B4oecBx6pOcodX11A= github.com/fluxcd/pkg/runtime v0.13.3/go.mod h1:dzWNKqFzFXeittbpFcJzR3cdC9CWlbzw+pNOgaVvF/0= github.com/fluxcd/pkg/ssa v0.15.1 h1:HXAT+K6c9Yy8Evxdyk3DU0KTk3yZ+fwgTEEzU1W/1V8= @@ -1440,24 +1442,24 @@ honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= k8s.io/api v0.23.2/go.mod h1:sYuDb3flCtRPI8ghn6qFrcK5ZBu2mhbElxRE95qpwlI= -k8s.io/api v0.23.4 h1:85gnfXQOWbJa1SiWGpE9EEtHs0UVvDyIsSMpEtl2D4E= -k8s.io/api v0.23.4/go.mod h1:i77F4JfyNNrhOjZF7OwwNJS5Y1S9dpwvb9iYRYRczfI= -k8s.io/apiextensions-apiserver v0.23.4 h1:AFDUEu/yEf0YnuZhqhIFhPLPhhcQQVuR1u3WCh0rveU= -k8s.io/apiextensions-apiserver v0.23.4/go.mod h1:TWYAKymJx7nLMxWCgWm2RYGXHrGlVZnxIlGnvtfYu+g= +k8s.io/api v0.23.5 h1:zno3LUiMubxD/V1Zw3ijyKO3wxrhbUF1Ck+VjBvfaoA= +k8s.io/api v0.23.5/go.mod h1:Na4XuKng8PXJ2JsploYYrivXrINeTaycCGcYgF91Xm8= +k8s.io/apiextensions-apiserver v0.23.5 h1:5SKzdXyvIJKu+zbfPc3kCbWpbxi+O+zdmAJBm26UJqI= +k8s.io/apiextensions-apiserver v0.23.5/go.mod h1:ntcPWNXS8ZPKN+zTXuzYMeg731CP0heCTl6gYBxLcuQ= k8s.io/apimachinery v0.23.2/go.mod h1:zDqeV0AK62LbCI0CI7KbWCAYdLg+E+8UXJ0rIz5gmS8= -k8s.io/apimachinery v0.23.4 h1:fhnuMd/xUL3Cjfl64j5ULKZ1/J9n8NuQEgNL+WXWfdM= -k8s.io/apimachinery v0.23.4/go.mod h1:BEuFMMBaIbcOqVIJqNZJXGFTP4W6AycEpb5+m/97hrM= -k8s.io/apiserver v0.23.4/go.mod h1:A6l/ZcNtxGfPSqbFDoxxOjEjSKBaQmE+UTveOmMkpNc= +k8s.io/apimachinery v0.23.5 h1:Va7dwhp8wgkUPWsEXk6XglXWU4IKYLKNlv8VkX7SDM0= +k8s.io/apimachinery v0.23.5/go.mod h1:BEuFMMBaIbcOqVIJqNZJXGFTP4W6AycEpb5+m/97hrM= +k8s.io/apiserver v0.23.5/go.mod h1:7wvMtGJ42VRxzgVI7jkbKvMbuCbVbgsWFT7RyXiRNTw= k8s.io/cli-runtime v0.23.2 h1:4zOZX78mFSakwe4gef81XDBu94Yu0th6bfveTOx8ZQk= k8s.io/cli-runtime v0.23.2/go.mod h1:Ag70akCDvwux4HxY+nH2J3UqE2e6iwSSdG1HE6p1VTU= k8s.io/client-go v0.23.2/go.mod h1:k3YbsWg6GWdHF1THHTQP88X9RhB1DWPo3Dq7KfU/D1c= -k8s.io/client-go v0.23.4 h1:YVWvPeerA2gpUudLelvsolzH7c2sFoXXR5wM/sWqNFU= -k8s.io/client-go v0.23.4/go.mod h1:PKnIL4pqLuvYUK1WU7RLTMYKPiIh7MYShLshtRY9cj0= +k8s.io/client-go v0.23.5 h1:zUXHmEuqx0RY4+CsnkOn5l0GU+skkRXKGJrhmE2SLd8= +k8s.io/client-go v0.23.5/go.mod h1:flkeinTO1CirYgzMPRWxUCnV0G4Fbu2vLhYCObnt/r4= k8s.io/code-generator v0.23.2/go.mod h1:S0Q1JVA+kSzTI1oUvbKAxZY/DYbA/ZUb4Uknog12ETk= -k8s.io/code-generator v0.23.4/go.mod h1:S0Q1JVA+kSzTI1oUvbKAxZY/DYbA/ZUb4Uknog12ETk= +k8s.io/code-generator v0.23.5/go.mod h1:S0Q1JVA+kSzTI1oUvbKAxZY/DYbA/ZUb4Uknog12ETk= k8s.io/component-base v0.23.2/go.mod h1:wS9Z03MO3oJ0RU8bB/dbXTiluGju+SC/F5i660gxB8c= -k8s.io/component-base v0.23.4 h1:SziYh48+QKxK+ykJ3Ejqd98XdZIseVBG7sBaNLPqy6M= -k8s.io/component-base v0.23.4/go.mod h1:8o3Gg8i2vnUXGPOwciiYlkSaZT+p+7gA9Scoz8y4W4E= +k8s.io/component-base v0.23.5 h1:8qgP5R6jG1BBSXmRYW+dsmitIrpk8F/fPEvgDenMCCE= +k8s.io/component-base v0.23.5/go.mod h1:c5Nq44KZyt1aLl0IpHX82fhsn84Sb0jjzwjpcA42bY0= k8s.io/component-helpers v0.23.2/go.mod h1:J6CMwiaf0izLoNwiLl2OymB4+rGTsTpWp6PL/AqOM4U= k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/gengo v0.0.0-20210813121822-485abfe95c7c/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= @@ -1480,11 +1482,11 @@ k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9/go.mod h1:jPW/WVKK9YHAvNhRxK0md/ rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= -sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.27/go.mod h1:tq2nT0Kx7W+/f2JVE+zxYtUhdjuELJkVpNz+x/QN5R4= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.30/go.mod h1:fEO7lRTdivWO2qYVCVG7dEADOMo/MLDCVr8So2g88Uw= sigs.k8s.io/cli-utils v0.29.3 h1:4QRB9ayCd5pd9M/D3q2KQgr+nYrvRaw3suW+rcOutvk= sigs.k8s.io/cli-utils v0.29.3/go.mod h1:WDVRa5/eQBKntG++uyKdyT+xU7MLdCR4XsgseqL5uX4= -sigs.k8s.io/controller-runtime v0.11.1 h1:7YIHT2QnHJArj/dk9aUkYhfqfK5cIxPOX5gPECfdZLU= -sigs.k8s.io/controller-runtime v0.11.1/go.mod h1:KKwLiTooNGu+JmLZGn9Sl3Gjmfj66eMbCQznLP5zcqA= +sigs.k8s.io/controller-runtime v0.11.2 h1:H5GTxQl0Mc9UjRJhORusqfJCIjBO8UtUxGggCwL1rLA= +sigs.k8s.io/controller-runtime v0.11.2/go.mod h1:P6QCzrEjLaZGqHsfd+os7JQ+WFZhvB8MRFsn4dWF7O4= sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6/go.mod h1:p4QtZmO4uMYipTQNzagwnNoseA6OxSUutVw05NhYDRs= sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2 h1:kDi4JBNAsJWfz1aEXhO8Jg87JJaPNLh5tIzYHgStQ9Y= sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2/go.mod h1:B+TnT182UBxE84DiCz4CVE26eOSDAeYCpfDnC2kdKMY=
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/advisories/GHSA-j77r-2fxf-5jrwghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2022-24877ghsaADVISORY
- github.com/fluxcd/kustomize-controllerghsaPACKAGE
- github.com/fluxcd/flux2/security/advisories/GHSA-j77r-2fxf-5jrwghsax_refsource_CONFIRMWEB
- github.com/fluxcd/kustomize-controller/commit/f4528fb25d611da94e491346bea056d5c5c3611fghsaWEB
- github.com/fluxcd/pkg/commit/0ec014baf417fd3879d366a45503a548b9267d2aghsaWEB
News mentions
0No linked articles in our index yet.