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.
This commit is contained in:
Brian Picciano 2023-08-27 16:09:03 +02:00
parent 661e2b28cb
commit 3d6ed8604a
5 changed files with 227 additions and 51 deletions

View File

@ -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,
)
},
}

View File

@ -70,6 +70,7 @@ func main() {
subCmdDaemon,
subCmdGarage,
subCmdHosts,
subCmdNebula,
subCmdVersion,
)

View File

@ -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,
)
},
}

View File

@ -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{

View File

@ -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
}