From f3312ef77ed0db94e20944efc6395750072f54d5 Mon Sep 17 00:00:00 2001 From: Serge Bazanski Date: Sat, 1 Aug 2020 17:15:52 +0200 Subject: [PATCH] *: developer machine HSPKI credentials In addition to k8s certificates, prodaccess now issues HSPKI certificates, with DN=$username.sso.hswaw.net. These are installed into XDG_CONFIG_HOME (or os equiv). //go/pki will now automatically attempt to load these certificates. This means you can now run any pki-dependant tool with -hspki_disable, and with automatic mTLS! Change-Id: I5b28e193e7c968d621bab0d42aabd6f0510fed6d --- cluster/kube/lib/prodvider.libsonnet | 2 +- cluster/prodaccess/BUILD.bazel | 2 + cluster/prodaccess/hspki.go | 33 +++++++++ cluster/prodaccess/prodaccess.go | 3 + cluster/prodvider/BUILD.bazel | 4 +- cluster/prodvider/hspki.go | 99 +++++++++++++++++++++++++ cluster/prodvider/proto/BUILD.bazel | 1 + cluster/prodvider/proto/prodvider.proto | 8 ++ cluster/prodvider/service.go | 12 ++- go/pki/BUILD.bazel | 5 +- go/pki/grpc.go | 31 ++++---- go/pki/locate.go | 88 ++++++++++++++++++++++ 12 files changed, 268 insertions(+), 20 deletions(-) create mode 100644 cluster/prodaccess/hspki.go create mode 100644 cluster/prodvider/hspki.go create mode 100644 go/pki/locate.go diff --git a/cluster/kube/lib/prodvider.libsonnet b/cluster/kube/lib/prodvider.libsonnet index 5805993d..3890f5ad 100644 --- a/cluster/kube/lib/prodvider.libsonnet +++ b/cluster/kube/lib/prodvider.libsonnet @@ -9,7 +9,7 @@ local kube = import "../../../kube/kube.libsonnet"; cfg:: { namespace: "prodvider", - image: "registry.k0.hswaw.net/cluster/prodvider:1567256363-71a21c769369d013972d8dd0a71b83bee3e6848e", + image: "registry.k0.hswaw.net/q3k/prodvider:1596294699-1e1a4ddfc88008465aa38e4c037d2ba5d6ec8130", apiEndpoint: error "API endpoint must be set", diff --git a/cluster/prodaccess/BUILD.bazel b/cluster/prodaccess/BUILD.bazel index 5124ffcb..6c720823 100644 --- a/cluster/prodaccess/BUILD.bazel +++ b/cluster/prodaccess/BUILD.bazel @@ -3,6 +3,7 @@ load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library") go_library( name = "go_default_library", srcs = [ + "hspki.go", "kubernetes.go", "prodaccess.go", ], @@ -11,6 +12,7 @@ go_library( deps = [ "//cluster/certs:go_default_library", "//cluster/prodvider/proto:go_default_library", + "//go/pki:go_default_library", "@com_github_golang_glog//:go_default_library", "@org_golang_google_grpc//:go_default_library", "@org_golang_google_grpc//credentials:go_default_library", diff --git a/cluster/prodaccess/hspki.go b/cluster/prodaccess/hspki.go new file mode 100644 index 00000000..2fcfaf05 --- /dev/null +++ b/cluster/prodaccess/hspki.go @@ -0,0 +1,33 @@ +package main + +import ( + "io/ioutil" + "os" + + "github.com/golang/glog" + + pb "code.hackerspace.pl/hscloud/cluster/prodvider/proto" + "code.hackerspace.pl/hscloud/go/pki" +) + +func useHSPKIKeys(keys *pb.HSPKIKeys) { + path, err := pki.DeveloperCredentialsLocation() + err = os.MkdirAll(path, 0700) + if err != nil { + glog.Exitf("mkdir %q: %v", path, err) + } + + for _, el := range []struct { + target string + data []byte + }{ + {path + "/ca.crt", keys.Ca}, + {path + "/tls.crt", keys.Cert}, + {path + "/tls.key", keys.Key}, + } { + err := ioutil.WriteFile(el.target, el.data, 400) + if err != nil { + glog.Exitf("Failed to write %q: %v", el.target, err) + } + } +} diff --git a/cluster/prodaccess/prodaccess.go b/cluster/prodaccess/prodaccess.go index e0e8ec2b..1153babf 100644 --- a/cluster/prodaccess/prodaccess.go +++ b/cluster/prodaccess/prodaccess.go @@ -99,6 +99,9 @@ func authenticate(ctx context.Context, prodvider pb.ProdviderClient) bool { } useKubernetesKeys(res.KubernetesKeys) + fmt.Printf("-> Kubernetes credentials installed\n") + useHSPKIKeys(res.HspkiKeys) + fmt.Printf("-> HSPKI credentials installed\n") return true } diff --git a/cluster/prodvider/BUILD.bazel b/cluster/prodvider/BUILD.bazel index 14690b73..c15ab668 100644 --- a/cluster/prodvider/BUILD.bazel +++ b/cluster/prodvider/BUILD.bazel @@ -5,6 +5,7 @@ go_library( name = "go_default_library", srcs = [ "certs.go", + "hspki.go", "kubernetes.go", "main.go", "service.go", @@ -15,6 +16,7 @@ go_library( "//cluster/prodvider/proto:go_default_library", "@com_github_cloudflare_cfssl//config:go_default_library", "@com_github_cloudflare_cfssl//csr:go_default_library", + "@com_github_cloudflare_cfssl//helpers:go_default_library", "@com_github_cloudflare_cfssl//signer:go_default_library", "@com_github_cloudflare_cfssl//signer/local:go_default_library", "@com_github_golang_glog//:go_default_library", @@ -59,6 +61,6 @@ container_push( image = ":runtime", format = "Docker", registry = "registry.k0.hswaw.net", - repository = "cluster/prodvider", + repository = "q3k/prodvider", tag = "{BUILD_TIMESTAMP}-{STABLE_GIT_COMMIT}", ) diff --git a/cluster/prodvider/hspki.go b/cluster/prodvider/hspki.go new file mode 100644 index 00000000..243a4241 --- /dev/null +++ b/cluster/prodvider/hspki.go @@ -0,0 +1,99 @@ +package main + +import ( + "encoding/pem" + "fmt" + "time" + + "github.com/cloudflare/cfssl/config" + "github.com/cloudflare/cfssl/csr" + "github.com/cloudflare/cfssl/helpers" + "github.com/cloudflare/cfssl/signer" + "github.com/cloudflare/cfssl/signer/local" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + pb "code.hackerspace.pl/hscloud/cluster/prodvider/proto" +) + +func (p *prodvider) hspkiSigner() (*local.Signer, error) { + policy := &config.Signing{ + Profiles: map[string]*config.SigningProfile{ + "client": &config.SigningProfile{ + Usage: []string{"signing", "key encipherment", "client auth"}, + ExpiryString: "30d", + }, + }, + Default: config.DefaultConfig(), + } + + secret, err := p.k8s.CoreV1().Secrets("cert-manager").Get("pki-selfsigned-cert", metav1.GetOptions{}) + if err != nil { + return nil, fmt.Errorf("hspki secret get failed: %w", err) + } + + parsedCa, err := helpers.ParseCertificatePEM(secret.Data["tls.crt"]) + if err != nil { + return nil, fmt.Errorf("when parsing tls.crt: %w", err) + } + + priv, err := helpers.ParsePrivateKeyPEMWithPassword(secret.Data["tls.key"], nil) + if err != nil { + return nil, fmt.Errorf("when parsing tls.key: %w", err) + } + + return local.NewSigner(priv, parsedCa, signer.DefaultSigAlgo(priv), policy) +} + +func (p *prodvider) hspkiCreds(username string) (*pb.HSPKIKeys, error) { + principal := fmt.Sprintf("%s.sso.hswaw.net", username) + + s, err := p.hspkiSigner() + if err != nil { + return nil, fmt.Errorf("hspkiSigner: %w", err) + } + + signerCert, _ := s.Certificate("", "") + req := &csr.CertificateRequest{ + CN: principal, + KeyRequest: &csr.BasicKeyRequest{ + A: "rsa", + S: 4096, + }, + Names: []csr.Name{ + { + O: "prodvider", + OU: fmt.Sprintf("Prodvider HSPKI Cert for %s", username), + }, + }, + } + + g := &csr.Generator{ + Validator: func(req *csr.CertificateRequest) error { return nil }, + } + + csrPEM, keyPEM, err := g.ProcessRequest(req) + if err != nil { + return nil, fmt.Errorf("when making CSR: %w", err) + } + + signReq := signer.SignRequest{ + Hosts: []string{}, + Request: string(csrPEM), + Profile: "client", + NotAfter: time.Now().Add(9 * time.Hour), + } + + certPEM, err := s.Sign(signReq) + if err != nil { + return nil, fmt.Errorf("when issuing certificate: %w", err) + } + + caPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: signerCert.Raw}) + + return &pb.HSPKIKeys{ + Ca: caPEM, + Cert: certPEM, + Key: keyPEM, + Principal: principal, + }, nil +} diff --git a/cluster/prodvider/proto/BUILD.bazel b/cluster/prodvider/proto/BUILD.bazel index 2efd457a..0817dfb0 100644 --- a/cluster/prodvider/proto/BUILD.bazel +++ b/cluster/prodvider/proto/BUILD.bazel @@ -1,3 +1,4 @@ +load("@rules_proto//proto:defs.bzl", "proto_library") load("@io_bazel_rules_go//go:def.bzl", "go_library") load("@io_bazel_rules_go//proto:def.bzl", "go_proto_library") diff --git a/cluster/prodvider/proto/prodvider.proto b/cluster/prodvider/proto/prodvider.proto index 1ae2798b..ba5bf9db 100644 --- a/cluster/prodvider/proto/prodvider.proto +++ b/cluster/prodvider/proto/prodvider.proto @@ -15,6 +15,7 @@ message AuthenticateResponse { } Result result = 1; KubernetesKeys kubernetes_keys = 2; + HSPKIKeys hspki_keys = 3; } message KubernetesKeys { @@ -24,6 +25,13 @@ message KubernetesKeys { bytes key = 4; } +message HSPKIKeys { + bytes ca = 1; + bytes cert = 2; + bytes key = 3; + string principal = 4; +} + service Prodvider { rpc Authenticate(AuthenticateRequest) returns (AuthenticateResponse); } diff --git a/cluster/prodvider/service.go b/cluster/prodvider/service.go index 04098841..17dfe6e7 100644 --- a/cluster/prodvider/service.go +++ b/cluster/prodvider/service.go @@ -69,14 +69,22 @@ func (p *prodvider) Authenticate(ctx context.Context, req *pb.AuthenticateReques return nil, status.Error(codes.Unavailable, "could not set up objects in Kubernetes") } - keys, err := p.kubernetesCreds(username) + kubernetesKeys, err := p.kubernetesCreds(username) if err != nil { glog.Errorf("kubernetesCreds(%q): %v", username, err) return nil, status.Error(codes.Unavailable, "could not generate k8s keys") } + + hspkiKeys, err := p.hspkiCreds(username) + if err != nil { + glog.Errorf("hspkiCreds(%q): %v", username, err) + return nil, status.Error(codes.Unavailable, "could not generate hspki keys") + } + return &pb.AuthenticateResponse{ Result: pb.AuthenticateResponse_RESULT_AUTHENTICATED, - KubernetesKeys: keys, + KubernetesKeys: kubernetesKeys, + HspkiKeys: hspkiKeys, }, nil } diff --git a/go/pki/BUILD.bazel b/go/pki/BUILD.bazel index 5bc75229..f2eae41a 100644 --- a/go/pki/BUILD.bazel +++ b/go/pki/BUILD.bazel @@ -2,7 +2,10 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") go_library( name = "go_default_library", - srcs = ["grpc.go"], + srcs = [ + "grpc.go", + "locate.go", + ], importpath = "code.hackerspace.pl/hscloud/go/pki", visibility = ["//visibility:public"], deps = [ diff --git a/go/pki/grpc.go b/go/pki/grpc.go index 1720ad80..313f4a93 100644 --- a/go/pki/grpc.go +++ b/go/pki/grpc.go @@ -20,7 +20,6 @@ import ( "crypto/x509" "flag" "fmt" - "io/ioutil" "strings" "github.com/golang/glog" @@ -210,18 +209,19 @@ func WithServerHSPKI() []grpc.ServerOption { return []grpc.ServerOption{} } - serverCert, err := tls.LoadX509KeyPair(flagCertificatePath, flagKeyPath) + loc, err := loadCredentials() + if err != nil { + glog.Exitf("WithServerHSPKI: loadCredentials: %v", err) + } + + serverCert, err := tls.X509KeyPair(loc.cert, loc.key) if err != nil { glog.Exitf("WithServerHSPKI: cannot load service certificate/key: %v", err) } certPool := x509.NewCertPool() - ca, err := ioutil.ReadFile(flagCAPath) - if err != nil { - glog.Exitf("WithServerHSPKI: cannot load CA certificate: %v", err) - } - if ok := certPool.AppendCertsFromPEM(ca); !ok { - glog.Exitf("WithServerHSPKI: cannot use CA certificate: %v", err) + if ok := certPool.AppendCertsFromPEM(loc.ca); !ok { + glog.Exitf("WithServerHSPKI: cannot use CA certificate") } creds := grpc.Creds(credentials.NewTLS(&tls.Config{ @@ -243,16 +243,17 @@ func WithClientHSPKI() grpc.DialOption { return grpc.WithInsecure() } - certPool := x509.NewCertPool() - ca, err := ioutil.ReadFile(flagCAPath) + loc, err := loadCredentials() if err != nil { - glog.Exitf("WithClientHSPKI: cannot load CA certificate: %v", err) - } - if ok := certPool.AppendCertsFromPEM(ca); !ok { - glog.Exitf("WithClientHSPKI: cannot use CA certificate: %v", err) + glog.Exitf("WithServerHSPKI: loadCredentials: %v", err) } - clientCert, err := tls.LoadX509KeyPair(flagCertificatePath, flagKeyPath) + certPool := x509.NewCertPool() + if ok := certPool.AppendCertsFromPEM(loc.ca); !ok { + glog.Exitf("WithServerHSPKI: cannot use CA certificate") + } + + clientCert, err := tls.X509KeyPair(loc.cert, loc.key) if err != nil { glog.Exitf("WithClientHSPKI: cannot load service certificate/key: %v", err) } diff --git a/go/pki/locate.go b/go/pki/locate.go new file mode 100644 index 00000000..e48e013b --- /dev/null +++ b/go/pki/locate.go @@ -0,0 +1,88 @@ +package pki + +import ( + "fmt" + "io/ioutil" + "os" + + "github.com/golang/glog" +) + +// DeveloperCredentialsLocation returns the path containing HSPKI credentials +// on developer machines. These are provisioned by //cluster/prodaccess, and +// are used if available. +func DeveloperCredentialsLocation() (string, error) { + cfgDir, err := os.UserConfigDir() + if err != nil { + glog.Exitf("UserConfigDir: %w", err) + } + + return fmt.Sprintf("%s/hspki", cfgDir), nil +} + +type creds struct { + ca []byte + cert []byte + key []byte +} + +func loadDeveloperCredentials() (*creds, error) { + path, err := DeveloperCredentialsLocation() + if err != nil { + return nil, fmt.Errorf("DeveloperCredentialsLocation: %w") + } + + c := creds{} + for _, el := range []struct { + target *[]byte + path string + }{ + {&c.ca, path + "/" + "ca.crt"}, + {&c.cert, path + "/" + "tls.crt"}, + {&c.key, path + "/" + "tls.key"}, + } { + data, err := ioutil.ReadFile(el.path) + if err != nil { + return nil, fmt.Errorf("ReadFile(%q): %w", el.path, err) + } + *el.target = data + } + + return &c, nil +} + +func loadFlagCredentials() (*creds, error) { + c := creds{} + for _, el := range []struct { + target *[]byte + path string + }{ + {&c.ca, flagCAPath}, + {&c.cert, flagCertificatePath}, + {&c.key, flagKeyPath}, + } { + data, err := ioutil.ReadFile(el.path) + if err != nil { + return nil, fmt.Errorf("ReadFile(%q): %w", el.path, err) + } + *el.target = data + } + + return &c, nil +} + +func loadCredentials() (*creds, error) { + dev, err := loadDeveloperCredentials() + if err == nil { + return dev, nil + } + glog.Warningf("Could not load developer PKI credentials: %v", err) + + fl, err := loadFlagCredentials() + if err == nil { + return fl, err + } + glog.Warningf("Could not load flag-defined PKI credentials: %v", err) + + return nil, fmt.Errorf("could not load any credentials") +}