1
0
Fork 0

cluster/admitomatic: Regexp-based admission rules

Change-Id: Ic2b1d6a952dc194c0ee2fa1673ceb91c43799308
Reviewed-on: https://gerrit.hackerspace.pl/c/hscloud/+/1723
Reviewed-by: q3k <q3k@hackerspace.pl>
master
radex 2023-10-11 00:41:48 +02:00
parent f4313b7b26
commit e36beba34c
6 changed files with 82 additions and 5 deletions

View File

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

View File

@ -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, &regexpFilter{
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
}

View File

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

View File

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

View File

@ -1,4 +1,4 @@
// Only the admitomatic instance in k0.
// Only the prodvider instance in k0.
local k0 = (import "k0.libsonnet").k0;

View File

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