Refactor how bootstrap files are created
The new code makes it a lot clearer what the sources of each file/directory is, and makes it more difficult to forget to add a file or directory. This will be helpful when it comes to bootstrapping an entire network, which will require yet a third way of generating bootstrap files.
This commit is contained in:
parent
24b7fe6339
commit
0e41a06121
@ -2,6 +2,10 @@
|
|||||||
package bootstrap
|
package bootstrap
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
crypticnet "cryptic-net"
|
||||||
|
"cryptic-net/garage"
|
||||||
|
"cryptic-net/nebula"
|
||||||
"cryptic-net/tarutil"
|
"cryptic-net/tarutil"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@ -32,3 +36,96 @@ func GetHashFromReader(r io.Reader) ([]byte, error) {
|
|||||||
|
|
||||||
return GetHashFromFS(bootstrapFS)
|
return GetHashFromFS(bootstrapFS)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newBootstrap(
|
||||||
|
ctx context.Context,
|
||||||
|
into io.Writer,
|
||||||
|
hostname provider,
|
||||||
|
nebulaCerts provider,
|
||||||
|
nebulaHosts provider,
|
||||||
|
garageRPCSecret provider,
|
||||||
|
garageGlobalBucketKey provider,
|
||||||
|
garageHosts provider,
|
||||||
|
) error {
|
||||||
|
|
||||||
|
pairs := []struct {
|
||||||
|
path string
|
||||||
|
provider provider
|
||||||
|
}{
|
||||||
|
{"hostname", hostname},
|
||||||
|
{"nebula/certs", nebulaCerts},
|
||||||
|
{"nebula/hosts", nebulaHosts},
|
||||||
|
{"garage/rpc-secret.txt", garageRPCSecret},
|
||||||
|
{"garage/global-bucket-key.yml", garageGlobalBucketKey},
|
||||||
|
{"garage/hosts", garageHosts},
|
||||||
|
}
|
||||||
|
|
||||||
|
w := tarutil.NewTGZWriter(into)
|
||||||
|
|
||||||
|
for _, pair := range pairs {
|
||||||
|
|
||||||
|
if err := pair.provider(ctx, w, pair.path); err != nil {
|
||||||
|
return fmt.Errorf("populating %q in new bootstrap: %w", pair.path, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return w.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewForThisHost generates a new bootstrap file for the current host, based on
|
||||||
|
// the existing environment as well as data in garage.
|
||||||
|
func NewForThisHost(env *crypticnet.Env, into io.Writer) error {
|
||||||
|
|
||||||
|
client, err := garage.GlobalBucketAPIClient(env)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("creating client for global bucket: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return newBootstrap(
|
||||||
|
env.Context,
|
||||||
|
into,
|
||||||
|
provideFromFS(env.BootstrapFS), // hostname
|
||||||
|
provideDirFromFS(env.BootstrapFS), // nebulaCerts
|
||||||
|
provideDirFromGarage(client), // nebulaHosts
|
||||||
|
provideFromFS(env.BootstrapFS), // garageRPCSecret
|
||||||
|
provideFromFS(env.BootstrapFS), // garageGlobalBucketKey
|
||||||
|
provideDirFromGarage(client), // garageHosts
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewForHost generates a new bootstrap file for an arbitrary host, based on the
|
||||||
|
// given admin file's FS and data in garage.
|
||||||
|
func NewForHost(env *crypticnet.Env, adminFS fs.FS, name string, into io.Writer) error {
|
||||||
|
|
||||||
|
host, ok := env.Hosts[name]
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("unknown host %q, make sure host entry has been created", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := garage.GlobalBucketAPIClient(env)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("creating client for global bucket: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
nebulaHostCert, err := nebula.NewHostCert(adminFS, host.Nebula)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("creating new nebula host key/cert: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
nebulaHostCertDir := map[string][]byte{
|
||||||
|
"ca.crt": nebulaHostCert.CACert,
|
||||||
|
"host.key": nebulaHostCert.HostKey,
|
||||||
|
"host.crt": nebulaHostCert.HostCert,
|
||||||
|
}
|
||||||
|
|
||||||
|
return newBootstrap(
|
||||||
|
env.Context,
|
||||||
|
into,
|
||||||
|
provideFromBytes([]byte(name)), // hostname
|
||||||
|
provideDirFromMap(nebulaHostCertDir), // nebulaCerts
|
||||||
|
provideDirFromGarage(client), // nebulaHosts
|
||||||
|
provideFromFS(adminFS), // garageRPCSecret
|
||||||
|
provideFromFS(adminFS), // garageGlobalBucketKey
|
||||||
|
provideDirFromGarage(client), // garageHosts
|
||||||
|
)
|
||||||
|
}
|
||||||
|
@ -1,161 +0,0 @@
|
|||||||
package bootstrap
|
|
||||||
|
|
||||||
import (
|
|
||||||
crypticnet "cryptic-net"
|
|
||||||
"cryptic-net/garage"
|
|
||||||
"cryptic-net/tarutil"
|
|
||||||
"crypto/rand"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"io/fs"
|
|
||||||
"net"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/slackhq/nebula/cert"
|
|
||||||
"golang.org/x/crypto/curve25519"
|
|
||||||
)
|
|
||||||
|
|
||||||
var ipCIDRMask = func() net.IPMask {
|
|
||||||
_, ipNet, err := net.ParseCIDR("10.10.0.0/16")
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return ipNet.Mask
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Generates a new key/cert for a nebula host, writing their encoded forms into
|
|
||||||
// the given TGZWriter. It will also write the ca.crt file to the TGZWriter.
|
|
||||||
//
|
|
||||||
// The logic here is largely based on
|
|
||||||
// https://github.com/slackhq/nebula/blob/v1.4.0/cmd/nebula-cert/sign.go
|
|
||||||
func writeNewNebulaCert(
|
|
||||||
w *tarutil.TGZWriter, adminFS fs.FS, host crypticnet.NebulaHost,
|
|
||||||
) error {
|
|
||||||
|
|
||||||
caKeyPEM, err := fs.ReadFile(adminFS, "nebula/certs/ca.key")
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("reading ca.key from admin fs: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
caKey, _, err := cert.UnmarshalEd25519PrivateKey(caKeyPEM)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("unmarshaling ca.key: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
caCrtPEM, err := fs.ReadFile(adminFS, "nebula/certs/ca.crt")
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("reading ca.crt from admin fs: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
caCrt, _, err := cert.UnmarshalNebulaCertificateFromPEM(caCrtPEM)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("unmarshaling ca.crt: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
issuer, err := caCrt.Sha256Sum()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("getting ca.crt issuer: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
expireAt := caCrt.Details.NotAfter.Add(-1 * time.Second)
|
|
||||||
|
|
||||||
ip := net.ParseIP(host.IP)
|
|
||||||
if ip == nil {
|
|
||||||
return fmt.Errorf("invalid host ip %q", host.IP)
|
|
||||||
}
|
|
||||||
|
|
||||||
ipNet := &net.IPNet{
|
|
||||||
IP: ip,
|
|
||||||
Mask: ipCIDRMask,
|
|
||||||
}
|
|
||||||
|
|
||||||
var hostPub, hostKey []byte
|
|
||||||
{
|
|
||||||
var pubkey, privkey [32]byte
|
|
||||||
if _, err := io.ReadFull(rand.Reader, privkey[:]); err != nil {
|
|
||||||
return fmt.Errorf("reading random bytes to form private key: %w", err)
|
|
||||||
}
|
|
||||||
curve25519.ScalarBaseMult(&pubkey, &privkey)
|
|
||||||
hostPub, hostKey = pubkey[:], privkey[:]
|
|
||||||
}
|
|
||||||
|
|
||||||
hostCrt := cert.NebulaCertificate{
|
|
||||||
Details: cert.NebulaCertificateDetails{
|
|
||||||
Name: host.Name,
|
|
||||||
Ips: []*net.IPNet{ipNet},
|
|
||||||
NotBefore: time.Now(),
|
|
||||||
NotAfter: expireAt,
|
|
||||||
PublicKey: hostPub,
|
|
||||||
IsCA: false,
|
|
||||||
Issuer: issuer,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := hostCrt.CheckRootConstrains(caCrt); err != nil {
|
|
||||||
return fmt.Errorf("validating certificate constraints: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := hostCrt.Sign(caKey); err != nil {
|
|
||||||
return fmt.Errorf("signing host cert with ca.key: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
hostKeyPEM := cert.MarshalX25519PrivateKey(hostKey)
|
|
||||||
|
|
||||||
hostCrtPEM, err := hostCrt.MarshalToPEM()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("marshalling host.crt: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
w.WriteFileBytes("nebula/certs/ca.crt", caCrtPEM)
|
|
||||||
w.WriteFileBytes("nebula/certs/host.key", hostKeyPEM)
|
|
||||||
w.WriteFileBytes("nebula/certs/host.crt", hostCrtPEM)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewForHost generates a new bootstrap file for an arbitrary host, based on the
|
|
||||||
// given admin file's FS and data in garage.
|
|
||||||
func NewForHost(env *crypticnet.Env, adminFS fs.FS, name string, into io.Writer) error {
|
|
||||||
|
|
||||||
host, ok := env.Hosts[name]
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("unknown host %q, make sure host entry has been created", name)
|
|
||||||
}
|
|
||||||
|
|
||||||
client, err := garage.GlobalBucketAPIClient(env)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("creating client for global bucket: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
w := tarutil.NewTGZWriter(into)
|
|
||||||
|
|
||||||
w.WriteFileBytes("hostname", []byte(name))
|
|
||||||
|
|
||||||
if err := writeNewNebulaCert(w, adminFS, host.Nebula); err != nil {
|
|
||||||
return fmt.Errorf("creating/adding host's nebula certs: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fsFilesToCopy := []string{
|
|
||||||
"garage/rpc-secret.txt",
|
|
||||||
"garage/cryptic-net-global-bucket-key.yml",
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, filePath := range fsFilesToCopy {
|
|
||||||
if err := copyFSFile(w, adminFS, filePath); err != nil {
|
|
||||||
return fmt.Errorf("copying %q from bootstrap fs: %w", filePath, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
garageDirsToCopy := []string{
|
|
||||||
"nebula/hosts",
|
|
||||||
"garage/hosts",
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, dirPath := range garageDirsToCopy {
|
|
||||||
if err := copyGarageDir(env.Context, client, w, dirPath); err != nil {
|
|
||||||
return fmt.Errorf("copying %q from garage: %w", dirPath, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return w.Close()
|
|
||||||
}
|
|
@ -1,49 +0,0 @@
|
|||||||
package bootstrap
|
|
||||||
|
|
||||||
import (
|
|
||||||
crypticnet "cryptic-net"
|
|
||||||
"cryptic-net/garage"
|
|
||||||
"cryptic-net/tarutil"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
)
|
|
||||||
|
|
||||||
// NewForThisHost generates a new bootstrap file for the current host, based on
|
|
||||||
// the existing environment as well as data in garage.
|
|
||||||
func NewForThisHost(env *crypticnet.Env, into io.Writer) error {
|
|
||||||
|
|
||||||
client, err := garage.GlobalBucketAPIClient(env)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("creating client for global bucket: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
w := tarutil.NewTGZWriter(into)
|
|
||||||
|
|
||||||
fsFilesToCopy := []string{
|
|
||||||
"hostname",
|
|
||||||
"nebula/certs/ca.crt",
|
|
||||||
"nebula/certs/host.crt",
|
|
||||||
"nebula/certs/host.key",
|
|
||||||
"garage/rpc-secret.txt",
|
|
||||||
"garage/cryptic-net-global-bucket-key.yml",
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, filePath := range fsFilesToCopy {
|
|
||||||
if err := copyFSFile(w, env.BootstrapFS, filePath); err != nil {
|
|
||||||
return fmt.Errorf("copying %q from bootstrap fs: %w", filePath, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
garageDirsToCopy := []string{
|
|
||||||
"nebula/hosts",
|
|
||||||
"garage/hosts",
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, dirPath := range garageDirsToCopy {
|
|
||||||
if err := copyGarageDir(env.Context, client, w, dirPath); err != nil {
|
|
||||||
return fmt.Errorf("copying %q from garage: %w", dirPath, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return w.Close()
|
|
||||||
}
|
|
139
go-workspace/src/bootstrap/provider.go
Normal file
139
go-workspace/src/bootstrap/provider.go
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
package bootstrap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"cryptic-net/garage"
|
||||||
|
"cryptic-net/tarutil"
|
||||||
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/minio/minio-go/v7"
|
||||||
|
)
|
||||||
|
|
||||||
|
// provider is a function which will populate the given filePath into the given
|
||||||
|
// TGZWriter. The path may be a file or a directory.
|
||||||
|
type provider func(context.Context, *tarutil.TGZWriter, string) error
|
||||||
|
|
||||||
|
func provideFromBytes(body []byte) provider {
|
||||||
|
|
||||||
|
return func(
|
||||||
|
ctx context.Context,
|
||||||
|
w *tarutil.TGZWriter,
|
||||||
|
filePath string,
|
||||||
|
) error {
|
||||||
|
|
||||||
|
w.WriteFileBytes(filePath, body)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func provideFromFS(srcFS fs.FS) provider {
|
||||||
|
|
||||||
|
return func(
|
||||||
|
ctx context.Context,
|
||||||
|
w *tarutil.TGZWriter,
|
||||||
|
filePath string,
|
||||||
|
) error {
|
||||||
|
|
||||||
|
return w.CopyFileFromFS(filePath, srcFS)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func provideDirFromFS(srcFS fs.FS) provider {
|
||||||
|
|
||||||
|
return func(
|
||||||
|
ctx context.Context,
|
||||||
|
w *tarutil.TGZWriter,
|
||||||
|
dirPath string,
|
||||||
|
) error {
|
||||||
|
|
||||||
|
return fs.WalkDir(
|
||||||
|
srcFS, dirPath,
|
||||||
|
func(filePath string, dirEntry fs.DirEntry, err error) error {
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
|
||||||
|
} else if dirEntry.IsDir() {
|
||||||
|
return nil
|
||||||
|
|
||||||
|
} else if err := w.CopyFileFromFS(filePath, srcFS); err != nil {
|
||||||
|
return fmt.Errorf("copying file %q: %w", filePath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func provideDirFromMap(m map[string][]byte) provider {
|
||||||
|
|
||||||
|
return func(
|
||||||
|
ctx context.Context,
|
||||||
|
w *tarutil.TGZWriter,
|
||||||
|
dirPath string,
|
||||||
|
) error {
|
||||||
|
|
||||||
|
for filePath, body := range m {
|
||||||
|
filePath := filepath.Join(dirPath, filePath)
|
||||||
|
w.WriteFileBytes(filePath, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO it'd be great if we could wrap a minio.Client into an fs.FS. That would
|
||||||
|
// get rid of a weird dependency in this package, and clean up this code a ton.
|
||||||
|
func provideDirFromGarage(client *minio.Client) provider {
|
||||||
|
|
||||||
|
return func(
|
||||||
|
ctx context.Context,
|
||||||
|
w *tarutil.TGZWriter,
|
||||||
|
dirPath string,
|
||||||
|
) error {
|
||||||
|
|
||||||
|
objInfoCh := client.ListObjects(
|
||||||
|
ctx, garage.GlobalBucket,
|
||||||
|
minio.ListObjectsOptions{
|
||||||
|
Prefix: dirPath,
|
||||||
|
Recursive: true,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
for objInfo := range objInfoCh {
|
||||||
|
|
||||||
|
if objInfo.Err != nil {
|
||||||
|
return fmt.Errorf("listing objects: %w", objInfo.Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
obj, err := client.GetObject(
|
||||||
|
ctx, garage.GlobalBucket, objInfo.Key, minio.GetObjectOptions{},
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"retrieving object %q from global bucket: %w",
|
||||||
|
objInfo.Key, err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
objStat, err := obj.Stat()
|
||||||
|
if err != nil {
|
||||||
|
obj.Close()
|
||||||
|
return fmt.Errorf(
|
||||||
|
"stating object %q from global bucket: %w",
|
||||||
|
objInfo.Key, err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteFile(objInfo.Key, objStat.Size, obj)
|
||||||
|
obj.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
@ -1,74 +0,0 @@
|
|||||||
package bootstrap
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"cryptic-net/garage"
|
|
||||||
"cryptic-net/tarutil"
|
|
||||||
"fmt"
|
|
||||||
"io/fs"
|
|
||||||
|
|
||||||
"github.com/minio/minio-go/v7"
|
|
||||||
)
|
|
||||||
|
|
||||||
func copyFSFile(w *tarutil.TGZWriter, srcFS fs.FS, path string) error {
|
|
||||||
|
|
||||||
f, err := srcFS.Open(path)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("opening %q in bootstrap fs: %w", path, err)
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
|
|
||||||
fStat, err := f.Stat()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("stating %q from bootstrap fs: %w", path, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
w.WriteFile(path, fStat.Size(), f)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func copyGarageDir(
|
|
||||||
ctx context.Context, client *minio.Client,
|
|
||||||
w *tarutil.TGZWriter, path string,
|
|
||||||
) error {
|
|
||||||
|
|
||||||
objInfoCh := client.ListObjects(
|
|
||||||
ctx, garage.GlobalBucket,
|
|
||||||
minio.ListObjectsOptions{
|
|
||||||
Prefix: path,
|
|
||||||
Recursive: true,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
for objInfo := range objInfoCh {
|
|
||||||
|
|
||||||
if objInfo.Err != nil {
|
|
||||||
return fmt.Errorf("listing objects: %w", objInfo.Err)
|
|
||||||
}
|
|
||||||
|
|
||||||
obj, err := client.GetObject(
|
|
||||||
ctx, garage.GlobalBucket, objInfo.Key, minio.GetObjectOptions{},
|
|
||||||
)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf(
|
|
||||||
"retrieving object %q from global bucket: %w",
|
|
||||||
objInfo.Key, err,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
objStat, err := obj.Stat()
|
|
||||||
if err != nil {
|
|
||||||
obj.Close()
|
|
||||||
return fmt.Errorf(
|
|
||||||
"stating object %q from global bucket: %w",
|
|
||||||
objInfo.Key, err,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
w.WriteFile(objInfo.Key, objStat.Size, obj)
|
|
||||||
obj.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
@ -29,7 +29,7 @@ type APICredentials struct {
|
|||||||
// GlobalBucketAPICredentials returns APICredentials for the global bucket.
|
// GlobalBucketAPICredentials returns APICredentials for the global bucket.
|
||||||
func GlobalBucketAPICredentials(env *crypticnet.Env) (APICredentials, error) {
|
func GlobalBucketAPICredentials(env *crypticnet.Env) (APICredentials, error) {
|
||||||
|
|
||||||
const path = "garage/cryptic-net-global-bucket-key.yml"
|
const path = "garage/priv/global-bucket-key.yml"
|
||||||
|
|
||||||
var creds APICredentials
|
var creds APICredentials
|
||||||
if err := yamlutil.LoadYamlFSFile(&creds, env.BootstrapFS, path); err != nil {
|
if err := yamlutil.LoadYamlFSFile(&creds, env.BootstrapFS, path); err != nil {
|
||||||
|
125
go-workspace/src/nebula/nebula.go
Normal file
125
go-workspace/src/nebula/nebula.go
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
// Package nebula contains helper functions and types which are useful for
|
||||||
|
// setting up nebula configs, processes, and deployments.
|
||||||
|
package nebula
|
||||||
|
|
||||||
|
import (
|
||||||
|
crypticnet "cryptic-net"
|
||||||
|
"crypto/rand"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/fs"
|
||||||
|
"net"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/slackhq/nebula/cert"
|
||||||
|
"golang.org/x/crypto/curve25519"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TODO this should one day not be hardcoded
|
||||||
|
var ipCIDRMask = func() net.IPMask {
|
||||||
|
_, ipNet, err := net.ParseCIDR("10.10.0.0/16")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return ipNet.Mask
|
||||||
|
}()
|
||||||
|
|
||||||
|
// HostCert contains the certificate and private key files which will need to
|
||||||
|
// be present on a particular host. Each file is PEM encoded.
|
||||||
|
type HostCert struct {
|
||||||
|
CACert []byte
|
||||||
|
HostKey []byte
|
||||||
|
HostCert []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewHostCert generates a new key/cert for a nebula host using the CA key
|
||||||
|
// which will be found in the adminFS.
|
||||||
|
func NewHostCert(
|
||||||
|
adminFS fs.FS, host crypticnet.NebulaHost,
|
||||||
|
) (
|
||||||
|
HostCert, error,
|
||||||
|
) {
|
||||||
|
|
||||||
|
// The logic here is largely based on
|
||||||
|
// https://github.com/slackhq/nebula/blob/v1.4.0/cmd/nebula-cert/sign.go
|
||||||
|
|
||||||
|
caKeyPEM, err := fs.ReadFile(adminFS, "nebula/certs/ca.key")
|
||||||
|
if err != nil {
|
||||||
|
return HostCert{}, fmt.Errorf("reading ca.key from admin fs: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
caKey, _, err := cert.UnmarshalEd25519PrivateKey(caKeyPEM)
|
||||||
|
if err != nil {
|
||||||
|
return HostCert{}, fmt.Errorf("unmarshaling ca.key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
caCrtPEM, err := fs.ReadFile(adminFS, "nebula/certs/ca.crt")
|
||||||
|
if err != nil {
|
||||||
|
return HostCert{}, fmt.Errorf("reading ca.crt from admin fs: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
caCrt, _, err := cert.UnmarshalNebulaCertificateFromPEM(caCrtPEM)
|
||||||
|
if err != nil {
|
||||||
|
return HostCert{}, fmt.Errorf("unmarshaling ca.crt: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
issuer, err := caCrt.Sha256Sum()
|
||||||
|
if err != nil {
|
||||||
|
return HostCert{}, fmt.Errorf("getting ca.crt issuer: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expireAt := caCrt.Details.NotAfter.Add(-1 * time.Second)
|
||||||
|
|
||||||
|
ip := net.ParseIP(host.IP)
|
||||||
|
if ip == nil {
|
||||||
|
return HostCert{}, fmt.Errorf("invalid host ip %q", host.IP)
|
||||||
|
}
|
||||||
|
|
||||||
|
ipNet := &net.IPNet{
|
||||||
|
IP: ip,
|
||||||
|
Mask: ipCIDRMask,
|
||||||
|
}
|
||||||
|
|
||||||
|
var hostPub, hostKey []byte
|
||||||
|
{
|
||||||
|
var pubkey, privkey [32]byte
|
||||||
|
if _, err := io.ReadFull(rand.Reader, privkey[:]); err != nil {
|
||||||
|
return HostCert{}, fmt.Errorf("reading random bytes to form private key: %w", err)
|
||||||
|
}
|
||||||
|
curve25519.ScalarBaseMult(&pubkey, &privkey)
|
||||||
|
hostPub, hostKey = pubkey[:], privkey[:]
|
||||||
|
}
|
||||||
|
|
||||||
|
hostCrt := cert.NebulaCertificate{
|
||||||
|
Details: cert.NebulaCertificateDetails{
|
||||||
|
Name: host.Name,
|
||||||
|
Ips: []*net.IPNet{ipNet},
|
||||||
|
NotBefore: time.Now(),
|
||||||
|
NotAfter: expireAt,
|
||||||
|
PublicKey: hostPub,
|
||||||
|
IsCA: false,
|
||||||
|
Issuer: issuer,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := hostCrt.CheckRootConstrains(caCrt); err != nil {
|
||||||
|
return HostCert{}, fmt.Errorf("validating certificate constraints: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := hostCrt.Sign(caKey); err != nil {
|
||||||
|
return HostCert{}, fmt.Errorf("signing host cert with ca.key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
hostKeyPEM := cert.MarshalX25519PrivateKey(hostKey)
|
||||||
|
|
||||||
|
hostCrtPEM, err := hostCrt.MarshalToPEM()
|
||||||
|
if err != nil {
|
||||||
|
return HostCert{}, fmt.Errorf("marshalling host.crt: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return HostCert{
|
||||||
|
CACert: caCrtPEM,
|
||||||
|
HostKey: hostKeyPEM,
|
||||||
|
HostCert: hostCrtPEM,
|
||||||
|
}, nil
|
||||||
|
}
|
@ -7,6 +7,7 @@ import (
|
|||||||
"crypto/sha512"
|
"crypto/sha512"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"io/fs"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
@ -149,3 +150,22 @@ func (w *TGZWriter) WriteFileBytes(path string, body []byte) {
|
|||||||
bodyR := bytes.NewReader(body)
|
bodyR := bytes.NewReader(body)
|
||||||
w.WriteFile(path, bodyR.Size(), bodyR)
|
w.WriteFile(path, bodyR.Size(), bodyR)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CopyFileFromFS copies the file at the given path from srcFS into the same
|
||||||
|
// path in the TGZWriter.
|
||||||
|
func (w *TGZWriter) CopyFileFromFS(path string, srcFS fs.FS) error {
|
||||||
|
|
||||||
|
f, err := srcFS.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("opening: %w", err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
fStat, err := f.Stat()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("stating: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteFile(path, fStat.Size(), f)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user