smsgw: productionize, implement kube/mirko
This productionizes smsgw.
We also add some jsonnet machinery to provide a unified service for Go
micro/mirkoservices.
This machinery provides all the nice stuff:
- a deployment
- a service for all your types of pots
- TLS certificates for HSPKI
We also update and test hspki for a new name scheme.
Change-Id: I292d00f858144903cbc8fe0c1c26eb1180d636bc
diff --git a/go/pki/BUILD.bazel b/go/pki/BUILD.bazel
index 0d3544f..5bc7522 100644
--- a/go/pki/BUILD.bazel
+++ b/go/pki/BUILD.bazel
@@ -1,4 +1,4 @@
-load("@io_bazel_rules_go//go:def.bzl", "go_library")
+load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
go_library(
name = "go_default_library",
@@ -15,3 +15,10 @@
"@org_golang_x_net//trace:go_default_library",
],
)
+
+go_test(
+ name = "go_default_test",
+ srcs = ["grpc_test.go"],
+ embed = [":go_default_library"],
+ deps = ["@com_github_go_test_deep//:go_default_library"],
+)
diff --git a/go/pki/README.md b/go/pki/README.md
index b84c32d..f44a970 100644
--- a/go/pki/README.md
+++ b/go/pki/README.md
@@ -24,36 +24,49 @@
All certs for mutual auth have the following CN/SAN format:
- <job>.<principal>.<realm>
+ <job>.<principal>.svc.<cluster-short>.<realm>
+ or
+ <principal>.person.<realm>
+ or
+ <principal>.external.<realm>
-For example, if principal maps into a 'group' and job into a 'user':
+Where in adition we define `<cluster>` as being `<realm>` plus its next left-side member.
- arista-proxy-dcr01u23.cluster-management-prod.c.example.com
+For example, for kubernetes jobs:
- job = arista-proxy-dcr01u23
- principal = cluster-management-prod
- realm = c.example.com
+ foo.bar.svc.k0.hswaw.net
+
+ job = foo
+ principal = bar.svc
+ cluster = k0.hswaw.net
+ realm = hswaw.net
+
+Where foo is the name of a kubernets service, bar is the name of the namespace its in, and
+k0.hswaw.net is the cluster running them.
+
+For people and external services:
+
+ q3k.person.hswaw.net
+
+ job =
+ principal = q3k
+ cluster = person.hswaw.net
+ realm = hswaw.net
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 Principal is any name that carries significance for an authentication principal,
+ie. a unit that gives information about an identity of an element. In case of kubernetes
+it's a namespace (as we split authentication/authorization into namespaces). In the case of external
+services and people it's the name of the service or person.
-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 Job is a name that makes the Principal more specific, if possible. If set, the Principal
+can be treated as a group of Jobs.
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?
-----------------------------------------------
@@ -84,8 +97,10 @@
Once linked into your program, the following flags will be automatically present:
- -hspki_realm string
+ -hspki_cluster string
PKI realm (default "svc.cluster.local")
+ -hspki_realm string
+ PKI realm (default "cluster.local")
-hspki_tls_ca_path string
Path to PKI CA certificate (default "pki/ca.pem")
-hspki_tls_certificate_path string
diff --git a/go/pki/grpc.go b/go/pki/grpc.go
index 6d8f173..1720ad8 100644
--- a/go/pki/grpc.go
+++ b/go/pki/grpc.go
@@ -36,6 +36,7 @@
flagCAPath string
flagCertificatePath string
flagKeyPath string
+ flagPKICluster string
flagPKIRealm string
flagPKIDisable bool
@@ -53,7 +54,8 @@
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")
+ flag.StringVar(&flagPKICluster, "hspki_cluster", "local.hswaw.net", "FQDN of cluster on which this service runs")
+ flag.StringVar(&flagPKIRealm, "hspki_realm", "hswaw.net", "Cluster realm (top level from which we accept foreign cluster certs)")
flag.BoolVar(&flagPKIDisable, "hspki_disable", false, "Disable PKI entirely (insecure!)")
}
@@ -81,14 +83,39 @@
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")
+
+ inRealm := strings.TrimSuffix(name, "."+flagPKIRealm)
+
+ special := []string{"person", "external"}
+
+ for _, s := range special {
+ // Special case for people running jobs from workstations, or for non-cluster services.
+ if strings.HasSuffix(inRealm, "."+s) {
+ asPerson := strings.TrimSuffix(inRealm, "."+s)
+ parts := strings.Split(asPerson, ".")
+ if len(parts) != 1 {
+ return nil, fmt.Errorf("invalid person fqdn")
+ }
+ return &ClientInfo{
+ Cluster: fmt.Sprintf("%s.%s", s, flagPKIRealm),
+ Principal: parts[0],
+ Job: "",
+ }, nil
+ }
}
+
+ parts := strings.Split(inRealm, ".")
+ if len(parts) != 4 {
+ return nil, fmt.Errorf("invalid job/principal format for in-cluster")
+ }
+ if parts[2] != "svc" {
+ return nil, fmt.Errorf("can only refer to services within cluster")
+ }
+ clusterShort := parts[3]
+
return &ClientInfo{
- Realm: flagPKIRealm,
- Principal: parts[1],
+ Cluster: fmt.Sprintf("%s.%s", clusterShort, flagPKIRealm),
+ Principal: fmt.Sprintf("%s.svc", parts[1]),
Job: parts[0],
}, nil
}
@@ -137,15 +164,24 @@
// ClientInfo contains information about the HSPKI authentication data of the
// gRPC client that has made the request.
type ClientInfo struct {
- Realm string
+ Cluster string
Principal string
Job string
}
// String returns a human-readable representation of the ClientInfo in the
-// form "job=foo, principal=bar, realm=baz".
+// form "job=foo, principal=bar.svc, cluster=baz.hswaw.net".
func (c *ClientInfo) String() string {
- return fmt.Sprintf("job=%q, principal=%q, realm=%q", c.Job, c.Principal, c.Realm)
+ return fmt.Sprintf("job=%q, principal=%q, cluster=%q", c.Job, c.Principal, c.Cluster)
+}
+
+// Person returns a reference to a person's ID if the ClientInfo describes a person.
+// Otherwise, it returns an empty string.
+func (c *ClientInfo) Person() string {
+ if c.Cluster != fmt.Sprintf("person.%s", flagPKIRealm) {
+ return ""
+ }
+ return c.Principal
}
// ClientInfoFromContext returns ClientInfo from a gRPC service context.
diff --git a/go/pki/grpc_test.go b/go/pki/grpc_test.go
new file mode 100644
index 0000000..9ce2a8c
--- /dev/null
+++ b/go/pki/grpc_test.go
@@ -0,0 +1,69 @@
+package pki
+
+import (
+ "testing"
+
+ "github.com/go-test/deep"
+)
+
+func TestParseClient(t *testing.T) {
+ flagPKIRealm = "hswaw.net"
+
+ tests := []struct {
+ name string
+ want *ClientInfo
+ }{
+ // Local cluster
+ {"foo.bar.svc.k0.hswaw.net", &ClientInfo{Cluster: "k0.hswaw.net", Principal: "bar.svc", Job: "foo"}},
+ {"foo.bar.k0.hswaw.net", nil},
+
+ // Foreign cluster
+ {"foo.bar.svc.k1.hswaw.net", &ClientInfo{Cluster: "k1.hswaw.net", Principal: "bar.svc", Job: "foo"}},
+ {"foo.bar.k1.hswaw.net", nil},
+
+ // Human admins (admins, as we know, don't have a real job)
+ {"q3k.person.hswaw.net", &ClientInfo{Cluster: "person.hswaw.net", Principal: "q3k", Job: ""}},
+
+ // External services
+ {"kasownik.external.hswaw.net", &ClientInfo{Cluster: "external.hswaw.net", Principal: "kasownik", Job: ""}},
+
+ // Broken.
+ {"foo.hswaw.net", nil},
+ {"ldap.hackerspace.pl", nil},
+ {"", nil},
+ {"..what..plz...don.t.hack.me.hswaw.net", nil},
+ }
+
+ for i, te := range tests {
+ res, err := parseClientName(te.name)
+ if err != nil {
+ if te.want != nil {
+ t.Errorf("#%d: wanted result, got err %v", i, err)
+ }
+ continue
+ }
+
+ if te.want == nil {
+ t.Errorf("#%d: wanted err, got %+v", i, res)
+ continue
+ }
+
+ if diff := deep.Equal(*te.want, *res); diff != nil {
+ t.Errorf("#%d: res diff: %v", i, diff)
+ continue
+ }
+ }
+}
+
+func TestCheckPerson(t *testing.T) {
+ flagPKIRealm = "hswaw.net"
+
+ res, err := parseClientName("q3k.person.hswaw.net")
+ if err != nil {
+ t.Fatalf("err: %v", err)
+ }
+
+ if want, got := "q3k", res.Person(); want != got {
+ t.Fatalf("wanted %q, got %q", want, got)
+ }
+}