diff --git a/cmd/flux/encrypt.go b/cmd/flux/encrypt.go new file mode 100644 index 00000000..09cb27ea --- /dev/null +++ b/cmd/flux/encrypt.go @@ -0,0 +1,39 @@ +/* +Copyright 2021 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 main + +import ( + "github.com/spf13/cobra" +) + +var encryptCmd = &cobra.Command{ + Use: "encrypt", + Short: "Encrypt secrets using SOPS", + Long: "The encrypt sub-commands initialise and manage Secret encryption using SOPS.", +} + +type encryptFlags struct { + export bool +} + +var encryptArgs encryptFlags + +func init() { + encryptCmd.PersistentFlags().BoolVar(&encryptArgs.export, "export", false, "export in YAML format to stdout") + + rootCmd.AddCommand(encryptCmd) +} diff --git a/cmd/flux/encrypt_init.go b/cmd/flux/encrypt_init.go new file mode 100644 index 00000000..83a74832 --- /dev/null +++ b/cmd/flux/encrypt_init.go @@ -0,0 +1,113 @@ +/* +Copyright 2021 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 main + +import ( + "context" + "fmt" + "io/ioutil" + "os" + "path/filepath" + + "filippo.io/age" + "github.com/fluxcd/flux2/internal/utils" + "github.com/go-git/go-git/v5" + "github.com/spf13/cobra" + corev1 "k8s.io/api/core/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +var encryptInitCmd = &cobra.Command{ + Use: "init", + Short: "Init SOPS encryption with age identity", + Long: "The encryption init command creates a new age identity and writes a .sops.yaml file to the current working directory.", + Example: ` # Init SOPS encryption with a new age identity + flux encryption init`, + RunE: encryptInitCmdRun, +} + +func init() { + encryptCmd.AddCommand(encryptInitCmd) +} + +func encryptInitCmdRun(cmd *cobra.Command, args []string) error { + // Confirm our current path is in a Git repository + path, err := os.Getwd() + if err != nil { + return err + } + if _, err := git.PlainOpen(path); err != nil { + if err == git.ErrRepositoryNotExists { + err = fmt.Errorf("'%s' is not in a Git repository", path) + } + return err + } + + // Abort early if .sops.yaml already exists + sopsCfgPath := filepath.Join(path, ".sops.yaml") + if _, err := os.Stat(sopsCfgPath); err == nil || os.IsExist(err) { + return fmt.Errorf("'%s' already contains a .sops.yaml config", path) + } + + // Generate a new identity + i, err := age.GenerateX25519Identity() + if err != nil { + return err + } + logger.Successf("Generated identity %s", i.Recipient().String()) + + // Attempt to configure identity in .sops.yaml + const sopsCfg = `creation_rules: + - path_regex: .*.yaml + encrypted_regex: ^(data|stringData)$ + age: %s +` + if err := ioutil.WriteFile(sopsCfgPath, []byte(fmt.Sprintf(sopsCfg, i.Recipient().String())), 0644); err != nil { + logger.Failuref("Failed to write recipient to .sops.yaml file") + return err + } + logger.Successf("Configured recipient in .sops.yaml file") + + // Init client + ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) + defer cancel() + kubeClient, err := utils.KubeClient(rootArgs.kubeconfig, rootArgs.kubecontext) + if err != nil { + return err + } + + // Create a secret + secret := &corev1.Secret{ + ObjectMeta: v1.ObjectMeta{ + Name: "sops-age", + Namespace: rootArgs.namespace, + }, + StringData: map[string]string{ + "flux-auto.age": i.String(), + }, + } + if err := kubeClient.Create(ctx, secret); err != nil { + return err + } + logger.Successf(`Secret '%s' with private key created`, secret.Name) + + // TODO(hidde): lookup kustomize based on path ref? Do direct cluster mutation? (Preferably not!) + // Feels something is missing in general to provide a user experience improving bridge between "die hard" + // `--export` and "please do not do this" direct-apply-to-cluster. + + return nil +} diff --git a/go.mod b/go.mod index 87410dde..9b64bae7 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/fluxcd/flux2 go 1.16 require ( + filippo.io/age v1.0.0-rc.3 github.com/Masterminds/semver/v3 v3.1.0 github.com/cyphar/filepath-securejoin v0.2.2 github.com/fluxcd/go-git-providers v0.1.1 diff --git a/go.sum b/go.sum index b2544898..77a7e9c4 100644 --- a/go.sum +++ b/go.sum @@ -24,6 +24,9 @@ cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiy cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +filippo.io/age v1.0.0-rc.3 h1:8JjuJ5ffGKDmC4SS0zoyQxZROZX75so768b7AjulKLw= +filippo.io/age v1.0.0-rc.3/go.mod h1:UjINLBMeA60aGZkHCGsmDzKcaXoTTzpvrqQM+Vo3YHU= +filippo.io/edwards25519 v1.0.0-beta.3/go.mod h1:X+pm78QAUPtFLi1z9PYIlS/bdDnvbCOGKtZ+ACWEf7o= github.com/Azure/azure-sdk-for-go v35.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/azure-sdk-for-go v38.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/azure-sdk-for-go v42.3.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= @@ -799,6 +802,7 @@ golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b h1:7mWr3k41Qtv8XlltBkDkl8LoP3mpSgBW8BUoxtEdbXg=