*: 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
This commit is contained in:
q3k 2020-08-01 17:15:52 +02:00
parent 1e1a4ddfc8
commit f3312ef77e
12 changed files with 268 additions and 20 deletions

View file

@ -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",

View file

@ -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",

View file

@ -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)
}
}
}

View file

@ -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
}

View file

@ -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}",
)

View file

@ -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
}

View file

@ -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")

View file

@ -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);
}

View file

@ -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
}

View file

@ -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 = [

View file

@ -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)
}

88
go/pki/locate.go Normal file
View file

@ -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")
}