From 3d6ed8604ac778cff35f87ecb8f2baf464fa2add Mon Sep 17 00:00:00 2001 From: Brian Picciano Date: Sun, 27 Aug 2023 16:09:03 +0200 Subject: [PATCH] Add ability to sign nebula public keys, and show nebula network info The new commands are: - `isle admin create-nebula-cert` - `isle nebula show` Between these two commands it's possible, with some effort, to get a nebula mobile client hooked up to an isle server. --- go/cmd/entrypoint/admin.go | 72 ++++++++++++++++++ go/cmd/entrypoint/main.go | 1 + go/cmd/entrypoint/nebula.go | 82 +++++++++++++++++++++ go/cmd/entrypoint/nebula_util.go | 2 +- go/nebula/nebula.go | 121 ++++++++++++++++++------------- 5 files changed, 227 insertions(+), 51 deletions(-) create mode 100644 go/cmd/entrypoint/nebula.go diff --git a/go/cmd/entrypoint/admin.go b/go/cmd/entrypoint/admin.go index 0523180..dd8765f 100644 --- a/go/cmd/entrypoint/admin.go +++ b/go/cmd/entrypoint/admin.go @@ -344,6 +344,77 @@ var subCmdAdminCreateBootstrap = subCmd{ }, } +var subCmdAdminCreateNebulaCert = subCmd{ + name: "create-nebula-cert", + descr: "Creates a signed nebula certificate file and writes it to stdout", + checkLock: false, + do: func(subCmdCtx subCmdCtx) error { + + flags := subCmdCtx.flagSet(false) + + hostName := flags.StringP( + "hostname", "h", "", + "Name of the host to generate bootstrap.yml for", + ) + + ipStr := flags.StringP( + "ip", "i", "", + "IP of the new host", + ) + + adminPath := flags.StringP( + "admin-path", "a", "", + `Path to admin.yml file. If the given path is "-" then stdin is used.`, + ) + + pubKeyPath := flags.StringP( + "public-key-path", "p", "", + `Path to PEM file containing public key which will be embedded in the cert.`, + ) + + if err := flags.Parse(subCmdCtx.args); err != nil { + return fmt.Errorf("parsing flags: %w", err) + } + + if *hostName == "" || *ipStr == "" || *adminPath == "" || *pubKeyPath == "" { + return errors.New("--hostname, --ip, --admin-path, and --pub-key-path are required") + } + + if err := validateHostName(*hostName); err != nil { + return fmt.Errorf("invalid hostname %q: %w", *hostName, err) + } + + ip := net.ParseIP(*ipStr) + + if ip == nil { + return fmt.Errorf("invalid ip %q", *ipStr) + } + + adm, err := readAdmin(*adminPath) + if err != nil { + return fmt.Errorf("reading admin.yml with --admin-path of %q: %w", *adminPath, err) + } + + hostPubPEM, err := os.ReadFile(*pubKeyPath) + if err != nil { + return fmt.Errorf("reading public key from %q: %w", *pubKeyPath, err) + } + + nebulaHostCertPEM, err := nebula.NewHostCertPEM( + adm.Nebula.CACredentials, string(hostPubPEM), *hostName, ip, + ) + if err != nil { + return fmt.Errorf("creating cert: %w", err) + } + + if _, err := os.Stdout.Write([]byte(nebulaHostCertPEM)); err != nil { + return fmt.Errorf("writing to stdout: %w", err) + } + + return nil + }, +} + var subCmdAdmin = subCmd{ name: "admin", descr: "Sub-commands which only admins can run", @@ -351,6 +422,7 @@ var subCmdAdmin = subCmd{ return subCmdCtx.doSubCmd( subCmdAdminCreateNetwork, subCmdAdminCreateBootstrap, + subCmdAdminCreateNebulaCert, ) }, } diff --git a/go/cmd/entrypoint/main.go b/go/cmd/entrypoint/main.go index 35ea788..3a1fdc0 100644 --- a/go/cmd/entrypoint/main.go +++ b/go/cmd/entrypoint/main.go @@ -70,6 +70,7 @@ func main() { subCmdDaemon, subCmdGarage, subCmdHosts, + subCmdNebula, subCmdVersion, ) diff --git a/go/cmd/entrypoint/nebula.go b/go/cmd/entrypoint/nebula.go new file mode 100644 index 0000000..a7a7eb7 --- /dev/null +++ b/go/cmd/entrypoint/nebula.go @@ -0,0 +1,82 @@ +package main + +import ( + "fmt" + "os" + + "github.com/slackhq/nebula/cert" + "gopkg.in/yaml.v3" +) + +var subCmdNebulaShow = subCmd{ + name: "show", + descr: "Writes nebula network information to stdout in yaml format", + do: func(subCmdCtx subCmdCtx) error { + + flags := subCmdCtx.flagSet(false) + if err := flags.Parse(subCmdCtx.args); err != nil { + return fmt.Errorf("parsing flags: %w", err) + } + + hostBootstrap, err := loadHostBootstrap() + if err != nil { + return fmt.Errorf("loading host bootstrap: %w", err) + } + + caPublicCreds := hostBootstrap.Nebula.CAPublicCredentials + caCert, _, err := cert.UnmarshalNebulaCertificateFromPEM([]byte(caPublicCreds.CertPEM)) + if err != nil { + return fmt.Errorf("unmarshaling ca.crt: %w", err) + } + + if len(caCert.Details.Subnets) != 1 { + return fmt.Errorf( + "malformed ca.crt, contains unexpected subnets %#v", + caCert.Details.Subnets, + ) + } + + subnet := caCert.Details.Subnets[0] + + type outLighthouse struct { + PublicAddr string `yaml:"public_addr"` + IP string `yaml:"ip"` + } + + out := struct { + CACert string `yaml:"ca_cert_pem"` + SubnetCIDR string `yaml:"subnet_cidr"` + Lighthouses []outLighthouse `yaml:"lighthouses"` + }{ + CACert: caPublicCreds.CertPEM, + SubnetCIDR: subnet.String(), + } + + for _, h := range hostBootstrap.Hosts { + if h.Nebula.PublicAddr == "" { + continue + } + + out.Lighthouses = append(out.Lighthouses, outLighthouse{ + PublicAddr: h.Nebula.PublicAddr, + IP: h.IP().String(), + }) + } + + if err := yaml.NewEncoder(os.Stdout).Encode(out); err != nil { + return fmt.Errorf("yaml encoding to stdout: %w", err) + } + + return nil + }, +} + +var subCmdNebula = subCmd{ + name: "nebula", + descr: "Sub-commands related to the nebula VPN", + do: func(subCmdCtx subCmdCtx) error { + return subCmdCtx.doSubCmd( + subCmdNebulaShow, + ) + }, +} diff --git a/go/cmd/entrypoint/nebula_util.go b/go/cmd/entrypoint/nebula_util.go index 2b8d7d6..82a80e9 100644 --- a/go/cmd/entrypoint/nebula_util.go +++ b/go/cmd/entrypoint/nebula_util.go @@ -60,7 +60,7 @@ func nebulaPmuxProcConfig( "pki": map[string]string{ "ca": hostBootstrap.Nebula.CAPublicCredentials.CertPEM, "cert": hostBootstrap.Nebula.HostCredentials.Public.CertPEM, - "key": hostBootstrap.Nebula.HostCredentials.KeyPEM, + "key": hostBootstrap.Nebula.HostCredentials.PrivateKeyPEM, }, "static_host_map": staticHostMap, "punchy": map[string]bool{ diff --git a/go/nebula/nebula.go b/go/nebula/nebula.go index 6150e5c..9ddbe08 100644 --- a/go/nebula/nebula.go +++ b/go/nebula/nebula.go @@ -32,7 +32,7 @@ type HostPublicCredentials struct { // need to be present on a particular host. Each file is PEM encoded. type HostCredentials struct { Public HostPublicCredentials `yaml:"public"` - KeyPEM string `yaml:"key_pem"` + PrivateKeyPEM string `yaml:"key_pem"` // TODO should be private_key_pem SigningPrivateKeyPEM string `yaml:"signing_private_key_pem"` } @@ -51,6 +51,71 @@ type CACredentials struct { SigningPrivateKeyPEM string `yaml:"signing_private_key_pem"` } +// NewHostCertPEM generates and signs a new host certificate containing the +// given public key. +func NewHostCertPEM( + caCreds CACredentials, hostPubPEM string, hostName string, ip net.IP, +) ( + string, error, +) { + hostPub, _, err := cert.UnmarshalX25519PublicKey([]byte(hostPubPEM)) + if err != nil { + return "", fmt.Errorf("unmarshaling public key PEM: %w", err) + } + + caSigningKey, _, err := cert.UnmarshalEd25519PrivateKey([]byte(caCreds.SigningPrivateKeyPEM)) + if err != nil { + return "", fmt.Errorf("unmarshaling ca.key: %w", err) + } + + caCert, _, err := cert.UnmarshalNebulaCertificateFromPEM([]byte(caCreds.Public.CertPEM)) + if err != nil { + return "", fmt.Errorf("unmarshaling ca.crt: %w", err) + } + + issuer, err := caCert.Sha256Sum() + if err != nil { + return "", fmt.Errorf("getting ca.crt issuer: %w", err) + } + + expireAt := caCert.Details.NotAfter.Add(-1 * time.Second) + + subnet := caCert.Details.Subnets[0] + if !subnet.Contains(ip) { + return "", fmt.Errorf("invalid ip %q, not contained by network subnet %q", ip, subnet) + } + + hostCert := cert.NebulaCertificate{ + Details: cert.NebulaCertificateDetails{ + Name: hostName, + Ips: []*net.IPNet{{ + IP: ip, + Mask: subnet.Mask, + }}, + NotBefore: time.Now(), + NotAfter: expireAt, + PublicKey: hostPub, + IsCA: false, + Issuer: issuer, + }, + } + + if err := hostCert.CheckRootConstrains(caCert); err != nil { + return "", fmt.Errorf("validating certificate constraints: %w", err) + } + + if err := hostCert.Sign(caSigningKey); err != nil { + return "", fmt.Errorf("signing host cert with ca.key: %w", err) + } + + hostCertPEM, err := hostCert.MarshalToPEM() + if err != nil { + return "", fmt.Errorf("marshalling host.crt: %w", err) + } + + return string(hostCertPEM), nil +} + // NewHostCredentials generates a new key/cert for a nebula host using the CA // key which will be found in the adminFS. func NewHostCredentials( @@ -62,28 +127,6 @@ func NewHostCredentials( // The logic here is largely based on // https://github.com/slackhq/nebula/blob/v1.4.0/cmd/nebula-cert/sign.go - caSigningKey, _, err := cert.UnmarshalEd25519PrivateKey([]byte(caCreds.SigningPrivateKeyPEM)) - if err != nil { - return HostCredentials{}, fmt.Errorf("unmarshaling ca.key: %w", err) - } - - caCert, _, err := cert.UnmarshalNebulaCertificateFromPEM([]byte(caCreds.Public.CertPEM)) - if err != nil { - return HostCredentials{}, fmt.Errorf("unmarshaling ca.crt: %w", err) - } - - issuer, err := caCert.Sha256Sum() - if err != nil { - return HostCredentials{}, fmt.Errorf("getting ca.crt issuer: %w", err) - } - - expireAt := caCert.Details.NotAfter.Add(-1 * time.Second) - - subnet := caCert.Details.Subnets[0] - if !subnet.Contains(ip) { - return HostCredentials{}, fmt.Errorf("invalid ip %q, not contained by network subnet %q", ip, subnet) - } - signingPubKey, signingPrivKey, err := ed25519.GenerateKey(rand.Reader) if err != nil { panic(fmt.Errorf("generating ed25519 key: %w", err)) @@ -102,42 +145,20 @@ func NewHostCredentials( hostPub, hostKey = pubkey[:], privkey[:] } - hostCert := cert.NebulaCertificate{ - Details: cert.NebulaCertificateDetails{ - Name: hostName, - Ips: []*net.IPNet{{ - IP: ip, - Mask: subnet.Mask, - }}, - NotBefore: time.Now(), - NotAfter: expireAt, - PublicKey: hostPub, - IsCA: false, - Issuer: issuer, - }, - } - - if err := hostCert.CheckRootConstrains(caCert); err != nil { - return HostCredentials{}, fmt.Errorf("validating certificate constraints: %w", err) - } - - if err := hostCert.Sign(caSigningKey); err != nil { - return HostCredentials{}, fmt.Errorf("signing host cert with ca.key: %w", err) - } - + hostPubPEM := cert.MarshalX25519PublicKey(hostPub) hostKeyPEM := cert.MarshalX25519PrivateKey(hostKey) - hostCertPEM, err := hostCert.MarshalToPEM() + hostCertPEM, err := NewHostCertPEM(caCreds, string(hostPubPEM), hostName, ip) if err != nil { - return HostCredentials{}, fmt.Errorf("marshalling host.crt: %w", err) + return HostCredentials{}, fmt.Errorf("creating host certificate: %w", err) } return HostCredentials{ Public: HostPublicCredentials{ - CertPEM: string(hostCertPEM), + CertPEM: hostCertPEM, SigningKeyPEM: string(signingPubKeyPEM), }, - KeyPEM: string(hostKeyPEM), + PrivateKeyPEM: string(hostKeyPEM), SigningPrivateKeyPEM: string(signingPrivKeyPEM), }, nil }