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:
parent
661e2b28cb
commit
3d6ed8604a
@ -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,
|
||||
)
|
||||
},
|
||||
}
|
||||
|
@ -70,6 +70,7 @@ func main() {
|
||||
subCmdDaemon,
|
||||
subCmdGarage,
|
||||
subCmdHosts,
|
||||
subCmdNebula,
|
||||
subCmdVersion,
|
||||
)
|
||||
|
||||
|
82
go/cmd/entrypoint/nebula.go
Normal file
82
go/cmd/entrypoint/nebula.go
Normal 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,
|
||||
)
|
||||
},
|
||||
}
|
@ -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{
|
||||
|
@ -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
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user