nebula management web interface
diff --git a/core/nebula/Makefile b/core/nebula/Makefile
index e184886..3f7a57e 100644
--- a/core/nebula/Makefile
+++ b/core/nebula/Makefile
@@ -1,14 +1,18 @@
clean:
- rm -f controller
+ rm -f controller web
generate:
rm -rf generated
./hack/generate.sh
-build: clean
+controller: clean
go1.16 mod tidy
go1.16 mod vendor
- go1.16 build -o controller .
+ go1.16 build -o controller main.go
+
+web: clean
+ go1.16 build -o web web.go
+
# image: clean build
# docker build --tag=giolekva/rpuppy-arm .
diff --git a/core/nebula/apis/nebula/v1/types.go b/core/nebula/apis/nebula/v1/types.go
index 1a33752..0a295b7 100644
--- a/core/nebula/apis/nebula/v1/types.go
+++ b/core/nebula/apis/nebula/v1/types.go
@@ -16,7 +16,6 @@
}
type NebulaCASpec struct {
- CAName string `json:"caName"`
SecretName string `json:"secretName"`
}
@@ -52,10 +51,11 @@
}
type NebulaNodeSpec struct {
- CAName string `json:"caName"`
- NodeName string `json:"nodeName"`
- IPCidr string `json:"ipCidr"`
- SecretName string `json:"secretName"`
+ CAName string `json:"caName"`
+ CANamespace string `json:"caNamespace"`
+ IPCidr string `json:"ipCidr"`
+ PubKey string `json:"pubKey"`
+ SecretName string `json:"secretName"`
}
type NebulaNodeStatus struct {
diff --git a/core/nebula/controllers/ca.go b/core/nebula/controllers/ca.go
index c848571..d31b6cd 100644
--- a/core/nebula/controllers/ca.go
+++ b/core/nebula/controllers/ca.go
@@ -11,8 +11,6 @@
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
- "k8s.io/apimachinery/pkg/labels"
- "k8s.io/apimachinery/pkg/selection"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/wait"
corev1informers "k8s.io/client-go/informers/core/v1"
@@ -181,7 +179,7 @@
fmt.Printf("%s CA is already in Ready state\n", ca.Name)
return nil
}
- keyDir, err := generateCAKey(ca.Spec.CAName, c.nebulaCert)
+ keyDir, err := generateCAKey(ca.Name, c.nebulaCert)
if err != nil {
panic(err)
}
@@ -216,7 +214,7 @@
fmt.Printf("%s Node is already in Ready state\n", node.Name)
return nil
}
- ca, err := c.getCA(namespace, node.Spec.CAName)
+ ca, err := c.getCA(node.Spec.CANamespace, node.Spec.CAName)
if ca.Status.State != nebulav1.NebulaCAStateReady {
return fmt.Errorf("Referenced CA %s is not ready yet.", node.Spec.CAName)
}
@@ -228,8 +226,14 @@
if err != nil {
panic(err)
}
- if err := generateNodeKey(node.Spec.NodeName, node.Spec.IPCidr, dir, c.nebulaCert); err != nil {
- panic(err)
+ if node.Spec.PubKey == "" {
+ if err := generateNodeKey(node.Name, node.Spec.IPCidr, dir, c.nebulaCert); err != nil {
+ panic(err)
+ }
+ } else {
+ if err := generateNodeKeyFromPub(node.Name, node.Spec.IPCidr, node.Spec.PubKey, dir, c.nebulaCert); err != nil {
+ panic(err)
+ }
}
defer os.RemoveAll(dir)
if err := os.Remove(filepath.Join(dir, "ca.key")); err != nil {
@@ -316,12 +320,32 @@
"-out-key", filepath.Join(tmp, "ca.key"),
"-out-crt", filepath.Join(tmp, "ca.crt"),
"-out-qr", filepath.Join(tmp, "ca.png"))
- if err := cmd.Run(); err != nil {
- return "", err
+ if d, err := cmd.CombinedOutput(); err != nil {
+ return "", fmt.Errorf(string(d))
}
return tmp, nil
}
+func generateNodeKeyFromPub(name, ip, pubKey, dir, nebulaCert string) error {
+ hostPub := filepath.Join(dir, "host.pub")
+ if err := ioutil.WriteFile(hostPub, []byte(pubKey), 0644); err != nil {
+ return err
+ }
+ defer os.Remove(hostPub)
+ cmd := exec.Command(nebulaCert, "sign",
+ "-ca-crt", filepath.Join(dir, "ca.crt"),
+ "-ca-key", filepath.Join(dir, "ca.key"),
+ "-name", name,
+ "-ip", ip,
+ "-in-pub", hostPub,
+ "-out-crt", filepath.Join(dir, "host.crt"),
+ "-out-qr", filepath.Join(dir, "host.png"))
+ if d, err := cmd.CombinedOutput(); err != nil {
+ return fmt.Errorf(string(d))
+ }
+ return nil
+}
+
func generateNodeKey(name, ip, dir, nebulaCert string) error {
cmd := exec.Command(nebulaCert, "sign",
"-ca-crt", filepath.Join(dir, "ca.crt"),
@@ -338,45 +362,47 @@
}
func (c *NebulaController) getCA(namespace, name string) (*nebulav1.NebulaCA, error) {
- s := labels.NewSelector()
- r, err := labels.NewRequirement("metadata.namespace", selection.Equals, []string{namespace})
- if err != nil {
- panic(err)
- }
- r1, err := labels.NewRequirement("metadata.name", selection.Equals, []string{name})
- if err != nil {
- panic(err)
- }
- s.Add(*r, *r1)
- ncas, err := c.caLister.List(s)
- if err != nil {
- panic(err)
- }
- if len(ncas) != 1 {
- panic("err")
- }
- return ncas[0], nil
+ return c.caLister.NebulaCAs(namespace).Get(name)
+ // s := labels.NewSelector()
+ // r, err := labels.NewRequirement("metadata.namespace", selection.Equals, []string{namespace})
+ // if err != nil {
+ // panic(err)
+ // }
+ // r1, err := labels.NewRequirement("metadata.name", selection.Equals, []string{name})
+ // if err != nil {
+ // panic(err)
+ // }
+ // s.Add(*r, *r1)
+ // ncas, err := c.caLister.List(s)
+ // if err != nil {
+ // panic(err)
+ // }
+ // if len(ncas) != 1 {
+ // panic("err")
+ // }
+ // return ncas[0], nil
}
func (c *NebulaController) getNode(namespace, name string) (*nebulav1.NebulaNode, error) {
- s := labels.NewSelector()
- r, err := labels.NewRequirement("metadata.namespace", selection.Equals, []string{namespace})
- if err != nil {
- panic(err)
- }
- r1, err := labels.NewRequirement("metadata.name", selection.Equals, []string{name})
- if err != nil {
- panic(err)
- }
- s.Add(*r, *r1)
- nodes, err := c.nodeLister.List(s)
- if err != nil {
- panic(err)
- }
- if len(nodes) != 1 {
- panic("err")
- }
- return nodes[0], nil
+ return c.nodeLister.NebulaNodes(namespace).Get(name)
+ // s := labels.NewSelector()
+ // r, err := labels.NewRequirement("metadata.namespace", selection.Equals, []string{namespace})
+ // if err != nil {
+ // panic(err)
+ // }
+ // r1, err := labels.NewRequirement("metadata.name", selection.Equals, []string{name})
+ // if err != nil {
+ // panic(err)
+ // }
+ // s.Add(*r, *r1)
+ // nodes, err := c.nodeLister.List(s)
+ // if err != nil {
+ // panic(err)
+ // }
+ // if len(nodes) != 1 {
+ // panic("err")
+ // }
+ // return nodes[0], nil
}
func (c *NebulaController) getSecret(namespace, name string) (*corev1.Secret, error) {
diff --git a/core/nebula/crds/nebula.crds.yaml b/core/nebula/crds/nebula.crds.yaml
index d75cac0..c8de194 100644
--- a/core/nebula/crds/nebula.crds.yaml
+++ b/core/nebula/crds/nebula.crds.yaml
@@ -26,8 +26,6 @@
spec:
type: object
properties:
- caName:
- type: string
secretName:
type: string
status:
@@ -68,10 +66,12 @@
properties:
caName:
type: string
- nodeName:
+ caNamespace:
type: string
ipCidr:
type: string
+ pubKey:
+ type: string
secretName:
type: string
status:
diff --git a/core/nebula/crds/test-ca.yaml b/core/nebula/crds/test-ca.yaml
deleted file mode 100644
index 19dcdab..0000000
--- a/core/nebula/crds/test-ca.yaml
+++ /dev/null
@@ -1,8 +0,0 @@
-apiVersion: lekva.me/v1
-kind: NebulaCA
-metadata:
- name: test
- namespace: test-nebula
-spec:
- caName: lekva-pcloud
- secretName: lekva-pcloud-ca
diff --git a/core/nebula/crds/test-node.yaml b/core/nebula/crds/test-node.yaml
deleted file mode 100644
index bcabecf..0000000
--- a/core/nebula/crds/test-node.yaml
+++ /dev/null
@@ -1,10 +0,0 @@
-apiVersion: lekva.me/v1
-kind: NebulaNode
-metadata:
- name: test-host
- namespace: test-nebula
-spec:
- caName: lekva-pcloud
- nodeName: lighthouse
- ipCidr: "111.0.0.1/24"
- secretName: node-lighthouse
diff --git a/core/nebula/go.mod b/core/nebula/go.mod
index 032bd23..ea547d2 100644
--- a/core/nebula/go.mod
+++ b/core/nebula/go.mod
@@ -3,6 +3,7 @@
go 1.16
require (
+ github.com/gorilla/mux v1.8.0
k8s.io/api v0.22.2
k8s.io/apimachinery v0.22.2
k8s.io/client-go v0.22.2
diff --git a/core/nebula/go.sum b/core/nebula/go.sum
index 8cf33e5..0bfb743 100644
--- a/core/nebula/go.sum
+++ b/core/nebula/go.sum
@@ -129,6 +129,8 @@
github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2cUuW7uA/OeU=
github.com/googleapis/gnostic v0.5.5 h1:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9xHw=
github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA=
+github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
+github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
diff --git a/core/nebula/templates/index.html b/core/nebula/templates/index.html
new file mode 100644
index 0000000..ac5ae32
--- /dev/null
+++ b/core/nebula/templates/index.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="utf-8" />
+ <title>Nebula Mesh VPN Manager</title>
+</head>
+<body>
+ <form action="/sign-node" method="POST">
+ <label for="ca-name">CA Name:</label><br />
+ <input type="text" name="ca-name" /><br />
+ <label for="ca-namespace">CA Namespace:</label><br />
+ <input type="text" name="ca-namespace" /><br />
+ <label for="node-name">Node Name:</label><br />
+ <input type="text" name="node-name" /><br />
+ <label for="node-namespace">Node Namespace:</label><br />
+ <input type="text" name="node-namespace" /><br />
+ <label for="ip-cidr">IP/CIDR:</label><br />
+ <input type="text" name="ip-cidr" /><br />
+ <label for="pub-key">Public Key:</label><br />
+ <textarea name="pub-key">Put node public key here</textarea><br />
+ <input type="submit" value="Sign node key" />
+ </form>
+ {{range .}}
+ <a href="/ca/{{.Namespace}}/{{.Name}}"><h1>{{.Name}}</h1></a>
+ <table>
+ <tr>
+ <th>Node</th>
+ <th>IP</th>
+ </tr>
+ {{range .Nodes}}
+ <tr>
+ <td>
+ <a href="/node/{{.Namespace}}/{{.Name}}">{{.Name}}</a>
+ </td>
+ <td>
+ {{.IP}}
+ </td>
+ </tr>
+ {{end}}
+ </table>
+ {{end}}
+</body>
+</html>
+
diff --git a/core/nebula/web.go b/core/nebula/web.go
new file mode 100644
index 0000000..8738cd4
--- /dev/null
+++ b/core/nebula/web.go
@@ -0,0 +1,225 @@
+package main
+
+import (
+ "context"
+ "embed"
+ "flag"
+ "fmt"
+ "html/template"
+ "log"
+ "net/http"
+
+ "github.com/gorilla/mux"
+
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/client-go/kubernetes"
+ "k8s.io/client-go/tools/clientcmd"
+
+ nebulav1 "github.com/giolekva/pcloud/core/nebula/apis/nebula/v1"
+ clientset "github.com/giolekva/pcloud/core/nebula/generated/clientset/versioned"
+)
+
+var port = flag.Int("port", 8080, "Port to listen on.")
+var kubeConfig = flag.String("kubeconfig", "", "Path to a kubeconfig. Only required if out-of-cluster.")
+var masterURL = flag.String("master", "", "The address of the Kubernetes API server. Overrides any value in kubeconfig. Only required if out-of-cluster.")
+
+//go:embed templates/*
+var tmpls embed.FS
+
+type Templates struct {
+ Index *template.Template
+}
+
+func ParseTemplates(fs embed.FS) (*Templates, error) {
+ index, err := template.ParseFS(fs, "templates/index.html")
+ if err != nil {
+ return nil, err
+ }
+ return &Templates{index}, nil
+}
+
+type nebulaCA struct {
+ Name string
+ Namespace string
+ Nodes []nebulaNode
+}
+
+type nebulaNode struct {
+ Name string
+ Namespace string
+ IP string
+}
+
+type Manager struct {
+ kubeClient kubernetes.Interface
+ nebulaClient clientset.Interface
+}
+
+func (m *Manager) ListAll() ([]*nebulaCA, error) {
+ ret := make([]*nebulaCA, 0)
+ cas, err := m.nebulaClient.LekvaV1().NebulaCAs("").List(context.TODO(), metav1.ListOptions{})
+ if err != nil {
+ return nil, err
+ }
+ for _, ca := range cas.Items {
+ ret = append(ret, &nebulaCA{
+ Name: ca.Name,
+ Namespace: ca.Namespace,
+ Nodes: make([]nebulaNode, 0),
+ })
+ }
+ nodes, err := m.nebulaClient.LekvaV1().NebulaNodes("").List(context.TODO(), metav1.ListOptions{})
+ if err != nil {
+ return nil, err
+ }
+ for _, node := range nodes.Items {
+ for _, ca := range ret {
+ if ca.Name == node.Spec.CAName {
+ ca.Nodes = append(ca.Nodes, nebulaNode{
+ Name: node.Name,
+ Namespace: node.Namespace,
+ IP: node.Spec.IPCidr,
+ })
+ }
+ }
+ }
+ return ret, nil
+}
+
+func (m *Manager) createNode(namespace, name, caNamespace, caName, ipCidr, pubKey string) (string, string, error) {
+ node := &nebulav1.NebulaNode{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: name,
+ Namespace: namespace,
+ },
+ Spec: nebulav1.NebulaNodeSpec{
+ CAName: caName,
+ CANamespace: caNamespace,
+ IPCidr: ipCidr,
+ PubKey: pubKey,
+ SecretName: fmt.Sprintf("%s-cert", name),
+ },
+ }
+ node, err := m.nebulaClient.LekvaV1().NebulaNodes(namespace).Create(context.TODO(), node, metav1.CreateOptions{})
+ if err != nil {
+ return "", "", err
+ }
+ return node.Namespace, node.Name, nil
+}
+
+func (m *Manager) getNodeCertQR(namespace, name string) ([]byte, error) {
+ node, err := m.nebulaClient.LekvaV1().NebulaNodes(namespace).Get(context.TODO(), name, metav1.GetOptions{})
+ if err != nil {
+ return nil, err
+ }
+ secret, err := m.kubeClient.CoreV1().Secrets(namespace).Get(context.TODO(), node.Spec.SecretName, metav1.GetOptions{})
+ if err != nil {
+ return nil, err
+ }
+ return secret.Data["host.png"], nil
+}
+
+func (m *Manager) getCACertQR(namespace, name string) ([]byte, error) {
+ ca, err := m.nebulaClient.LekvaV1().NebulaCAs(namespace).Get(context.TODO(), name, metav1.GetOptions{})
+ if err != nil {
+ return nil, err
+ }
+ secret, err := m.kubeClient.CoreV1().Secrets(namespace).Get(context.TODO(), ca.Spec.SecretName, metav1.GetOptions{})
+ if err != nil {
+ return nil, err
+ }
+ return secret.Data["ca.png"], nil
+}
+
+type Handler struct {
+ mgr Manager
+ tmpls *Templates
+}
+
+func (h *Handler) handleIndex(w http.ResponseWriter, r *http.Request) {
+ cas, err := h.mgr.ListAll()
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ if err := h.tmpls.Index.Execute(w, cas); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ }
+}
+
+func (h *Handler) handleNode(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ namespace := vars["namespace"]
+ name := vars["name"]
+ qr, err := h.mgr.getNodeCertQR(namespace, name)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ }
+ w.Header().Set("Content-Type", "img/png")
+ w.Write(qr)
+}
+
+func (h *Handler) handleCA(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ namespace := vars["namespace"]
+ name := vars["name"]
+ qr, err := h.mgr.getCACertQR(namespace, name)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ }
+ w.Header().Set("Content-Type", "img/png")
+ w.Write(qr)
+}
+
+func (h *Handler) handleSignNode(w http.ResponseWriter, r *http.Request) {
+ if err := r.ParseForm(); err != nil {
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+ _, _, err := h.mgr.createNode(
+ r.FormValue("node-namespace"),
+ r.FormValue("node-name"),
+ r.FormValue("ca-namespace"),
+ r.FormValue("ca-name"),
+ r.FormValue("ip-cidr"),
+ r.FormValue("pub-key"),
+ )
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ http.Redirect(w, r, "/", http.StatusSeeOther)
+}
+
+func main() {
+ flag.Parse()
+ cfg, err := clientcmd.BuildConfigFromFlags(*masterURL, *kubeConfig)
+ if err != nil {
+ panic(err)
+ }
+ kubeClient, err := kubernetes.NewForConfig(cfg)
+ if err != nil {
+ panic(err)
+ }
+ nebulaClient := clientset.NewForConfigOrDie(cfg)
+ t, err := ParseTemplates(tmpls)
+ if err != nil {
+ log.Fatal(err)
+ }
+ mgr := Manager{
+ kubeClient: kubeClient,
+ nebulaClient: nebulaClient,
+ }
+ handler := Handler{
+ mgr: mgr,
+ tmpls: t,
+ }
+ r := mux.NewRouter()
+ r.HandleFunc("/node/{namespace:[a-zA-z0-9-]+}/{name:[a-zA-z0-9-]+}", handler.handleNode)
+ r.HandleFunc("/ca/{namespace:[a-zA-z0-9-]+}/{name:[a-zA-z0-9-]+}", handler.handleCA)
+ r.HandleFunc("/sign-node", handler.handleSignNode)
+ r.HandleFunc("/", handler.handleIndex)
+ http.Handle("/", r)
+ fmt.Printf("Starting HTTP server on port: %d\n", *port)
+ log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", *port), nil))
+}