VPN: move certificate signing logit to api service to which controller delegates ops
diff --git a/core/nebula/api/main.go b/core/nebula/api/main.go
index 906a674..9127630 100644
--- a/core/nebula/api/main.go
+++ b/core/nebula/api/main.go
@@ -15,6 +15,7 @@
"io"
"io/ioutil"
"log"
+ "net"
"net/http"
"time"
@@ -262,8 +263,6 @@
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
- fmt.Println("---- APPROVE")
- fmt.Printf("%#v\n", req)
_, _, err := h.mgr.CreateNode(
*namespace,
req.Name,
@@ -280,6 +279,59 @@
}
}
+type processCAReq struct {
+ Name string `json:"name"`
+}
+
+func (h *Handler) processCA(w http.ResponseWriter, r *http.Request) {
+ var req processCAReq
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+ ca, err := CreateCertificateAuthority(req.Name)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ w.Header().Set("Content-Type", "application/json")
+ if err := json.NewEncoder(w).Encode(ca); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+}
+
+type processNodeReq struct {
+ CAPrivateKey []byte `json:"ca_private_key"`
+ CACert []byte `json:"ca_certificate"`
+ NodeName string `json:"node_name"`
+ NodePublicKey []byte `json:"node_public_key,omitempty"`
+ NodeIPCidr string `json:"node_ip_cidr"`
+}
+
+func (h *Handler) processNode(w http.ResponseWriter, r *http.Request) {
+ var req processNodeReq
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+ _, ipNet, err := net.ParseCIDR(req.NodeIPCidr)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+ node, err := SignNebulaNode(req.CAPrivateKey, req.CACert, req.NodeName, req.NodePublicKey, ipNet)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+ w.Header().Set("Content-Type", "application/json")
+ if err := json.NewEncoder(w).Encode(node); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+}
+
func loadConfigTemplate(path string) (map[string]interface{}, error) {
tmpl, err := ioutil.ReadFile(path)
if err != nil {
@@ -327,6 +379,8 @@
r.HandleFunc("/api/join", handler.join)
r.HandleFunc("/api/approve", handler.approve)
r.HandleFunc("/api/get/{name:[a-zA-z0-9-]+}", handler.get)
+ r.HandleFunc("/api/process/authority", handler.processCA)
+ r.HandleFunc("/api/process/node", handler.processNode)
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("/", handler.handleIndex)
diff --git a/core/nebula/api/nebula.go b/core/nebula/api/nebula.go
new file mode 100644
index 0000000..b4649f1
--- /dev/null
+++ b/core/nebula/api/nebula.go
@@ -0,0 +1,125 @@
+package main
+
+import (
+ "crypto/ed25519"
+ "crypto/rand"
+ "errors"
+ "io"
+ "net"
+ "time"
+
+ "github.com/slackhq/nebula/cert"
+ "golang.org/x/crypto/curve25519"
+)
+
+type CertificateAuthority struct {
+ PrivateKey []byte `json:"private_key"`
+ Certificate []byte `json:"certificate"`
+}
+
+func CreateCertificateAuthority(name string) (*CertificateAuthority, error) {
+ t := time.Now().Add(time.Duration(-1 * time.Second))
+ rawPub, rawPriv, err := ed25519.GenerateKey(rand.Reader)
+ if err != nil {
+ return nil, err
+ }
+ nc := cert.NebulaCertificate{
+ Details: cert.NebulaCertificateDetails{
+ Name: name,
+ NotBefore: t,
+ NotAfter: t.Add(time.Duration(8760 * time.Hour)),
+ PublicKey: rawPub,
+ IsCA: true,
+ },
+ }
+ if err := nc.Sign(rawPriv); err != nil {
+ return nil, err
+ }
+ certSerialized, err := nc.MarshalToPEM()
+ if err != nil {
+ return nil, err
+ }
+ privKeySerialzied := cert.MarshalEd25519PrivateKey(rawPriv)
+ return &CertificateAuthority{
+ privKeySerialzied,
+ certSerialized,
+ }, nil
+}
+
+type NebulaNode struct {
+ PrivateKey []byte `json:"private_key,omitempty"`
+ Certificate []byte `json:"certificate"`
+}
+
+func SignNebulaNode(rawCAPrivateKey []byte, rawCACert []byte, nodeName string, nodePublicKey []byte, ip *net.IPNet) (*NebulaNode, error) {
+ caKey, _, err := cert.UnmarshalEd25519PrivateKey(rawCAPrivateKey)
+ if err != nil {
+ return nil, err
+ }
+ caCert, _, err := cert.UnmarshalNebulaCertificateFromPEM(rawCACert)
+ if err != nil {
+ return nil, err
+ }
+
+ if err := caCert.VerifyPrivateKey(caKey); err != nil {
+ return nil, err
+ }
+ issuer, err := caCert.Sha256Sum()
+ if err != nil {
+ return nil, err
+ }
+ if caCert.Expired(time.Now()) {
+ return nil, errors.New("ca certificate is expired")
+ }
+ var pub, priv []byte
+ if nodePublicKey != nil {
+ var err error
+ pub, _, err = cert.UnmarshalX25519PublicKey(nodePublicKey)
+ if err != nil {
+ return nil, err
+ }
+ } else {
+ var rawPriv []byte
+ var err error
+ pub, rawPriv, err = x25519Keypair()
+ if err != nil {
+ return nil, err
+ }
+ priv = cert.MarshalX25519PrivateKey(rawPriv)
+ }
+ t := time.Now().Add(time.Duration(-1 * time.Second))
+ nc := cert.NebulaCertificate{
+ Details: cert.NebulaCertificateDetails{
+ Name: nodeName,
+ Ips: []*net.IPNet{ip},
+ NotBefore: t,
+ NotAfter: caCert.Details.NotAfter.Add(time.Duration(-1 * time.Second)),
+ PublicKey: pub,
+ IsCA: false,
+ Issuer: issuer,
+ },
+ }
+ if err := nc.CheckRootConstrains(caCert); err != nil {
+ return nil, err
+ }
+ if err := nc.Sign(caKey); err != nil {
+ return nil, err
+ }
+ certSerialized, err := nc.MarshalToPEM()
+ if err != nil {
+ return nil, err
+ }
+ return &NebulaNode{
+ PrivateKey: priv,
+ Certificate: certSerialized,
+ }, nil
+}
+
+func x25519Keypair() ([]byte, []byte, error) {
+ var pubkey, privkey [32]byte
+ if _, err := io.ReadFull(rand.Reader, privkey[:]); err != nil {
+ return nil, nil, err
+ }
+ curve25519.ScalarBaseMult(&pubkey, &privkey)
+ return pubkey[:], privkey[:], nil
+}
diff --git a/core/nebula/controller/Dockerfile b/core/nebula/controller/Dockerfile
index 82586e4..79b4809 100644
--- a/core/nebula/controller/Dockerfile
+++ b/core/nebula/controller/Dockerfile
@@ -5,7 +5,7 @@
COPY controller_${TARGETARCH} /usr/bin/nebula-controller
RUN chmod +x /usr/bin/nebula-controller
-RUN wget https://github.com/slackhq/nebula/releases/download/v1.4.0/nebula-linux-${TARGETARCH}.tar.gz -O nebula.tar.gz
-RUN tar -xvf nebula.tar.gz
-RUN mv nebula-cert /usr/bin
-RUN chmod +x /usr/bin/nebula-cert
+# RUN wget https://github.com/slackhq/nebula/releases/download/v1.4.0/nebula-linux-${TARGETARCH}.tar.gz -O nebula.tar.gz
+# RUN tar -xvf nebula.tar.gz
+# RUN mv nebula-cert /usr/bin
+# RUN chmod +x /usr/bin/nebula-cert
diff --git a/core/nebula/controller/controllers/ca.go b/core/nebula/controller/controllers/ca.go
index 37b3856..9cc284b 100644
--- a/core/nebula/controller/controllers/ca.go
+++ b/core/nebula/controller/controllers/ca.go
@@ -3,10 +3,6 @@
import (
"context"
"fmt"
- "io/ioutil"
- "os"
- "os/exec"
- "path/filepath"
"time"
corev1 "k8s.io/api/core/v1"
@@ -47,16 +43,13 @@
secretLister corev1listers.SecretLister
secretSynced cache.InformerSynced
workqueue workqueue.RateLimitingInterface
-
- nebulaCert string
}
func NewNebulaController(kubeClient kubernetes.Interface,
nebulaClient clientset.Interface,
caInformer informers.NebulaCAInformer,
nodeInformer informers.NebulaNodeInformer,
- secretInformer corev1informers.SecretInformer,
- nebulaCert string) *NebulaController {
+ secretInformer corev1informers.SecretInformer) *NebulaController {
c := &NebulaController{
kubeClient: kubeClient,
nebulaClient: nebulaClient,
@@ -67,7 +60,6 @@
secretLister: secretInformer.Lister(),
secretSynced: secretInformer.Informer().HasSynced,
workqueue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "Nebula"),
- nebulaCert: nebulaCert,
}
caInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
@@ -186,17 +178,20 @@
fmt.Printf("%s CA is already in Ready state\n", ca.Name)
return nil
}
- keyDir, err := generateCAKey(ca.Name, c.nebulaCert)
+ privKey, cert, err := CreateCertificateAuthority(apiAddr(ca.Name), ca.Name)
if err != nil {
return err
}
- defer os.RemoveAll(keyDir)
- secret, err := createSecretFromDir(keyDir)
- if err != nil {
- return err
+ secret := &corev1.Secret{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: ca.Spec.SecretName,
+ },
+ Immutable: &secretImmutable,
+ Data: map[string][]byte{
+ "ca.key": privKey,
+ "ca.crt": cert,
+ },
}
- secret.Immutable = &secretImmutable
- secret.Name = ca.Spec.SecretName
_, err = c.kubeClient.CoreV1().Secrets(namespace).Create(context.TODO(), secret, metav1.CreateOptions{})
if err != nil {
return err
@@ -239,32 +234,25 @@
}
return err
}
- dir, err := extractSecret(caSecret)
+ var pubKey []byte
+ if node.Spec.PubKey != "" {
+ pubKey = []byte(node.Spec.PubKey)
+ }
+ privKey, nodeCert, err := SignNebulaNode(apiAddr(ca.Name), caSecret.Data["ca.key"], caSecret.Data["ca.crt"], node.Name, pubKey, node.Spec.IPCidr)
if err != nil {
return err
}
- if node.Spec.PubKey == "" {
- if err := generateNodeKey(node.Name, node.Spec.IPCidr, dir, c.nebulaCert); err != nil {
- return err
- }
- } else {
- if err := generateNodeKeyFromPub(node.Name, node.Spec.IPCidr, node.Spec.PubKey, dir, c.nebulaCert); err != nil {
- return err
- }
+ secret := &corev1.Secret{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: node.Spec.SecretName,
+ },
+ Immutable: &secretImmutable,
+ Data: map[string][]byte{
+ "ca.crt": caSecret.Data["ca.crt"],
+ "host.crt": nodeCert,
+ "host.key": privKey,
+ },
}
- defer os.RemoveAll(dir)
- if err := os.Remove(filepath.Join(dir, "ca.key")); err != nil {
- return err
- }
- if err := os.Remove(filepath.Join(dir, "ca.png")); err != nil {
- return err
- }
- secret, err := createSecretFromDir(dir)
- if err != nil {
- return err
- }
- secret.Immutable = &secretImmutable
- secret.Name = node.Spec.SecretName
_, err = c.kubeClient.CoreV1().Secrets(namespace).Create(context.TODO(), secret, metav1.CreateOptions{})
if err != nil {
return err
@@ -292,88 +280,7 @@
return err
}
-func createSecretFromDir(path string) (*corev1.Secret, error) {
- all, err := ioutil.ReadDir(path)
- if err != nil {
- return nil, err
- }
- secret := &corev1.Secret{
- Data: make(map[string][]byte),
- }
- for _, f := range all {
- if f.IsDir() {
- continue
- }
- d, err := ioutil.ReadFile(filepath.Join(path, f.Name()))
- if err != nil {
- return nil, err
- }
- secret.Data[f.Name()] = d
- }
- return secret, nil
-}
-
-func extractSecret(secret *corev1.Secret) (string, error) {
- tmp, err := os.MkdirTemp("", secret.Name)
- if err != nil {
- return "", err
- }
- for name, data := range secret.Data {
- if err := ioutil.WriteFile(filepath.Join(tmp, name), data, 0644); err != nil {
- defer os.RemoveAll(tmp)
- return "", err
- }
- }
- return tmp, nil
-}
-
-func generateCAKey(name, nebulaCert string) (string, error) {
- tmp, err := os.MkdirTemp("", name)
- if err != nil {
- return "", err
- }
- cmd := exec.Command(nebulaCert, "ca",
- "-name", name,
- "-out-key", filepath.Join(tmp, "ca.key"),
- "-out-crt", filepath.Join(tmp, "ca.crt"),
- "-out-qr", filepath.Join(tmp, "ca.png"))
- 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 _, err := cmd.CombinedOutput(); err != nil {
- return err
- }
- return nil
-}
-
-func generateNodeKey(name, ip, dir, nebulaCert string) error {
- cmd := exec.Command(nebulaCert, "sign",
- "-ca-crt", filepath.Join(dir, "ca.crt"),
- "-ca-key", filepath.Join(dir, "ca.key"),
- "-name", name,
- "-ip", ip,
- "-out-key", filepath.Join(dir, "host.key"),
- "-out-crt", filepath.Join(dir, "host.crt"),
- "-out-qr", filepath.Join(dir, "host.png"))
- if _, err := cmd.CombinedOutput(); err != nil {
- return err
- }
- return nil
+// TODO(giolekva): maybe pass by flag?
+func apiAddr(ca string) string {
+ return fmt.Sprintf("http://nebula-api.%s-ingress-private.svc.cluster.local", ca)
}
diff --git a/core/nebula/controller/controllers/client.go b/core/nebula/controller/controllers/client.go
new file mode 100644
index 0000000..da5b818
--- /dev/null
+++ b/core/nebula/controller/controllers/client.go
@@ -0,0 +1,81 @@
+package controllers
+
+import (
+ "bytes"
+ "crypto/tls"
+ "encoding/json"
+ "net/http"
+)
+
+type createCAReq struct {
+ Name string `json:"name"`
+}
+
+type createCAResp struct {
+ PrivateKey []byte `json:"private_key"`
+ Certificate []byte `json:"certificate"`
+}
+
+func CreateCertificateAuthority(apiAddr, name string) ([]byte, []byte, error) {
+ var data bytes.Buffer
+ if err := json.NewEncoder(&data).Encode(createCAReq{name}); err != nil {
+ return nil, nil, err
+ }
+ client := &http.Client{
+ // TODO(giolekva): remove, for some reason valid certificates are not accepted on gioui android.
+ Transport: &http.Transport{
+ TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
+ },
+ }
+ resp, err := client.Post(apiAddr+"/api/process/ca", "application/json", &data)
+ if err != nil {
+ return nil, nil, err
+ }
+ var ret createCAResp
+ if err := json.NewDecoder(resp.Body).Decode(&ret); err != nil {
+ return nil, nil, err
+ }
+ return ret.PrivateKey, ret.Certificate, nil
+}
+
+type signNodeReq struct {
+ CAPrivateKey []byte `json:"ca_private_key"`
+ CACert []byte `json:"ca_certificate"`
+ NodeName string `json:"node_name"`
+ NodePublicKey []byte `json:"node_public_key,omitempty"`
+ NodeIPCidr string `json:"node_ip_cidr"`
+}
+
+type signNodeResp struct {
+ PrivateKey []byte `json:"private_key,omitempty"`
+ Certificate []byte `json:"certificate"`
+}
+
+func SignNebulaNode(apiAddr string, caPrivateKey, caCert []byte, nodeName string, nodePublicKey []byte, nodeIp string) ([]byte, []byte, error) {
+ req := signNodeReq{
+ caPrivateKey,
+ caCert,
+ nodeName,
+ nodePublicKey,
+ nodeIp,
+ }
+ var data bytes.Buffer
+ if err := json.NewEncoder(&data).Encode(req); err != nil {
+ return nil, nil, err
+ }
+ client := &http.Client{
+ // TODO(giolekva): remove, for some reason valid certificates are not accepted on gioui android.
+ Transport: &http.Transport{
+ TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
+ },
+ }
+ resp, err := client.Post(apiAddr+"/api/process/node", "application/json", &data)
+ if err != nil {
+ return nil, nil, err
+ }
+ var ret signNodeResp
+ if err := json.NewDecoder(resp.Body).Decode(&ret); err != nil {
+ return nil, nil, err
+ }
+ return ret.PrivateKey, ret.Certificate, nil
+}
diff --git a/core/nebula/controller/main.go b/core/nebula/controller/main.go
index e230129..7867491 100644
--- a/core/nebula/controller/main.go
+++ b/core/nebula/controller/main.go
@@ -19,7 +19,6 @@
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.")
-var nebulaCert = flag.String("nebula-cert", "", "Path to the nebula-cert binary.")
func main() {
flag.Parse()
@@ -40,8 +39,7 @@
nebulaClient,
nebulaInformerFactory.Lekva().V1().NebulaCAs(),
nebulaInformerFactory.Lekva().V1().NebulaNodes(),
- kubeInformerFactory.Core().V1().Secrets(),
- *nebulaCert)
+ kubeInformerFactory.Core().V1().Secrets())
stopCh := make(chan struct{})
kubeInformerFactory.Start(stopCh)
nebulaInformerFactory.Start(stopCh)