Skip to content

Commit

Permalink
Merge pull request #14 from anguslees/certserve
Browse files Browse the repository at this point in the history
Fetch certificate from controller at runtime, if not specified locally
  • Loading branch information
anguslees committed Jun 20, 2017
2 parents 3d4ce56 + 0435b72 commit b1d1457
Show file tree
Hide file tree
Showing 247 changed files with 135,587 additions and 54 deletions.
25 changes: 11 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,26 +30,21 @@ and be ready for operation. If it does not, check the controller
logs.

The key certificate (public key portion) is used for sealing secrets,
and needs to be installed wherever `ksonnet-seal` is going to be
and needs to be available wherever `ksonnet-seal` is going to be
used. The certificate is not secret information, although you need to
ensure you are using the correct file.

The certificate is printed to the controller log on startup, and can
also be retrieved directly from the underlying secret (the latter
requires access to the sealing secret, which is generally an
undesirable thing). (TODO: Improve this part)

```sh
# Fetch cluster-wide certificate used for encrypting.
# The certificate is also printed to the controller log on startup.
$ kubectl get secret -n kube-system sealed-secrets-key -o go-template='{{index .data "tls.crt"}}' | base64 -d > seal.crt
```
`ksonnet-seal` will fetch the certificate from the controller at
runtime (requires secure access to the Kubernetes API server), but can
also be read from a local file for offline situations (eg: automated
jobs). The certificate is also printed to the controller log on
startup.

## Usage

```sh
# This is the important bit:
$ ksonnet-seal --cert seal.crt <mysecret.json >mysealedsecret.json
$ ksonnet-seal --namespace default <mysecret.json >mysealedsecret.json

# mysealedsecret.json is safe to upload to github, post to twitter,
# etc. Eventually:
Expand All @@ -71,7 +66,7 @@ they like (provided the namespace/name matches). It is up to your
existing config management workflow, cluster RBAC rules, etc to ensure
that only the intended `SealedSecret` is uploaded to the cluster. The
only change from existing Kubernetes is that the *contents* of the
`Secret` are now hidden.
`Secret` are now hidden while outside the cluster.

## Details

Expand All @@ -84,7 +79,9 @@ startup, and generates a new key pair if not found. The key is
persisted in a regular `Secret` in the same namespace as the
controller. The public key portion of this (in the form of a
self-signed certificate) should be made publicly available to anyone
wanting to use `SealedSecret`s with this cluster.
wanting to use `SealedSecret`s with this cluster. The certificate is
printed to the controller log at startup, and available via an HTTP
GET to `/v1/cert.pem` on the controller.

During encryption, the original `Secret` is JSON-encoded and
symmetrically encrypted using AES-GCM with a randomly-generated
Expand Down
29 changes: 20 additions & 9 deletions cmd/controller/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import (
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"flag"
goflag "flag"
"io"
"io/ioutil"
"log"
Expand All @@ -16,6 +16,7 @@ import (
"syscall"
"time"

flag "github.com/spf13/pflag"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
Expand All @@ -37,6 +38,15 @@ var (
myCN = flag.String("my-cn", "", "CN to use in generated certificate.")
)

func init() {
// Standard goflags (glog in particular)
flag.CommandLine.AddGoFlagSet(goflag.CommandLine)
if f := flag.CommandLine.Lookup("logtostderr"); f != nil {
f.DefValue = "true"
f.Value.Set(f.DefValue)
}
}

type controller struct {
clientset kubernetes.Interface
}
Expand Down Expand Up @@ -113,36 +123,36 @@ func signKey(r io.Reader, key *rsa.PrivateKey) (*x509.Certificate, error) {
return x509.ParseCertificate(data)
}

func initKey(client kubernetes.Interface, r io.Reader, keySize int, namespace, keyName string) (*rsa.PrivateKey, error) {
func initKey(client kubernetes.Interface, r io.Reader, keySize int, namespace, keyName string) (*rsa.PrivateKey, []*x509.Certificate, error) {
privKey, certs, err := readKey(client, namespace, keyName)
if err != nil {
if errors.IsNotFound(err) {
log.Printf("Key %s/%s not found, generating new %d bit key", namespace, keyName, keySize)
privKey, err = rsa.GenerateKey(r, keySize)
if err != nil {
return nil, err
return nil, nil, err
}

cert, err := signKey(r, privKey)
if err != nil {
return nil, err
return nil, nil, err
}
certs = []*x509.Certificate{cert}

if err = writeKey(client, privKey, certs, namespace, keyName); err != nil {
return nil, err
return nil, nil, err
}
log.Printf("New key written to %s/%s", namespace, keyName)
} else {
return nil, err
return nil, nil, err
}
}

for _, cert := range certs {
log.Printf("Certificate is:\n%s\n", certUtil.EncodeCertPEM(cert))
}

return privKey, nil
return privKey, certs, nil
}

func myNamespace() string {
Expand Down Expand Up @@ -187,7 +197,7 @@ func main2() error {

myNs := myNamespace()

privKey, err := initKey(clientset, rand.Reader, *keySize, myNs, *keyName)
privKey, certs, err := initKey(clientset, rand.Reader, *keySize, myNs, *keyName)
if err != nil {
return err
}
Expand All @@ -199,7 +209,7 @@ func main2() error {

go controller.Run(stop)

go httpserver()
go httpserver(func() ([]*x509.Certificate, error) { return certs, nil })

sigterm := make(chan os.Signal, 1)
signal.Notify(sigterm, syscall.SIGTERM)
Expand All @@ -210,6 +220,7 @@ func main2() error {

func main() {
flag.Parse()
goflag.CommandLine.Parse([]string{})

if err := main2(); err != nil {
panic(err.Error())
Expand Down
10 changes: 7 additions & 3 deletions cmd/controller/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ func TestInitKey(t *testing.T) {
rand := testRand()
client := fake.NewSimpleClientset()

key, err := initKey(client, rand, 1024, "testns", "testkey")
key, certs, err := initKey(client, rand, 1024, "testns", "testkey")
if err != nil {
t.Fatalf("initKey returned err: %v", err)
}
Expand All @@ -132,12 +132,16 @@ func TestInitKey(t *testing.T) {

client.ClearActions()

key2, err := initKey(client, rand, 1024, "testns", "testkey")
key2, certs2, err := initKey(client, rand, 1024, "testns", "testkey")
if err != nil {
t.Fatalf("initKey returned err: %v", err)
}

if !reflect.DeepEqual(key, key2) {
t.Fatalf("Failed to find same key")
t.Errorf("Failed to find same key")
}

if !reflect.DeepEqual(certs, certs2) {
t.Errorf("Failed to find same certs")
}
}
27 changes: 25 additions & 2 deletions cmd/controller/server.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package main

import (
"flag"
"crypto/x509"
"io"
"log"
"net/http"
"time"

flag "github.com/spf13/pflag"
certUtil "k8s.io/client-go/util/cert"
)

var (
Expand All @@ -14,14 +17,34 @@ var (
writeTimeout = flag.Duration("write-timeout", 2*time.Minute, "HTTP response timeout.")
)

func httpserver() {
// Called on every request to /cert. Errors will be logged and return a 500.
type certProvider func() ([]*x509.Certificate, error)

func httpserver(cp certProvider) {
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/cert.pem", func(w http.ResponseWriter, r *http.Request) {
certs, err := cp()

if err != nil {
log.Printf("Error handling /cert request: %v", err)
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.WriteHeader(http.StatusInternalServerError)
io.WriteString(w, "Internal error\n")
return
}

w.Header().Set("Content-Type", "application/x-pem-file")
for _, cert := range certs {
w.Write(certUtil.EncodeCertPEM(cert))
}
})

server := http.Server{
Addr: *listenAddr,
Handler: mux,
Expand Down
97 changes: 79 additions & 18 deletions cmd/ksonnet-seal/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,34 +3,51 @@ package main
import (
"crypto/rsa"
"errors"
"flag"
goflag "flag"
"fmt"
"io"
"io/ioutil"
"os"

flag "github.com/spf13/pflag"
"k8s.io/apimachinery/pkg/runtime"
runtimeserializer "k8s.io/apimachinery/pkg/runtime/serializer"
corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
"k8s.io/client-go/pkg/api"
"k8s.io/client-go/pkg/api/v1"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/util/cert"

// Install standard API types
_ "k8s.io/client-go/kubernetes"

ssv1alpha1 "github.com/ksonnet/sealed-secrets/apis/v1alpha1"

// Register v1.Secret type
_ "k8s.io/client-go/pkg/api/install"
)

var (
// TODO: Fetch this automatically.
// TODO: Verify k8s server signature against cert in kube client config.
certFile = flag.String("cert", "", "Certificate / public key to use for encryption.")
certFile = flag.String("cert", "", "Certificate / public key to use for encryption. Overrides --controller-*")
controllerNs = flag.String("controller-namespace", api.NamespaceSystem, "Namespace of sealed-secrets controller.")
controllerName = flag.String("controller-name", "sealed-secrets-controller", "Name of sealed-secrets controller.")

// TODO: Fetch default from regular kubectl config
defaultNamespace = flag.String("namespace", api.NamespaceDefault, "Default namespace to assume for Secret.")
clientConfig clientcmd.ClientConfig
)

func readKey(r io.Reader) (*rsa.PublicKey, error) {
func init() {
// The "usual" clientcmd/kubectl flags
loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
loadingRules.DefaultClientConfig = &clientcmd.DefaultClientConfig
overrides := clientcmd.ConfigOverrides{}
kflags := clientcmd.RecommendedConfigOverrideFlags("")
flag.StringVar(&loadingRules.ExplicitPath, "kubeconfig", "", "Path to a kube config. Only required if out-of-cluster")
clientcmd.BindOverrideFlags(&overrides, flag.CommandLine, kflags)
clientConfig = clientcmd.NewInteractiveDeferredLoadingClientConfig(loadingRules, &overrides, os.Stdin)

// Standard goflags (glog in particular)
flag.CommandLine.AddGoFlagSet(goflag.CommandLine)
}

func parseKey(r io.Reader) (*rsa.PublicKey, error) {
data, err := ioutil.ReadAll(r)
if err != nil {
return nil, err
Expand Down Expand Up @@ -83,24 +100,56 @@ func prettyEncoder(codecs runtimeserializer.CodecFactory, mediaType string, gv r
return enc, nil
}

func seal(in io.Reader, out io.Writer, codecs runtimeserializer.CodecFactory) error {
secret, err := readSecret(codecs.UniversalDecoder(), in)
func openCertFile(certFile string) (io.ReadCloser, error) {
f, err := os.Open(certFile)
if err != nil {
return err
return nil, fmt.Errorf("Error reading %s: %v", certFile, err)
}
return f, nil
}

if secret.GetNamespace() == "" {
secret.SetNamespace(*defaultNamespace)
func openCertHTTP(c corev1.CoreV1Interface, namespace, name string) (io.ReadCloser, error) {
f, err := c.
Services(namespace).
ProxyGet("http", name, "", "/v1/cert.pem", nil).
Stream()
if err != nil {
return nil, fmt.Errorf("Error fetching certificate: %v", err)
}
return f, nil
}

func openCert() (io.ReadCloser, error) {
if *certFile != "" {
return openCertFile(*certFile)
}

f, err := os.Open(*certFile)
conf, err := clientConfig.ClientConfig()
if err != nil {
return fmt.Errorf("Error reading %s: %v", *certFile, err)
return nil, err
}
pubKey, err := readKey(f)
conf.AcceptContentTypes = "application/x-pem-file, */*"
restClient, err := corev1.NewForConfig(conf)
if err != nil {
return nil, err
}
return openCertHTTP(restClient, *controllerNs, *controllerName)
}

func seal(in io.Reader, out io.Writer, codecs runtimeserializer.CodecFactory, pubKey *rsa.PublicKey) error {
secret, err := readSecret(codecs.UniversalDecoder(), in)
if err != nil {
return err
}

if secret.GetNamespace() == "" {
ns, _, err := clientConfig.Namespace()
if err != nil {
return err
}
secret.SetNamespace(ns)
}

ssecret, err := ssv1alpha1.NewSealedSecret(codecs, pubKey, secret)
if err != nil {
return err
Expand All @@ -124,8 +173,20 @@ func seal(in io.Reader, out io.Writer, codecs runtimeserializer.CodecFactory) er

func main() {
flag.Parse()
goflag.CommandLine.Parse([]string{})

f, err := openCert()
if err != nil {
panic(err.Error())
}
defer f.Close()

pubKey, err := parseKey(f)
if err != nil {
panic(err.Error())
}

if err := seal(os.Stdin, os.Stdout, api.Codecs); err != nil {
if err := seal(os.Stdin, os.Stdout, api.Codecs, pubKey); err != nil {
panic(err.Error())
}
}

0 comments on commit b1d1457

Please sign in to comment.