mirror of
https://gerrit.hackerspace.pl/hscloud
synced 2025-01-18 18:03:54 +00:00
cluster/admitomatic: Regexp-based admission rules
Change-Id: Ic2b1d6a952dc194c0ee2fa1673ceb91c43799308 Reviewed-on: https://gerrit.hackerspace.pl/c/hscloud/+/1723 Reviewed-by: q3k <q3k@hackerspace.pl>
This commit is contained in:
parent
f4313b7b26
commit
e36beba34c
6 changed files with 82 additions and 5 deletions
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Only the admitomatic instance in k0.
|
||||
// Only the prodvider instance in k0.
|
||||
|
||||
local k0 = (import "k0.libsonnet").k0;
|
||||
|
||||
|
|
|
@ -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: [
|
||||
|
|
Loading…
Reference in a new issue