commit f02cd7707be4ac7fdabcdbcb0e45faccf30ce37b Author: Sergiusz Bazanski Date: Tue Aug 28 15:25:33 2018 +0100 Initial commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..422c3e7 --- /dev/null +++ b/README.md @@ -0,0 +1,96 @@ +HSCloud PKI +=========== + +a.k.a. API tokens are so 2012 + +Introduction +------------ + +The HSCloud Public Key Infrastructure system is a lightweight specification on how microservices within the HSCloud ecosystem authenticate themselves. + +The driving force behind this being standardized is to make it very easy for developers to write new microservices and other tools that can mutually authenticate themselves without having to use public TLS certificates, API tokens or passwords. + +Each microservice or tool has a key/certificate pair that it uses to both serve incoming requests and to use as a client certificate when performing outgoing requests. + +We currently support gRPC as a first-class transport. Other transports (HTTPS for debug pages, HTTPS for JSON(-RPC)) are not yet implemented. + +Where do I get certificates from? +--------------------------------- + +The distribution of HSPKI certificates to production services is currently being designed (and will likely be based on Hashicorp Vault or a similar NIH tool). For development purposes, the `gen.sh` script in `dev-certs/` can be used to generate a temporary CA, service keypair and developer keypair. + +Concepts +-------- + +All certs for mutual auth have the following CN/SAN format: + + .. + +For example, if principal maps into a 'group' and job into a 'user': + + arista-proxy-dcr01u23.cluster-management-prod.c.example.com + + job = arista-proxy-dcr01u23 + principal = cluster-management-prod + realm = c.example.com + +The Realm is a DNS name that is global to all jobs that need mutual authentication. + +The Principal is any name that carries significance for logical grouping of jobs. +It can, but doesn't need to, group jobs by similar permissions. + +The Job is any name that identifies uniquely (within the principal) a security +endpoint that describes a single security policy for a gRPC endpoint. + +The entire CN should be DNS resolvable into an IP address that would respond to +gRPC requests on port 42000 (with a server TLS certificate that represents this CN) if the +job represents a service. + +This maps nicely to the Kubernetes Cluster DNS format if you set `realm` to `svc.cluster.local`. +Then, `principal` maps to a Kubernetes namespace, and `job` maps into a Kubernetes service. + + arista-proxy-dcr01u23.infrastructure-prod.svc.cluster.local + + job/service = arista-proxy-dcr01u23 + principal/namespace = infrastructure-prod + realm = svc.cluster.local + +ACL, or How do I restrict access to my service? +----------------------------------------------- + +Currently you'll have to manually check the PKI information via your language's library and reject unauthorized access within your handler. A unified ACL system with an external RBAC store is currently being designed. + +Go Library +========== + +We provide a Go library that all microservices should use to interact with HSPKI. + +Usage with gRPC +--------------- + +In lieu of a godoc (soon (TM)), here's a quick usage example: + + + import ( + "code.hackerspace.pl/q3k/hspki" + ) + ... + g := grpc.NewServer(hspki.WithServerHSPKI()...) + pb.RegiserXXXServer(g, service) + ... + +Flags +----- + +Once linked into your program, the following flags will be automatically present: + + -hspki_realm string + PKI realm (default "svc.cluster.local") + -hspki_tls_ca_path string + Path to PKI CA certificate (default "pki/ca.pem") + -hspki_tls_certificate_path string + Path to PKI service certificate (default "pki/service.pem") + -hspki_tls_key_path string + Path to PKI service private key (default "pki/service-key.pem") + +These should be set accordingly in your development environment. diff --git a/dev-certs/.gitignore b/dev-certs/.gitignore new file mode 100644 index 0000000..e24607d --- /dev/null +++ b/dev-certs/.gitignore @@ -0,0 +1,2 @@ +*csr +*pem diff --git a/dev-certs/ca_config.json b/dev-certs/ca_config.json new file mode 100644 index 0000000..113a08f --- /dev/null +++ b/dev-certs/ca_config.json @@ -0,0 +1,13 @@ +{ + "signing": { + "default": { + "expiry": "8760h" + }, + "profiles": { + "test": { + "usages": ["signing", "key encipherment", "server auth", "client auth"], + "expiry": "8760h" + } + } + } +} diff --git a/dev-certs/ca_csr.json b/dev-certs/ca_csr.json new file mode 100644 index 0000000..b24c638 --- /dev/null +++ b/dev-certs/ca_csr.json @@ -0,0 +1,11 @@ +{ + "names": [ + { + "C": "US", + "L": "San Francisco", + "O": "Internet Widgets, Inc.", + "OU": "WWW", + "ST": "California" + } + ] +} diff --git a/dev-certs/clean.sh b/dev-certs/clean.sh new file mode 100755 index 0000000..490223d --- /dev/null +++ b/dev-certs/clean.sh @@ -0,0 +1,6 @@ +#!/bin/sh + +set -e -x + +rm *pem +rm *csr diff --git a/dev-certs/client_csr.json b/dev-certs/client_csr.json new file mode 100644 index 0000000..26fc041 --- /dev/null +++ b/dev-certs/client_csr.json @@ -0,0 +1,12 @@ +{ + "CN": "developer.humans.svc.cluster.local", + "names": [ + { + "C": "US", + "L": "San Francisco", + "O": "Internet Widgets, Inc.", + "OU": "WWW", + "ST": "California" + } + ] +} diff --git a/dev-certs/gen.sh b/dev-certs/gen.sh new file mode 100755 index 0000000..e09e9f3 --- /dev/null +++ b/dev-certs/gen.sh @@ -0,0 +1,7 @@ +#!/bin/sh + +set -e -x + +test -f ca.pem || ( cfssl gencert -initca ca_csr.json | cfssljson -bare ca ) +test -f service.pem || ( cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca_config.json -profile=test service_csr.json | cfssljson -bare service ) +test -f client.pem || ( cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca_config.json -profile=test client_csr.json | cfssljson -bare client ) diff --git a/dev-certs/service_csr.json b/dev-certs/service_csr.json new file mode 100644 index 0000000..72c910e --- /dev/null +++ b/dev-certs/service_csr.json @@ -0,0 +1,12 @@ +{ + "CN": "test.arista-proxy.svc.cluster.local", + "names": [ + { + "C": "US", + "L": "San Francisco", + "O": "Internet Widgets, Inc.", + "OU": "WWW", + "ST": "California" + } + ] +} diff --git a/grpc.go b/grpc.go new file mode 100644 index 0000000..3cace95 --- /dev/null +++ b/grpc.go @@ -0,0 +1,194 @@ +package hspki + +// Copyright 2018 Sergiusz Bazanski +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +import ( + "context" + "crypto/tls" + "crypto/x509" + "flag" + "fmt" + "io/ioutil" + "strings" + + "github.com/golang/glog" + "golang.org/x/net/trace" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/peer" + "google.golang.org/grpc/status" +) + +var ( + flagCAPath string + flagCertificatePath string + flagKeyPath string + flagPKIRealm string + + // Enable logging HSPKI info into traces + Trace = true + // Enable logging HSPKI info into glog + Log = false +) + +const ( + ctxKeyClientInfo = "hspki-client-info" +) + +func init() { + flag.StringVar(&flagCAPath, "hspki_tls_ca_path", "pki/ca.pem", "Path to PKI CA certificate") + flag.StringVar(&flagCertificatePath, "hspki_tls_certificate_path", "pki/service.pem", "Path to PKI service certificate") + flag.StringVar(&flagKeyPath, "hspki_tls_key_path", "pki/service-key.pem", "Path to PKI service private key") + flag.StringVar(&flagPKIRealm, "hspki_realm", "svc.cluster.local", "PKI realm") +} + +func maybeTrace(ctx context.Context, f string, args ...interface{}) { + if Log { + glog.Infof(f, args...) + } + + if !Trace { + return + } + + tr, ok := trace.FromContext(ctx) + if !ok { + if !Log { + fmtd := fmt.Sprintf(f, args...) + glog.Info("[no trace] %v", fmtd) + } + return + } + tr.LazyPrintf(f, args...) +} + +func parseClientName(name string) (*ClientInfo, error) { + if !strings.HasSuffix(name, "."+flagPKIRealm) { + return nil, fmt.Errorf("invalid realm") + } + service := strings.TrimSuffix(name, "."+flagPKIRealm) + parts := strings.Split(service, ".") + if len(parts) != 2 { + return nil, fmt.Errorf("invalid job/principal format") + } + return &ClientInfo{ + Realm: flagPKIRealm, + Principal: parts[1], + Job: parts[0], + }, nil +} + +func withPKIInfo(ctx context.Context, c *ClientInfo) context.Context { + maybeTrace(ctx, "HSPKI: Applying ClientInfo: %s", c.String()) + return context.WithValue(ctx, ctxKeyClientInfo, c) +} + +func grpcInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) { + peer, ok := peer.FromContext(ctx) + if !ok { + maybeTrace(ctx, "HSPKI: Could not establish identity of peer.") + return nil, status.Errorf(codes.PermissionDenied, "no peer info") + } + + authInfo, ok := peer.AuthInfo.(credentials.TLSInfo) + if !ok { + maybeTrace(ctx, "HSPKI: Could not establish TLS identity of peer.") + return nil, status.Errorf(codes.PermissionDenied, "no TLS certificate presented") + } + + chains := authInfo.State.VerifiedChains + if len(chains) != 1 { + maybeTrace(ctx, "HSPKI: No trusted chains found.") + return nil, status.Errorf(codes.PermissionDenied, "no trusted TLS certificate presented") + } + + chain := chains[0] + + certDNs := make([]string, len(chain)) + for i, cert := range chain { + certDNs[i] = cert.Subject.String() + } + maybeTrace(ctx, "HSPKI: Trust chain: %s", strings.Join(certDNs, ", ")) + + clientInfo, err := parseClientName(chain[0].Subject.CommonName) + if err != nil { + maybeTrace(ctx, "HSPKI: Invalid CN %q: %v", chain[0].Subject.CommonName, err) + return nil, status.Errorf(codes.PermissionDenied, "invalid TLS CN format") + } + ctx = withPKIInfo(ctx, clientInfo) + return handler(ctx, req) +} + +// ClientInfo contains information about the HSPKI authentication data of the +// gRPC client that has made the request. +type ClientInfo struct { + Realm string + Principal string + Job string +} + +// String returns a human-readable representation of the ClientInfo in the +// form "job=foo, principal=bar, realm=baz". +func (c *ClientInfo) String() string { + return fmt.Sprintf("job=%q, principal=%q, realm=%q", c.Job, c.Principal, c.Realm) +} + +// ClientInfoFromContext returns ClientInfo from a gRPC service context. +func ClientInfoFromContext(ctx context.Context) *ClientInfo { + v := ctx.Value(ctxKeyClientInfo) + if v == nil { + return nil + } + ci, ok := v.(*ClientInfo) + if !ok { + return nil + } + return ci +} + +// WithServerHSPKI is a grpc.ServerOptions array that ensures that the gRPC server: +// - runs with HSPKI TLS Service Certificate +// - rejects all non_HSPKI compatible requests +// - injects ClientInfo into the service context, which can be later retrieved +// using ClientInfoFromContext +func WithServerHSPKI() []grpc.ServerOption { + if !flag.Parsed() { + glog.Exitf("WithServerHSPKI called before flag.Parse!") + } + serverCert, err := tls.LoadX509KeyPair(flagCertificatePath, flagKeyPath) + 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) + } + + creds := grpc.Creds(credentials.NewTLS(&tls.Config{ + ClientAuth: tls.RequireAndVerifyClientCert, + Certificates: []tls.Certificate{serverCert}, + ClientCAs: certPool, + })) + + interceptor := grpc.UnaryInterceptor(grpcInterceptor) + + return []grpc.ServerOption{creds, interceptor} +}