Compare commits

..

No commits in common. "3b195521733855549d3ac05553cb10f8f72b80ac" and "cf52cbff52cbee789d0fb12c49992a831260095f" have entirely different histories.

10 changed files with 126 additions and 166 deletions

View File

@ -11,17 +11,17 @@ tmp="$(mktemp -d -t cryptic-net-dnsmasq-entrypoint-XXX)"
( trap "rm -rf '$tmp'" EXIT ( trap "rm -rf '$tmp'" EXIT
tar xzf "$_BOOTSTRAP_PATH" -C "$tmp" ./hosts tar xzf "$_BOOTSTRAP_PATH" -C "$tmp" ./nebula/hosts
thisHostName=$(tar xzf "$_BOOTSTRAP_PATH" --to-stdout ./hostname) thisHostName=$(tar xzf "$_BOOTSTRAP_PATH" --to-stdout ./hostname)
thisHostIP=$(cat "$tmp"/hosts/"$thisHostName".yml | yq '.nebula.ip') thisHostIP=$(cat "$tmp"/nebula/hosts/"$thisHostName".yml | yq '.ip')
echo "listen-address=$thisHostIP" >> "$conf_path" echo "listen-address=$thisHostIP" >> "$conf_path"
ls -1 "$tmp"/hosts | while read hostYml; do ls -1 "$tmp"/nebula/hosts | while read hostYml; do
hostName=$(echo "$hostYml" | cut -d. -f1) hostName=$(echo "$hostYml" | cut -d. -f1)
hostIP=$(cat "$tmp"/hosts/"$hostYml" | yq '.nebula.ip') hostIP=$(cat "$tmp"/nebula/hosts/"$hostYml" | yq '.ip')
echo "address=/${hostName}.hosts.cryptic.io/$hostIP" >> "$conf_path" echo "address=/${hostName}.hosts.cryptic.io/$hostIP" >> "$conf_path"
done done

View File

@ -47,7 +47,7 @@ state AppDir {
} }
state "./bin/garage -c $_RUNTIME_DIR_PATH/garage-N.toml server" as garage state "./bin/garage -c $_RUNTIME_DIR_PATH/garage-N.toml server" as garage
state "./bin/garage-apply-layout-diff" as garageApplyLayoutDiff { state "./bin/cryptic-net-main garage-apply-layout-diff" as garageApplyLayoutDiff {
garageApplyLayoutDiff : * Runs once then exits garageApplyLayoutDiff : * Runs once then exits
garageApplyLayoutDiff : * Updates cluster topo garageApplyLayoutDiff : * Updates cluster topo
} }

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -5,10 +5,10 @@ tmp="$(mktemp -d -t cryptic-net-garage-apply-layout-diff-XXX)"
( trap "rm -rf '$tmp'" EXIT ( trap "rm -rf '$tmp'" EXIT
tar xzf "$_BOOTSTRAP_PATH" -C "$tmp" ./hosts tar xzf "$_BOOTSTRAP_PATH" -C "$tmp" ./nebula/hosts
thisHostName=$(tar xzf "$_BOOTSTRAP_PATH" --to-stdout ./hostname) thisHostName=$(tar xzf "$_BOOTSTRAP_PATH" --to-stdout ./hostname)
thisHostIP=$(cat "$tmp"/hosts/"$thisHostName".yml | yq '.nebula.ip') thisHostIP=$(cat "$tmp"/nebula/hosts/"$thisHostName".yml | yq '.ip')
firstRPCPort=$(cat "$_DAEMON_YML_PATH" | yq '.storage.allocations[0].rpc_port') firstRPCPort=$(cat "$_DAEMON_YML_PATH" | yq '.storage.allocations[0].rpc_port')

View File

@ -1,122 +0,0 @@
// Package admin deals with the parsing and creation of admin.tgz files.
package admin
import (
"cryptic-net/garage"
"cryptic-net/nebula"
"cryptic-net/tarutil"
"cryptic-net/yamlutil"
"fmt"
"io"
"io/fs"
"gopkg.in/yaml.v3"
)
const (
nebulaCertsCACertPath = "nebula/certs/ca.crt"
nebulaCertsCAKeyPath = "nebula/certs/ca.key"
garageGlobalBucketKeyYmlPath = "garage/cryptic-net-global-bucket-key.yml"
garageAdminBucketKeyYmlPath = "garage/cryptic-net-admin-bucket-key.yml"
garageRPCSecretPath = "garage/rpc-secret.txt"
)
// Admin is used for accessing all information contained within an admin.tgz.
type Admin struct {
NebulaCACert nebula.CACert
GarageRPCSecret string
GarageGlobalBucketS3APICredentials garage.S3APICredentials
GarageAdminBucketS3APICredentials garage.S3APICredentials
}
// FromFS loads an Admin instance from the given fs.FS, which presumably
// represents the file structure of an admin.tgz file.
func FromFS(adminFS fs.FS) (Admin, error) {
var a Admin
filesToLoadAsYAML := []struct {
into interface{}
path string
}{
{&a.GarageGlobalBucketS3APICredentials, garageGlobalBucketKeyYmlPath},
{&a.GarageAdminBucketS3APICredentials, garageAdminBucketKeyYmlPath},
}
for _, f := range filesToLoadAsYAML {
if err := yamlutil.LoadYamlFSFile(f.into, adminFS, f.path); err != nil {
return Admin{}, fmt.Errorf("loading %q from fs: %w", f.path, err)
}
}
filesToLoadAsString := []struct {
into *string
path string
}{
{&a.NebulaCACert.CACert, nebulaCertsCACertPath},
{&a.NebulaCACert.CAKey, nebulaCertsCAKeyPath},
{&a.GarageRPCSecret, garageRPCSecretPath},
}
for _, f := range filesToLoadAsString {
body, err := fs.ReadFile(adminFS, f.path)
if err != nil {
return Admin{}, fmt.Errorf("loading %q from fs: %w", f.path, err)
}
*f.into = string(body)
}
return a, nil
}
// FromReader reads an admin.tgz file from the given io.Reader.
func FromReader(r io.Reader) (Admin, error) {
fs, err := tarutil.FSFromReader(r)
if err != nil {
return Admin{}, fmt.Errorf("reading admin.tgz: %w", err)
}
return FromFS(fs)
}
// WriteTo writes the Admin as a new admin.tgz to the given io.Writer.
func (a Admin) WriteTo(into io.Writer) error {
w := tarutil.NewTGZWriter(into)
filesToWriteAsYAML := []struct {
value interface{}
path string
}{
{a.GarageGlobalBucketS3APICredentials, garageGlobalBucketKeyYmlPath},
{a.GarageAdminBucketS3APICredentials, garageAdminBucketKeyYmlPath},
}
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
}{
{a.NebulaCACert.CACert, nebulaCertsCACertPath},
{a.NebulaCACert.CAKey, nebulaCertsCAKeyPath},
{a.GarageRPCSecret, garageRPCSecretPath},
}
for _, f := range filesToWriteAsString {
w.WriteFileBytes(f.path, []byte(f.value))
}
return w.Close()
}

View File

@ -1,10 +1,8 @@
// Package bootstrap deals with the parsing and creation of bootstrap.tgz files. // Package bootstrap deals with the creation of bootstrap files
// It also contains some helpers which rely on bootstrap data.
package bootstrap package bootstrap
import ( import (
"cryptic-net/garage" "cryptic-net/garage"
"cryptic-net/nebula"
"cryptic-net/tarutil" "cryptic-net/tarutil"
"cryptic-net/yamlutil" "cryptic-net/yamlutil"
"crypto/sha512" "crypto/sha512"
@ -14,6 +12,7 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"sort" "sort"
"strings"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
@ -25,11 +24,16 @@ const (
// Bootstrap is used for accessing all information contained within a // Bootstrap is used for accessing all information contained within a
// bootstrap.tgz file. // bootstrap.tgz file.
//
// An instance of Bootstrap is read-only, the creator sub-package should be used
// to create new instances.
type Bootstrap struct { type Bootstrap struct {
Hosts map[string]Host Hosts map[string]Host
HostName string HostName string
NebulaHostCert nebula.HostCert NebulaCertsCACert string
NebulaCertsHostCert string
NebulaCertsHostKey string
GarageRPCSecret string GarageRPCSecret string
GarageGlobalBucketS3APICredentials garage.S3APICredentials GarageGlobalBucketS3APICredentials garage.S3APICredentials
@ -61,9 +65,9 @@ func FromFS(bootstrapFS fs.FS) (Bootstrap, error) {
path string path string
}{ }{
{&b.HostName, hostNamePath}, {&b.HostName, hostNamePath},
{&b.NebulaHostCert.CACert, nebulaCertsCACertPath}, {&b.NebulaCertsCACert, nebulaCertsCACertPath},
{&b.NebulaHostCert.HostCert, nebulaCertsHostCertPath}, {&b.NebulaCertsHostCert, nebulaCertsHostCertPath},
{&b.NebulaHostCert.HostKey, nebulaCertsHostKeyPath}, {&b.NebulaCertsHostKey, nebulaCertsHostKeyPath},
{&b.GarageRPCSecret, garageRPCSecretPath}, {&b.GarageRPCSecret, garageRPCSecretPath},
} }
@ -75,6 +79,9 @@ func FromFS(bootstrapFS fs.FS) (Bootstrap, error) {
*f.into = string(body) *f.into = string(body)
} }
// TODO confirm if this is necessary
b.GarageRPCSecret = strings.TrimSpace(b.GarageRPCSecret)
return b, nil return b, nil
} }
@ -111,9 +118,9 @@ func (b Bootstrap) WriteTo(into io.Writer) error {
path string path string
}{ }{
{b.HostName, hostNamePath}, {b.HostName, hostNamePath},
{b.NebulaHostCert.CACert, nebulaCertsCACertPath}, {b.NebulaCertsCACert, nebulaCertsCACertPath},
{b.NebulaHostCert.HostCert, nebulaCertsHostCertPath}, {b.NebulaCertsHostCert, nebulaCertsHostCertPath},
{b.NebulaHostCert.HostKey, nebulaCertsHostKeyPath}, {b.NebulaCertsHostKey, nebulaCertsHostKeyPath},
{b.GarageRPCSecret, garageRPCSecretPath}, {b.GarageRPCSecret, garageRPCSecretPath},
} }

View File

@ -1,11 +1,12 @@
package entrypoint package entrypoint
import ( import (
"cryptic-net/admin"
"cryptic-net/bootstrap" "cryptic-net/bootstrap"
"cryptic-net/nebula" "cryptic-net/nebula"
"cryptic-net/tarutil"
"errors" "errors"
"fmt" "fmt"
"io/fs"
"net" "net"
"os" "os"
"regexp" "regexp"
@ -140,25 +141,25 @@ var subCmdHostsDelete = subCmd{
}, },
} }
func readAdmin(path string) (admin.Admin, error) { func readAdminFS(path string) (fs.FS, error) {
if path == "-" { if path == "-" {
adm, err := admin.FromReader(os.Stdin) outFS, err := tarutil.FSFromReader(os.Stdin)
if err != nil { if err != nil {
return admin.Admin{}, fmt.Errorf("parsing admin.tgz from stdin: %w", err) return nil, fmt.Errorf("reading admin.tgz from stdin: %w", err)
} }
return adm, nil return outFS, nil
} }
f, err := os.Open(path) f, err := os.Open(path)
if err != nil { if err != nil {
return admin.Admin{}, fmt.Errorf("opening file: %w", err) return nil, fmt.Errorf("opening file: %w", err)
} }
defer f.Close() defer f.Close()
return admin.FromReader(f) return tarutil.FSFromReader(f)
} }
var subCmdHostsMakeBootstrap = subCmd{ var subCmdHostsMakeBootstrap = subCmd{
@ -189,7 +190,7 @@ var subCmdHostsMakeBootstrap = subCmd{
env := subCmdCtx.env env := subCmdCtx.env
adm, err := readAdmin(*adminPath) adminFS, err := readAdminFS(*adminPath)
if err != nil { if err != nil {
return fmt.Errorf("reading admin.tgz with --admin-path of %q: %w", *adminPath, err) return fmt.Errorf("reading admin.tgz with --admin-path of %q: %w", *adminPath, err)
} }
@ -213,7 +214,7 @@ var subCmdHostsMakeBootstrap = subCmd{
return fmt.Errorf("couldn't find host into for %q in garage, has `cryptic-net hosts add` been run yet?", *name) return fmt.Errorf("couldn't find host into for %q in garage, has `cryptic-net hosts add` been run yet?", *name)
} }
nebulaHostCert, err := nebula.NewHostCert(adm.NebulaCACert, host.Name, host.Nebula.IP) nebulaHostCert, err := nebula.NewHostCert(adminFS, host)
if err != nil { if err != nil {
return fmt.Errorf("creating new nebula host key/cert: %w", err) return fmt.Errorf("creating new nebula host key/cert: %w", err)
} }
@ -222,10 +223,13 @@ var subCmdHostsMakeBootstrap = subCmd{
Hosts: hosts, Hosts: hosts,
HostName: *name, HostName: *name,
NebulaHostCert: nebulaHostCert, NebulaCertsCACert: nebulaHostCert.CACert,
NebulaCertsHostCert: nebulaHostCert.HostCert,
NebulaCertsHostKey: nebulaHostCert.HostKey,
GarageRPCSecret: adm.GarageRPCSecret, // TODO these should use adminFS
GarageGlobalBucketS3APICredentials: adm.GarageGlobalBucketS3APICredentials, GarageRPCSecret: env.Bootstrap.GarageRPCSecret,
GarageGlobalBucketS3APICredentials: env.Bootstrap.GarageGlobalBucketS3APICredentials,
} }
return newBootstrap.WriteTo(os.Stdout) return newBootstrap.WriteTo(os.Stdout)

View File

@ -37,9 +37,9 @@ func Main() {
config := map[string]interface{}{ config := map[string]interface{}{
"pki": map[string]string{ "pki": map[string]string{
"ca": env.Bootstrap.NebulaHostCert.CACert, "ca": env.Bootstrap.NebulaCertsCACert,
"cert": env.Bootstrap.NebulaHostCert.HostCert, "cert": env.Bootstrap.NebulaCertsHostCert,
"key": env.Bootstrap.NebulaHostCert.HostKey, "key": env.Bootstrap.NebulaCertsHostKey,
}, },
"static_host_map": staticHostMap, "static_host_map": staticHostMap,
"punchy": map[string]bool{ "punchy": map[string]bool{

View File

@ -3,10 +3,12 @@
package nebula package nebula
import ( import (
"cryptic-net/bootstrap"
"crypto/ed25519" "crypto/ed25519"
"crypto/rand" "crypto/rand"
"fmt" "fmt"
"io" "io"
"io/fs"
"net" "net"
"time" "time"
@ -41,7 +43,7 @@ type CACert struct {
// NewHostCert generates a new key/cert for a nebula host using the CA key // NewHostCert generates a new key/cert for a nebula host using the CA key
// which will be found in the adminFS. // which will be found in the adminFS.
func NewHostCert( func NewHostCert(
caCert CACert, hostName, hostIP string, adminFS fs.FS, host bootstrap.Host,
) ( ) (
HostCert, error, HostCert, error,
) { ) {
@ -49,12 +51,22 @@ func NewHostCert(
// The logic here is largely based on // The logic here is largely based on
// https://github.com/slackhq/nebula/blob/v1.4.0/cmd/nebula-cert/sign.go // https://github.com/slackhq/nebula/blob/v1.4.0/cmd/nebula-cert/sign.go
caKey, _, err := cert.UnmarshalEd25519PrivateKey([]byte(caCert.CAKey)) 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 { if err != nil {
return HostCert{}, fmt.Errorf("unmarshaling ca.key: %w", err) return HostCert{}, fmt.Errorf("unmarshaling ca.key: %w", err)
} }
caCrt, _, err := cert.UnmarshalNebulaCertificateFromPEM([]byte(caCert.CACert)) 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 { if err != nil {
return HostCert{}, fmt.Errorf("unmarshaling ca.crt: %w", err) return HostCert{}, fmt.Errorf("unmarshaling ca.crt: %w", err)
} }
@ -66,9 +78,9 @@ func NewHostCert(
expireAt := caCrt.Details.NotAfter.Add(-1 * time.Second) expireAt := caCrt.Details.NotAfter.Add(-1 * time.Second)
ip := net.ParseIP(hostIP) ip := net.ParseIP(host.Nebula.IP)
if ip == nil { if ip == nil {
return HostCert{}, fmt.Errorf("invalid host ip %q", hostIP) return HostCert{}, fmt.Errorf("invalid host ip %q", host.Nebula.IP)
} }
ipNet := &net.IPNet{ ipNet := &net.IPNet{
@ -88,7 +100,7 @@ func NewHostCert(
hostCrt := cert.NebulaCertificate{ hostCrt := cert.NebulaCertificate{
Details: cert.NebulaCertificateDetails{ Details: cert.NebulaCertificateDetails{
Name: hostName, Name: host.Name,
Ips: []*net.IPNet{ipNet}, Ips: []*net.IPNet{ipNet},
NotBefore: time.Now(), NotBefore: time.Now(),
NotAfter: expireAt, NotAfter: expireAt,
@ -114,7 +126,7 @@ func NewHostCert(
} }
return HostCert{ return HostCert{
CACert: caCert.CACert, CACert: string(caCrtPEM),
HostKey: string(hostKeyPEM), HostKey: string(hostKeyPEM),
HostCert: string(hostCrtPEM), HostCert: string(hostCrtPEM),
}, nil }, nil

View File

@ -4,21 +4,40 @@ import (
"archive/tar" "archive/tar"
"bytes" "bytes"
"compress/gzip" "compress/gzip"
"crypto/sha512"
"fmt" "fmt"
"io" "io"
"io/fs"
"path/filepath" "path/filepath"
"sort"
"strings" "strings"
) )
const (
// Path to the file containing the content hash of the tgz, which is
// included as part of all tgz files created by TGZWriter.
HashBinPath = "hash.bin"
)
type fileHash struct {
path string
hash []byte
}
// TGZWriter is a utility for writing tgz files. If an internal error is // TGZWriter is a utility for writing tgz files. If an internal error is
// encountered by any method then all subsequent methods will be no-ops, and // encountered by any method then all subsequent methods will be no-ops, and
// Close() will return that error (after closing out resources). // Close() will return that error (after closing out resources).
//
// A `hash.bin` file will be automatically included in the resulting tgz, which
// will contain a consistent hash of all other contents in the tgz file.
type TGZWriter struct { type TGZWriter struct {
gzipW *gzip.Writer gzipW *gzip.Writer
tarW *tar.Writer tarW *tar.Writer
err error err error
dirsWritten map[string]bool dirsWritten map[string]bool
fileHashes []fileHash
} }
// NewTGZWriter initializes and returns a new instance of TGZWriter which will // NewTGZWriter initializes and returns a new instance of TGZWriter which will
@ -36,8 +55,22 @@ func NewTGZWriter(w io.Writer) *TGZWriter {
// Close cleans up all open resources being held by TGZWriter, and returns the // Close cleans up all open resources being held by TGZWriter, and returns the
// first internal error which was encountered during its operation (if any). // first internal error which was encountered during its operation (if any).
func (w *TGZWriter) Close() error { func (w *TGZWriter) Close() error {
sort.Slice(w.fileHashes, func(i, j int) bool {
return w.fileHashes[i].path < w.fileHashes[j].path
})
h := sha512.New()
for i := range w.fileHashes {
fmt.Fprintf(h, "%q:%x\n", w.fileHashes[i].path, w.fileHashes[i].hash)
}
w.WriteFile(HashBinPath, int64(h.Size()), bytes.NewBuffer(h.Sum(nil)))
w.tarW.Close() w.tarW.Close()
w.gzipW.Close() w.gzipW.Close()
return w.err return w.err
} }
@ -98,10 +131,17 @@ func (w *TGZWriter) WriteFile(path string, size int64, body io.Reader) {
return return
} }
if _, err := io.Copy(w.tarW, body); err != nil { h := sha512.New()
if _, err := io.Copy(io.MultiWriter(w.tarW, h), body); err != nil {
w.err = fmt.Errorf("writing file body of file %q: %w", path, err) w.err = fmt.Errorf("writing file body of file %q: %w", path, err)
return return
} }
w.fileHashes = append(w.fileHashes, fileHash{
path: path,
hash: h.Sum(nil),
})
} }
// WriteFileBytes is a shortcut for calling WriteFile with the given byte slice // WriteFileBytes is a shortcut for calling WriteFile with the given byte slice
@ -110,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
}