diff --git a/.gitignore b/.gitignore index 241e02e5b..c0995cb71 100644 --- a/.gitignore +++ b/.gitignore @@ -18,4 +18,7 @@ /controller.image /*-static /controller.yaml +/sealedsecret-crd.yaml /docker/controller +*.iml +.idea diff --git a/cmd/controller/controller.go b/cmd/controller/controller.go index 4e171307d..a66286544 100644 --- a/cmd/controller/controller.go +++ b/cmd/controller/controller.go @@ -3,6 +3,7 @@ package main import ( "crypto/rsa" "fmt" + "k8s.io/apimachinery/pkg/runtime" "log" "time" @@ -59,7 +60,7 @@ func unseal(sclient v1.SecretsGetter, codecs runtimeserializer.CodecFactory, key } // NewController returns the main sealed-secrets controller loop. -func NewController(clientset kubernetes.Interface, ssinformer ssinformer.SharedInformerFactory, privKey *rsa.PrivateKey) cache.Controller { +func NewController(clientset kubernetes.Interface, ssinformer ssinformer.SharedInformerFactory, privKey *rsa.PrivateKey) *Controller { queue := workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()) informer := ssinformer.Bitnami().V1alpha1(). @@ -193,3 +194,21 @@ func (c *Controller) unseal(key string) error { } return err } + +func (c *Controller) AttemptUnseal(content []byte) (bool, error) { + object, err := runtime.Decode(scheme.Codecs.UniversalDecoder(ssv1alpha1.SchemeGroupVersion), content) + if err != nil { + return false, err + } + + switch s := object.(type) { + case *ssv1alpha1.SealedSecret: + if _, err := s.Unseal(scheme.Codecs, c.privKey); err != nil { + return false, nil + } + return true, nil + default: + return false, fmt.Errorf("Unexpected resource type: %s", s.GetObjectKind().GroupVersionKind().String()) + + } +} diff --git a/cmd/controller/main.go b/cmd/controller/main.go index 12f72fb3a..200873a14 100644 --- a/cmd/controller/main.go +++ b/cmd/controller/main.go @@ -203,7 +203,7 @@ func main2() error { go controller.Run(stop) - go httpserver(func() ([]*x509.Certificate, error) { return certs, nil }) + go httpserver(func() ([]*x509.Certificate, error) { return certs, nil }, controller.AttemptUnseal) sigterm := make(chan os.Signal, 1) signal.Notify(sigterm, syscall.SIGTERM) diff --git a/cmd/controller/server.go b/cmd/controller/server.go index 7b559e533..546b9c99b 100644 --- a/cmd/controller/server.go +++ b/cmd/controller/server.go @@ -3,6 +3,7 @@ package main import ( "crypto/x509" "io" + "io/ioutil" "log" "net/http" "time" @@ -20,7 +21,9 @@ var ( // Called on every request to /cert. Errors will be logged and return a 500. type certProvider func() ([]*x509.Certificate, error) -func httpserver(cp certProvider) { +type secretChecker func([]byte) (bool, error) + +func httpserver(cp certProvider, sc secretChecker) { mux := http.NewServeMux() mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) { @@ -28,6 +31,31 @@ func httpserver(cp certProvider) { io.WriteString(w, "ok\n") }) + mux.HandleFunc("/v1/verify", func(w http.ResponseWriter, r *http.Request) { + content, err := ioutil.ReadAll(r.Body) + + if err != nil { + log.Printf("Error handling /v1/verify request: %v", err) + w.WriteHeader(http.StatusBadRequest) + return + } + + valid, err := sc(content) + + if err != nil { + log.Printf("Error validating secret: %v", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + if valid { + w.WriteHeader(http.StatusOK) + } else { + w.WriteHeader(http.StatusConflict) + } + + }) + mux.HandleFunc("/v1/cert.pem", func(w http.ResponseWriter, r *http.Request) { certs, err := cp() diff --git a/cmd/kubeseal/main.go b/cmd/kubeseal/main.go index e14621116..5f6e91982 100644 --- a/cmd/kubeseal/main.go +++ b/cmd/kubeseal/main.go @@ -7,6 +7,8 @@ import ( "fmt" "io" "io/ioutil" + "k8s.io/apimachinery/pkg/util/net" + "net/http" "os" "strings" @@ -24,6 +26,8 @@ import ( // Register Auth providers _ "k8s.io/client-go/plugin/pkg/client/auth" + + k8serrors "k8s.io/apimachinery/pkg/api/errors" ) var ( @@ -34,6 +38,7 @@ var ( outputFormat = flag.String("format", "json", "Output format for sealed secret. Either json or yaml") dumpCert = flag.Bool("fetch-cert", false, "Write certificate to stdout. Useful for later use with --cert") printVersion = flag.Bool("version", false, "Print version information and exit") + validateSecret = flag.Bool("validate", false, "Validate that the sealed secret can be decrypted") // VERSION set from Makefile VERSION = "UNKNOWN" @@ -211,6 +216,40 @@ func seal(in io.Reader, out io.Writer, codecs runtimeserializer.CodecFactory, pu return nil } +func validateSealedSecret(in io.Reader, namespace, name string) error { + conf, err := clientConfig.ClientConfig() + if err != nil { + return err + } + restClient, err := corev1.NewForConfig(conf) + if err != nil { + return err + } + + content, err := ioutil.ReadAll(in) + if err != nil { + return err + } + + req := restClient.RESTClient().Post(). + Namespace(namespace). + Resource("services"). + SubResource("proxy"). + Name(net.JoinSchemeNamePort("http", name, "")). + Suffix("/v1/verify") + + req.Body(content) + res := req.Do() + if err := res.Error(); err != nil { + if status, ok := err.(*k8serrors.StatusError); ok && status.Status().Code == http.StatusConflict { + return fmt.Errorf("Unable to decrypt sealed secret") + } + return fmt.Errorf("Error occurred while validating sealed secret") + } + + return nil +} + func main() { flag.Parse() goflag.CommandLine.Parse([]string{}) @@ -220,6 +259,14 @@ func main() { return } + if *validateSecret { + err := validateSealedSecret(os.Stdin, *controllerNs, *controllerName) + if err != nil { + panic(err.Error()) + } + return + } + f, err := openCert() if err != nil { panic(err.Error()) diff --git a/integration/kubeseal_test.go b/integration/kubeseal_test.go index fee0aa969..b8f060dd9 100644 --- a/integration/kubeseal_test.go +++ b/integration/kubeseal_test.go @@ -229,3 +229,63 @@ var _ = Describe("kubeseal --version", func() { Expect(output.String()).Should(MatchRegexp("^kubeseal version: (v[0-9]+\\.[0-9]+\\.[0-9]+|[0-9a-f]{40})(\\+dirty)?")) }) }) + +var _ = Describe("kubeseal --verify", func() { + var c corev1.CoreV1Interface + const secretName = "testSecret" + const testNs = "testns" + var input io.Reader + var output *bytes.Buffer + var ss *ssv1alpha1.SealedSecret + var args []string + var err error + + BeforeEach(func() { + c = corev1.NewForConfigOrDie(clusterConfigOrDie()) + args = append(args, "--validate") + output = &bytes.Buffer{} + }) + + BeforeEach(func() { + input := &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: testNs, + Name: secretName, + }, + Data: map[string][]byte{ + "foo": []byte("bar"), + }, + } + outobj, err := runKubesealWith([]string{}, input) + Expect(err).NotTo(HaveOccurred()) + ss = outobj.(*ssv1alpha1.SealedSecret) + }) + + JustBeforeEach(func() { + enc := scheme.Codecs.LegacyCodec(ssv1alpha1.SchemeGroupVersion) + indata, err := runtime.Encode(enc, ss) + Expect(err).NotTo(HaveOccurred()) + input = bytes.NewReader(indata) + }) + + JustBeforeEach(func() { + err = runKubeseal(args, input, output) + }) + + Context("valid sealed secret", func() { + It("should see the sealed secret as valid", func() { + Expect(err).NotTo(HaveOccurred()) + }) + }) + + Context("invalid sealed secret", func() { + BeforeEach(func() { + ss.Name = "a-completely-different-name" + }) + + It("should see the sealed secret as invalid", func() { + Expect(err).To(HaveOccurred()) + }) + }) + +})