7dceb659ef
This allows each host to verify the cert against the CA cert. We also now have each host sign the yaml file that it posts to garage, to ensure that a host can't arbitrarily overwrite another host's file.
212 lines
5.2 KiB
Go
212 lines
5.2 KiB
Go
// Package bootstrap deals with the parsing and creation of bootstrap.tgz files.
|
|
// It also contains some helpers which rely on bootstrap data.
|
|
package bootstrap
|
|
|
|
import (
|
|
"cryptic-net/admin"
|
|
"cryptic-net/garage"
|
|
"cryptic-net/nebula"
|
|
"cryptic-net/tarutil"
|
|
"cryptic-net/yamlutil"
|
|
"crypto/sha512"
|
|
"fmt"
|
|
"io"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
// Paths within the bootstrap FS which for general data.
|
|
const (
|
|
adminCreationParamsPath = "admin/creation-params.yml"
|
|
hostNamePath = "hostname"
|
|
)
|
|
|
|
// DataDirPath returns the path within the user's data directory where the
|
|
// bootstrap file is stored.
|
|
func DataDirPath(dataDirPath string) string {
|
|
return filepath.Join(dataDirPath, "bootstrap.tgz")
|
|
}
|
|
|
|
// AppDirPath returns the path within the AppDir where an embedded bootstrap
|
|
// file might be found.
|
|
func AppDirPath(appDirPath string) string {
|
|
return filepath.Join(appDirPath, "share/bootstrap.tgz")
|
|
}
|
|
|
|
// Bootstrap is used for accessing all information contained within a
|
|
// bootstrap.tgz file.
|
|
type Bootstrap struct {
|
|
AdminCreationParams admin.CreationParams
|
|
|
|
Hosts map[string]Host
|
|
HostName string
|
|
|
|
NebulaHostCredentials nebula.HostCredentials
|
|
|
|
GarageRPCSecret string
|
|
GarageAdminToken string
|
|
GarageGlobalBucketS3APICredentials garage.S3APICredentials
|
|
}
|
|
|
|
// FromFS loads a Boostrap instance from the given fs.FS, which presumably
|
|
// represents the file structure of a bootstrap.tgz file.
|
|
func FromFS(bootstrapFS fs.FS) (Bootstrap, error) {
|
|
|
|
var (
|
|
b Bootstrap
|
|
err error
|
|
)
|
|
|
|
if b.Hosts, err = loadHosts(bootstrapFS); err != nil {
|
|
return Bootstrap{}, fmt.Errorf("loading hosts info from fs: %w", err)
|
|
}
|
|
|
|
filesToLoadAsYAML := []struct {
|
|
into interface{}
|
|
path string
|
|
}{
|
|
{&b.AdminCreationParams, adminCreationParamsPath},
|
|
{&b.GarageGlobalBucketS3APICredentials, garageGlobalBucketKeyYmlPath},
|
|
}
|
|
|
|
for _, f := range filesToLoadAsYAML {
|
|
if err := yamlutil.LoadYamlFSFile(f.into, bootstrapFS, f.path); err != nil {
|
|
return Bootstrap{}, fmt.Errorf("loading %q from fs: %w", f.path, err)
|
|
}
|
|
}
|
|
|
|
filesToLoadAsString := []struct {
|
|
into *string
|
|
path string
|
|
}{
|
|
{&b.HostName, hostNamePath},
|
|
{&b.NebulaHostCredentials.CACertPEM, nebulaCertsCACertPath},
|
|
{&b.NebulaHostCredentials.HostCertPEM, nebulaCertsHostCertPath},
|
|
{&b.NebulaHostCredentials.HostKeyPEM, nebulaCertsHostKeyPath},
|
|
{&b.GarageRPCSecret, garageRPCSecretPath},
|
|
{&b.GarageAdminToken, garageAdminTokenPath},
|
|
}
|
|
|
|
for _, f := range filesToLoadAsString {
|
|
body, err := fs.ReadFile(bootstrapFS, f.path)
|
|
if err != nil {
|
|
return Bootstrap{}, fmt.Errorf("loading %q from fs: %w", f.path, err)
|
|
}
|
|
*f.into = string(body)
|
|
}
|
|
|
|
return b, nil
|
|
}
|
|
|
|
// FromReader reads a bootstrap.tgz file from the given io.Reader.
|
|
func FromReader(r io.Reader) (Bootstrap, error) {
|
|
|
|
fs, err := tarutil.FSFromReader(r)
|
|
if err != nil {
|
|
return Bootstrap{}, fmt.Errorf("reading bootstrap.tgz: %w", err)
|
|
}
|
|
|
|
return FromFS(fs)
|
|
}
|
|
|
|
// FromFile reads a bootstrap.tgz from a file at the given path.
|
|
func FromFile(path string) (Bootstrap, error) {
|
|
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
return Bootstrap{}, fmt.Errorf("opening file: %w", err)
|
|
}
|
|
defer f.Close()
|
|
|
|
return FromReader(f)
|
|
}
|
|
|
|
// WriteTo writes the Bootstrap as a new bootstrap.tgz to the given io.Writer.
|
|
func (b Bootstrap) WriteTo(into io.Writer) error {
|
|
|
|
w := tarutil.NewTGZWriter(into)
|
|
|
|
for _, host := range b.Hosts {
|
|
|
|
hostB, err := yaml.Marshal(host)
|
|
if err != nil {
|
|
return fmt.Errorf("yaml encoding host %#v: %w", host, err)
|
|
}
|
|
|
|
path := filepath.Join(hostsDirPath, host.Name+".yml")
|
|
|
|
w.WriteFileBytes(path, hostB)
|
|
}
|
|
|
|
filesToWriteAsYAML := []struct {
|
|
value interface{}
|
|
path string
|
|
}{
|
|
{b.AdminCreationParams, adminCreationParamsPath},
|
|
{b.GarageGlobalBucketS3APICredentials, garageGlobalBucketKeyYmlPath},
|
|
}
|
|
|
|
for _, f := range filesToWriteAsYAML {
|
|
|
|
b, err := yaml.Marshal(f.value)
|
|
if err != nil {
|
|
return fmt.Errorf("yaml encoding data for %q: %w", f.path, err)
|
|
}
|
|
|
|
w.WriteFileBytes(f.path, b)
|
|
}
|
|
|
|
filesToWriteAsString := []struct {
|
|
value string
|
|
path string
|
|
}{
|
|
{b.HostName, hostNamePath},
|
|
{b.NebulaHostCredentials.CACertPEM, nebulaCertsCACertPath},
|
|
{b.NebulaHostCredentials.HostCertPEM, nebulaCertsHostCertPath},
|
|
{b.NebulaHostCredentials.HostKeyPEM, nebulaCertsHostKeyPath},
|
|
{b.GarageRPCSecret, garageRPCSecretPath},
|
|
{b.GarageAdminToken, garageAdminTokenPath},
|
|
}
|
|
|
|
for _, f := range filesToWriteAsString {
|
|
w.WriteFileBytes(f.path, []byte(f.value))
|
|
}
|
|
|
|
return w.Close()
|
|
}
|
|
|
|
// ThisHost is a shortcut for b.Hosts[b.HostName], but will panic if the
|
|
// HostName isn't found in the Hosts map.
|
|
func (b Bootstrap) ThisHost() Host {
|
|
|
|
host, ok := b.Hosts[b.HostName]
|
|
if !ok {
|
|
panic(fmt.Sprintf("hostname %q not defined in bootstrap's hosts", b.HostName))
|
|
}
|
|
|
|
return host
|
|
}
|
|
|
|
// Hash returns a deterministic hash of the given hosts map.
|
|
func HostsHash(hostsMap map[string]Host) ([]byte, error) {
|
|
|
|
hosts := make([]Host, 0, len(hostsMap))
|
|
for _, host := range hostsMap {
|
|
hosts = append(hosts, host)
|
|
}
|
|
|
|
sort.Slice(hosts, func(i, j int) bool { return hosts[i].Name < hosts[j].Name })
|
|
|
|
h := sha512.New()
|
|
|
|
if err := yaml.NewEncoder(h).Encode(hosts); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return h.Sum(nil), nil
|
|
}
|