commit 4d56d8ea21032855623d0e3d7fe07cce7ae8f077 Author: Erik Brakkee Date: Thu Jan 2 11:37:20 2025 +0100 first commit diff --git a/DESIGN.md b/DESIGN.md new file mode 100644 index 0000000..e6eb07a --- /dev/null +++ b/DESIGN.md @@ -0,0 +1,134 @@ + +Network policy and (later) linkerd policy generator. + +Basic idea: +1. The kubernetes clusters hosts applications +2. Applications communicate to other applications + +Allowed communication betwen applications is configured as follows: + +communication: +- from: app1 + to: app2 + ports: + - 80 + - linkerd-admin + +Ports are optional. When omitted all ports are intended + +There are pre-defined applications such as api-server. +Beyond that thera are two types of applications: +- cidr: a cidr with possible except clauses following netpol syntax +- app: regular app based on matchLabels, together with namespace name. + + +Application names must be unique. + +There are also standard capablities for an application such as: +* linkerd: addes egress to linkerd-jaeger, egress to linkerd, ingress from + linkerd-viz + +capablities can also be defined at the namespace level, which means they +apply to each pod in the namespace. + + + +networks: + - name: internet + cidr: 0.0.0.0/0 + except: + - 10.0.0.0/8 + - 172.16.0.0/12 + - 192.168.0.0/16 + + +namespaces: +- namespace: wamblee-org + capabilities: + - linkerd + applications: + - name: nexus-server + # ports when specified at the application level are used when + # not explicitly mentioned when a link is made + ports: + - 8081 + - 8082 + matchLabels: + app: nexus-server + +- namespace: exposure + applications: + - name: httpd-wamblee-org + matchLabels: + app: wamblee-org + +communications: +- from: # can we support both string and list of strings? + - httpd-wamblee-org + to: + - nexus-server + - wamblee-static + - wamblee-safe + +# or limiting ports further +- from: + - httpd-wamblee-org + to: + - nexus-server:8081,8082 + + +Handling of capabilities: +1. capabilities at namespace level apply to each individual pod in the + namespace +2. a capability is a list of templates. + + Ingress template + + from: + - linkerd-viz + to: + - {{ application }} + + egress template + + from: + - {{ application }} + to: + - linkerd-jaeger + - linkerd + + The templates are evaluated for an application and then parsed, and added + to the allowed communications. + + +Linkerd extension: +* for each application an optional service account is defined, when not + defined, 'default' is assumed. +* together with the communication links this determines the authorization + policies. + +Technical realization could be in the form of go templates where the input +the template is: +1. map of app name to definition with for each app also a list of + capabilities. +2. a single app config with for each app + * all ingresses + * all egresses + +Similar and easier technical solution via interface with a network policy +implementation and a linkerd implementation. + +Then we can have: +1. networkpolicy templates +2. server, meshtls, networkauthentication, authorization policy + * when linkerd capability for both pods, the tool can generated a + meshtlsauthentication, when the target pods does not have linkerd + capability, it can be ignored. + + + + + + + + diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..b84e313 --- /dev/null +++ b/Makefile @@ -0,0 +1,29 @@ +.DEFAULT_GOAL := all + +# seems superfluous +#.PHONY: fmt vet build clean + +tools: + GOPROXY=direct go install git.wamblee.org/public/gotools/cmd/go2junit@main + +fmt: + go fmt ./... + +vet: fmt + go vet ./... + + +build: vet + mkdir -p bin + go build -o bin ./cmd/... + +install: + go install ./... + +test: build + go test -count=1 -coverprofile=testout/coverage.out -json ${TESTFLAGS} ./... | go2junit testout + +clean: + rm -rf bin + +all: build diff --git a/cmd/policygen/config.go b/cmd/policygen/config.go new file mode 100644 index 0000000..e3d313f --- /dev/null +++ b/cmd/policygen/config.go @@ -0,0 +1,45 @@ +package main + +import ( + "github.com/goccy/go-yaml" + "net" +) + +func validateCIDR(cidr string) error { + _, _, err := net.ParseCIDR(cidr) + return err +} + +type CIDR string + +// UnmarshalYAML implements the yaml.Unmarshaler interface +func (c *CIDR) UnmarshalYAML(value []byte) error { + // Get the string value from the node + var s string + if err := yaml.Unmarshal(value, &s); err != nil { + return err + } + if err := validateCIDR(s); err != nil { + return err + } + *c = CIDR(s) + return nil +} + +// MarshalYAML implements the yaml.Marshaler interface +func (c CIDR) MarshalYAML() ([]byte, error) { + // Do any custom processing here before marshalling + return []byte(string(c)), nil +} + +// CIDRS represents each network entry in the YAML +type CIDRS struct { + Name string `yaml:"name"` + CIDR CIDR `yaml:"cidr"` + Except []CIDR `yaml:"except,omitempty"` +} + +// Config represents the top-level YAML structure +type Config struct { + Networks []CIDRS `yaml:"networks"` +} diff --git a/cmd/policygen/main.go b/cmd/policygen/main.go new file mode 100644 index 0000000..42b690d --- /dev/null +++ b/cmd/policygen/main.go @@ -0,0 +1,50 @@ +package main + +import ( + "bytes" + "fmt" + "github.com/goccy/go-yaml" + "github.com/spf13/cobra" + "os" +) + +type Options struct { +} + +func execute(files []string, options *Options) error { + if len(files) != 1 { + return fmt.Errorf("File expected") + } + yamlFile, err := os.ReadFile(files[0]) + if err != nil { + return fmt.Errorf("Error reading YAML file: %v", err) + } + + // Parse the YAML content + dec := yaml.NewDecoder(bytes.NewReader(yamlFile), + yaml.UseJSONUnmarshaler(), + yaml.DisallowUnknownField(), + ) + var config Config + err = dec.Decode(&config) + if err != nil { + return fmt.Errorf("Error parsing YAML: %v", err) + } + + fmt.Printf("PARSED %+v\n", config) + return nil +} + +func main() { + options := Options{} + cmd := &cobra.Command{ + Use: "policygen", + Short: "Generate network policies", + Long: "Generated policies based on a more compact representation of topology", + RunE: func(cmd *cobra.Command, args []string) error { + return execute(args, &options) + }, + } + + cmd.Execute() +} diff --git a/cmd/policygen/templates.go b/cmd/policygen/templates.go new file mode 100644 index 0000000..7f23905 --- /dev/null +++ b/cmd/policygen/templates.go @@ -0,0 +1,94 @@ +package main + +import ( + "fmt" + "html/template" + "io/fs" + "os" + "strings" +) +import "embed" +import sprig "github.com/Masterminds/sprig/v3" // This provides most Helm functions +import "github.com/goccy/go-yaml" + +// This provides most Helm functions + +//go:embed templates/* +var templateFS embed.FS + +func NewTemplate() *template.Template { + tmpl := template.New("") + + // Combine sprig functions with custom functions + funcMap := template.FuncMap{} + + // Add all sprig functions + for name, fn := range sprig.FuncMap() { + funcMap[name] = fn + } + + // Add any custom functions you want + customFuncs := template.FuncMap{ + "toYaml": func(v interface{}) string { + data, err := yaml.Marshal(v) + if err != nil { + return "" + } + return string(data) + }, + } + + // Merge custom functions + for name, fn := range customFuncs { + funcMap[name] = fn + } + + // Add the function map to the template + tmpl = tmpl.Funcs(funcMap) + + return tmpl +} + +func showContents(files fs.FS) { + entries, err := fs.ReadDir(files, ".") + if err != nil { + panic(err) + } + for _, entry := range entries { + fmt.Printf("entry %s %s\n", entry.Name(), entry.Type()) + if entry.Type().IsDir() { + subdir, err := fs.Sub(files, entry.Name()) + if err != nil { + panic(err) + } + showContents(subdir) + } + } +} + +func loadTemplates() (*template.Template, error) { + showContents(templateFS) + + // Parse all templates at once from the embedded FS + tmpl := NewTemplate() + + err := loadTemplatesGlob(tmpl) + if err != nil { + return nil, err + } + return tmpl, err +} + +func loadTemplatesGlob(tmpl *template.Template) error { + return fs.WalkDir(templateFS, ".", func(path string, d os.DirEntry, err error) error { + if strings.HasSuffix(path, ".yaml") { + data, err := fs.ReadFile(templateFS, path) + if err != nil { + return err + } + fmt.Fprintf(os.Stderr, "Loading template %s\n", path) + tmpl.New(path).Parse(string(data)) + } + return nil + }) +} diff --git a/cmd/policygen/templates/netpol/apiserver/cilium/egress.yaml b/cmd/policygen/templates/netpol/apiserver/cilium/egress.yaml new file mode 100644 index 0000000..b36c1d1 --- /dev/null +++ b/cmd/policygen/templates/netpol/apiserver/cilium/egress.yaml @@ -0,0 +1,15 @@ +kind: CiliumNetworkPolicy +apiVersion: cilium.io/v2 +metadata: + name: {{.name}} + namespace: {{.namespace}} +spec: + endpointSelector: + {{ .selector }} + egress: + - toEntities: + - kube-apiserver + - toPorts: + - ports: + - port: "6443" + protocol: TCP diff --git a/cmd/policygen/templates/netpol/apiserver/cilium/ingress.yaml b/cmd/policygen/templates/netpol/apiserver/cilium/ingress.yaml new file mode 100644 index 0000000..a81376f --- /dev/null +++ b/cmd/policygen/templates/netpol/apiserver/cilium/ingress.yaml @@ -0,0 +1,13 @@ +kind: CiliumNetworkPolicy +apiVersion: cilium.io/v2 +metadata: + name: {{.name}} + namespace: {{.namespace}} +spec: + endpointSelector: + {{ .selector }} + ingress: + - fromEntities: + - kube-apiserver + # See https://github.com/cilium/cilium/issues/35401 + - remote-node diff --git a/cmd/policygen/templates/netpol/egress.yaml b/cmd/policygen/templates/netpol/egress.yaml new file mode 100644 index 0000000..793dfd9 --- /dev/null +++ b/cmd/policygen/templates/netpol/egress.yaml @@ -0,0 +1,14 @@ +kind: NetworkPolicy +apiVersion: networking.k8s.io/v1 +metadata: + name: "{{.name}}" + namespace: "{{.namespace}}" +spec: + policyTypes: + - Egress + podSelector: + {{.selector}} + egress: + {{- range $from := .from }} + - {{ $from | nindent 4 }} + {{- end }} \ No newline at end of file diff --git a/cmd/policygen/templates/netpol/ingress.yaml b/cmd/policygen/templates/netpol/ingress.yaml new file mode 100644 index 0000000..5cd8b77 --- /dev/null +++ b/cmd/policygen/templates/netpol/ingress.yaml @@ -0,0 +1,14 @@ +kind: NetworkPolicy +apiVersion: networking.k8s.io/v1 +metadata: + name: "{{.name}}" + namespace: "{{.namespace}}" +spec: + policyTypes: + - Ingress + podSelector: + {{.selector}} + ingress: + {{- range $from := .from }} + - {{ $from | nindent 4 }} + {{- end }} \ No newline at end of file diff --git a/example/config.yaml b/example/config.yaml new file mode 100644 index 0000000..a5e6cf9 --- /dev/null +++ b/example/config.yaml @@ -0,0 +1,12 @@ + + + +networks: + - name: internet + cidr: 0.0.0.0/0 + except: + - 10.0.0.0/8 + - 172.16.0.0/12 + - 192.168.0.0/16 + + diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..0d6aa28 --- /dev/null +++ b/go.mod @@ -0,0 +1,21 @@ +module git.wamblee.org/public/policy-generator + +go 1.23.4 + +require ( + dario.cat/mergo v1.0.1 // indirect + github.com/Masterminds/goutils v1.1.1 // indirect + github.com/Masterminds/semver/v3 v3.3.0 // indirect + github.com/Masterminds/sprig/v3 v3.3.0 // indirect + github.com/goccy/go-yaml v1.15.13 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/huandu/xstrings v1.5.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/shopspring/decimal v1.4.0 // indirect + github.com/spf13/cast v1.7.0 // indirect + github.com/spf13/cobra v1.8.1 // indirect + github.com/spf13/pflag v1.0.5 // indirect + golang.org/x/crypto v0.26.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..1c6ca82 --- /dev/null +++ b/go.sum @@ -0,0 +1,34 @@ +dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= +dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= +github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0= +github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= +github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/goccy/go-yaml v1.15.13 h1:Xd87Yddmr2rC1SLLTm2MNDcTjeO/GYo0JGiww6gSTDg= +github.com/goccy/go-yaml v1.15.13/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= +github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= +github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= +golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=