diff --git a/cluster/admitomatic/config/config.proto b/cluster/admitomatic/config/config.proto index 2bc5b02e..69ca332e 100644 --- a/cluster/admitomatic/config/config.proto +++ b/cluster/admitomatic/config/config.proto @@ -37,4 +37,9 @@ message AllowDomain { // catch the root itself, ie. the above would not catch // 'foo.example.com'. string dns = 2; + // regexp enables `dns` to be treated as a domain name regexp + // and namespace as a template that can use $n regexp match references + // For example, dns: `([^.]+)\.hscloud\.ovh` and template: `personal-$1` + // will allow `hans.hscloud.ovh` ingress only for `personal-hans` namespace + bool regexp = 3; } diff --git a/cluster/admitomatic/ingress.go b/cluster/admitomatic/ingress.go index b7bdf91f..390a1600 100644 --- a/cluster/admitomatic/ingress.go +++ b/cluster/admitomatic/ingress.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "strings" + "regexp" "github.com/golang/glog" admission "k8s.io/api/admission/v1beta1" @@ -37,6 +38,9 @@ type ingressFilter struct { // allowed is a map from namespace to list of domain matchers. allowed map[string][]*domain + // allowedRegexp is a list of domain regexps and their allowed namespaces + allowedRegexp []*regexpFilter + // anythingGoesNamespaces are namespaces that are opted out of security // checks. anythingGoesNamespaces []string @@ -53,6 +57,11 @@ type domain struct { wildcard bool } +type regexpFilter struct { + namespace string + dns *regexp.Regexp +} + // match returns whether this matcher matches a given domain. func (d *domain) match(dns string) bool { if !d.wildcard { @@ -63,7 +72,7 @@ func (d *domain) match(dns string) bool { // allow adds a given (namespace, dns) pair to the filter. The dns variable is // a string that is either a simple domain name, or a wildcard like -// *.foo.example.com. An error is returned if the dns stirng could not be +// *.foo.example.com. An error is returned if the dns string could not be // parsed. func (i *ingressFilter) allow(ns, dns string) error { // If the filter is brand new, initialize it. @@ -97,6 +106,22 @@ func (i *ingressFilter) allow(ns, dns string) error { return nil } +func (i *ingressFilter) allowRegexp(ns string, dns string) error { + // Parse dns as a regexp + dnsPattern := "^" + dns + "$" + re, err := regexp.Compile(dnsPattern) + if err != nil { + return err + } + + i.allowedRegexp = append(i.allowedRegexp, ®expFilter{ + namespace: ns, + dns: re, + }) + + return nil +} + // domainAllowed returns whether a given domain is allowed to be backed by an // ingress within a given namespace. func (i *ingressFilter) domainAllowed(ns, domain string) bool { @@ -125,6 +150,24 @@ func (i *ingressFilter) domainAllowed(ns, domain string) bool { if domainFound { return false } + + // Check regexp matching + for _, filter := range i.allowedRegexp { + re := filter.dns + allowedNs := filter.namespace + + submatches := re.FindStringSubmatchIndex(domain) + if submatches == nil { + continue + } + + // Domain matched, expand allowed namespace template + expectedNs := []byte{} + expectedNs = re.ExpandString(expectedNs, allowedNs, domain, submatches) + didMatch := string(expectedNs) == ns + return didMatch + } + // No direct match found, and this domain is not restricted. Allow. return true } diff --git a/cluster/admitomatic/ingress_test.go b/cluster/admitomatic/ingress_test.go index 92b13579..8544fabf 100644 --- a/cluster/admitomatic/ingress_test.go +++ b/cluster/admitomatic/ingress_test.go @@ -39,6 +39,9 @@ func TestPatterns(t *testing.T) { if err := f.allow("borked", "*foo.example.com"); err == nil { t.Fatalf("allow(partial wildcard): wanted err, got nil") } + if err := f.allowRegexp("borked", "(.*"); err == nil { + t.Fatalf("allowRegexp(bad regexp): wanted err, got nil") + } } func TestMatch(t *testing.T) { @@ -49,6 +52,8 @@ func TestMatch(t *testing.T) { f.allow("personal-q3k", "*.k0.q3k.org") f.allow("personal-vuko", "shells.vuko.pl") f.allow("minecraft", "*.k0.q3k.org") + f.allow("hscloud-ovh-root", "hscloud.ovh") + f.allowRegexp("personal-$2", `(.*\.)?([^.]+)\.hscloud\.ovh`) for _, el := range []struct { ns string @@ -79,6 +84,16 @@ func TestMatch(t *testing.T) { {"personal-hacker", "foobar.vuko.pl", true}, // Unknown domains are fine. {"personal-hacker", "www.github.com", true}, + // Regexp matching for auto-namespaced domains + {"personal-radex", "radex.hscloud.ovh", true}, + {"personal-radex", "foo.bar.radex.hscloud.ovh", true}, + // Disallowed for other namespaces + {"personal-hacker", "radex.hscloud.ovh", false}, + {"personal-hacker", "foo.bar.radex.hscloud.ovh", false}, + {"matrix", "radex.hscloud.ovh", false}, + // Check auto-namespaced domain's root + {"hscloud-ovh-root", "hscloud.ovh", true}, + {"personal-hacker", "hscloud.ovh", false}, } { if want, got := el.expected, f.domainAllowed(el.ns, el.dns); got != want { t.Errorf("%q on %q is %v, wanted %v", el.dns, el.ns, got, want) diff --git a/cluster/admitomatic/service.go b/cluster/admitomatic/service.go index b5f76625..fc0a7d5d 100644 --- a/cluster/admitomatic/service.go +++ b/cluster/admitomatic/service.go @@ -34,8 +34,14 @@ func newService(configuration []byte) (*service, error) { if ad.Dns == "" { return nil, fmt.Errorf("config entry %d: dns must be set", i) } - if err := s.ingress.allow(ad.Namespace, ad.Dns); err != nil { - return nil, fmt.Errorf("config entry %d: %v", i, err) + if ad.Regexp { + if err := s.ingress.allowRegexp(ad.Namespace, ad.Dns); err != nil { + return nil, fmt.Errorf("config entry (regexp) %d: %v", i, err) + } + } else { + if err := s.ingress.allow(ad.Namespace, ad.Dns); err != nil { + return nil, fmt.Errorf("config entry %d: %v", i, err) + } } glog.Infof("Ingress: allowing %s in %s", ad.Dns, ad.Namespace) } diff --git a/cluster/kube/k0-prodvider.jsonnet b/cluster/kube/k0-prodvider.jsonnet index b05aae6e..fe131abe 100644 --- a/cluster/kube/k0-prodvider.jsonnet +++ b/cluster/kube/k0-prodvider.jsonnet @@ -1,4 +1,4 @@ -// Only the admitomatic instance in k0. +// Only the prodvider instance in k0. local k0 = (import "k0.libsonnet").k0; diff --git a/cluster/kube/k0.libsonnet b/cluster/kube/k0.libsonnet index 81627ac7..648ed3fa 100644 --- a/cluster/kube/k0.libsonnet +++ b/cluster/kube/k0.libsonnet @@ -358,15 +358,23 @@ local admins = import "lib/admins.libsonnet"; { namespace: "mastodon-hackerspace-qa", dns: "social-qa-2.hackerspace.pl" }, { namespace: "mastodon-hackerspace-prod", dns: "social.hackerspace.pl" }, + // auto-namespaced domains, i.e: + // USER.hscloud.ovh is allowed for personal-USER namespace + // *.USER.hscloud.ovh is allowed for personal-USER namespace + { namespace: "personal-$2", dns: "(.*\\.)?([^.]+)\\.hscloud\\.ovh", regexp: true }, + + // cluster infra { namespace: "ceph-waw3", dns: "ceph-waw3.hswaw.net" }, { namespace: "ceph-waw3", dns: "object.ceph-waw3.hswaw.net" }, { namespace: "ceph-waw3", dns: "object.ceph-eu.hswaw.net" }, { namespace: "monitoring-global-k0", dns: "*.hswaw.net" }, { namespace: "registry", dns: "*.hswaw.net" }, - // q3k's legacy namespace (pre-prodvider) + // personal namespaces { namespace: "q3k", dns: "*.q3k.org" }, { namespace: "personal-q3k", dns: "*.q3k.org" }, + { namespace: "personal-radex", dns: "hs.radex.io" }, + { namespace: "personal-radex", dns: "*.hs.radex.io" }, ], anything_goes_namespace: [