Skip to content

Commit

Permalink
Add the ability to check whether a sealed secret is valid
Browse files Browse the repository at this point in the history
Make it easier to verify whether a sealed secret is valid, without having to rely on reading the logs of the controller. Example: `cat manifest.yml | kubeseal --validate`
Fixes bitnami-labs#118
  • Loading branch information
wjam committed Oct 24, 2018
1 parent 5703042 commit b931cbc
Show file tree
Hide file tree
Showing 6 changed files with 160 additions and 3 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,7 @@
/controller.image
/*-static
/controller.yaml
/sealedsecret-crd.yaml
/docker/controller
*.iml
.idea
21 changes: 20 additions & 1 deletion cmd/controller/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package main
import (
"crypto/rsa"
"fmt"
"k8s.io/apimachinery/pkg/runtime"
"log"
"time"

Expand Down Expand Up @@ -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().
Expand Down Expand Up @@ -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())

}
}
2 changes: 1 addition & 1 deletion cmd/controller/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
30 changes: 29 additions & 1 deletion cmd/controller/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package main
import (
"crypto/x509"
"io"
"io/ioutil"
"log"
"net/http"
"time"
Expand All @@ -20,14 +21,41 @@ 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) {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
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()

Expand Down
47 changes: 47 additions & 0 deletions cmd/kubeseal/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"fmt"
"io"
"io/ioutil"
"k8s.io/apimachinery/pkg/util/net"
"net/http"
"os"
"strings"

Expand All @@ -24,6 +26,8 @@ import (

// Register Auth providers
_ "k8s.io/client-go/plugin/pkg/client/auth"

k8serrors "k8s.io/apimachinery/pkg/api/errors"
)

var (
Expand All @@ -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"
Expand Down Expand Up @@ -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{})
Expand All @@ -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())
Expand Down
60 changes: 60 additions & 0 deletions integration/kubeseal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
})
})

})

0 comments on commit b931cbc

Please sign in to comment.