diff --git a/go-workspace/src/admin/admin.go b/go-workspace/src/admin/admin.go index 0dbac5a..ad8bdc6 100644 --- a/go-workspace/src/admin/admin.go +++ b/go-workspace/src/admin/admin.go @@ -27,8 +27,8 @@ const ( // CreationParams are general parameters used when creating a new network. These // are available to all hosts within the network via their bootstrap files. type CreationParams struct { - Domain string `yaml:"domain"` - CIDRs []string `yaml:"cidrs"` + ID string `yaml:"id"` + Domain string `yaml:"domain"` } // Admin is used for accessing all information contained within an admin.tgz. diff --git a/go-workspace/src/cmd/entrypoint/admin.go b/go-workspace/src/cmd/entrypoint/admin.go index 497090f..1ab0ba9 100644 --- a/go-workspace/src/cmd/entrypoint/admin.go +++ b/go-workspace/src/cmd/entrypoint/admin.go @@ -1,14 +1,22 @@ package entrypoint import ( + "context" + crypticnet "cryptic-net" "cryptic-net/admin" "cryptic-net/bootstrap" + "cryptic-net/garage" "cryptic-net/nebula" "crypto/rand" "encoding/hex" "errors" "fmt" + "net" "os" + "strconv" + "strings" + + "github.com/cryptic-io/pmux/pmuxlib" ) func randStr(l int) string { @@ -40,6 +48,236 @@ func readAdmin(path string) (admin.Admin, error) { return admin.FromReader(f) } +func garageInitializeGlobalBucket( + env *crypticnet.Env, globalBucketCreds garage.S3APICredentials, +) error { + + var ( + ctx = env.Context + thisHost = env.Bootstrap.ThisHost() + thisDaemon = env.ThisDaemon() + allocs = thisDaemon.Storage.Allocations + ) + + adminClient := garage.NewAdminClient( + net.JoinHostPort(thisHost.Nebula.IP, strconv.Itoa(allocs[0].AdminPort)), + env.Bootstrap.GarageAdminToken, + ) + + // first attempt to import the key + err := adminClient.Do(ctx, nil, "POST", "/v0/key/import", map[string]string{ + "accessKeyId": globalBucketCreds.ID, + "secretAccessKey": globalBucketCreds.Secret, + "name": "shared-global-bucket-key", + }) + + if err != nil { + return fmt.Errorf("importing global bucket key into garage: %w", err) + } + + // create global bucket + err = adminClient.Do(ctx, nil, "POST", "/v0/bucket", map[string]string{ + "globalAlias": garage.GlobalBucket, + }) + + if err != nil { + return fmt.Errorf("creating global bucket: %w", err) + } + + // retrieve newly created bucket's id + var getBucketRes struct { + ID string `json:"id"` + } + + err = adminClient.Do( + ctx, &getBucketRes, + "GET", "/v0/bucket?globalAlias="+garage.GlobalBucket, nil, + ) + + if err != nil { + return fmt.Errorf("fetching global bucket id: %w", err) + } + + // allow shared global bucket key to perform all operations + err = adminClient.Do(ctx, nil, "POST", "/v0/bucket/allow", map[string]interface{}{ + "bucketId": getBucketRes.ID, + "accessKeyId": globalBucketCreds.ID, + "permissions": map[string]bool{ + "read": true, + "write": true, + }, + }) + + if err != nil { + return fmt.Errorf("granting permissions to shared global bucket key: %w", err) + } + + return nil +} + +var subCmdAdminCreateNetwork = subCmd{ + name: "create-network", + descr: "Creates a new cryptic-net network, outputting the resulting admin.tgz to stdout", + do: func(subCmdCtx subCmdCtx) error { + + flags := subCmdCtx.flagSet(false) + + daemonYmlPath := flags.StringP( + "config-path", "c", "", + "Optional path to a daemon.yml file to load configuration from.", + ) + + dumpConfig := flags.Bool( + "dump-config", false, + "Write the default configuration file to stdout and exit.", + ) + + domain := flags.StringP( + "domain", "d", "", + "Domain name that should be used as the root domain in the network.", + ) + + subnetStr := flags.StringP( + "subnet", "s", "", + "CIDR which denotes the subnet that IPs hosts on the network can be assigned.", + ) + + if err := flags.Parse(subCmdCtx.args); err != nil { + return fmt.Errorf("parsing flags: %w", err) + } + + env := subCmdCtx.env + + if *dumpConfig { + return writeBuiltinDaemonYml(env, os.Stdout) + } + + if *domain == "" || *subnetStr == "" { + return errors.New("--domain and --subnet are required") + } + + *domain = strings.TrimRight(strings.TrimLeft(*domain, "."), ".") + + ip, subnet, err := net.ParseCIDR(*subnetStr) + if err != nil { + return fmt.Errorf("parsing %q as a CIDR: %w", *subnetStr, err) + } + + hostName := "genesis" + + adminCreationParams := admin.CreationParams{ + ID: randStr(32), + Domain: *domain, + } + + garageRPCSecret := randStr(32) + + { + runtimeDirPath := env.RuntimeDirPath + + fmt.Fprintf(os.Stderr, "will use runtime directory %q for temporary state\n", runtimeDirPath) + + if err := os.MkdirAll(runtimeDirPath, 0700); err != nil { + return fmt.Errorf("creating directory %q: %w", runtimeDirPath, err) + } + + defer func() { + fmt.Fprintf(os.Stderr, "cleaning up runtime directory %q\n", runtimeDirPath) + if err := os.RemoveAll(runtimeDirPath); err != nil { + fmt.Fprintf(os.Stderr, "error removing temporary directory %q: %v", runtimeDirPath, err) + } + }() + } + + if err := writeMergedDaemonYml(env, *daemonYmlPath); err != nil { + return fmt.Errorf("merging and writing daemon.yml file: %w", err) + } + + daemon := env.ThisDaemon() + + if len(daemon.Storage.Allocations) < 3 { + return fmt.Errorf("daemon.yml with at least 3 allocations was not provided") + } + + nebulaCACert, err := nebula.NewCACert(*domain, subnet) + if err != nil { + return fmt.Errorf("creating nebula CA cert: %w", err) + } + + nebulaHostCert, err := nebula.NewHostCert(nebulaCACert, hostName, ip) + if err != nil { + return fmt.Errorf("creating nebula cert for host: %w", err) + } + + host := bootstrap.Host{ + Name: hostName, + Nebula: bootstrap.NebulaHost{ + IP: ip.String(), + }, + } + + env.Bootstrap = bootstrap.Bootstrap{ + AdminCreationParams: adminCreationParams, + Hosts: map[string]bootstrap.Host{ + hostName: host, + }, + HostName: hostName, + NebulaHostCert: nebulaHostCert, + GarageRPCSecret: garageRPCSecret, + } + + // this will also write the bootstrap file + if err := mergeDaemonIntoBootstrap(env); err != nil { + return fmt.Errorf("merging daemon.yml into bootstrap data: %w", err) + } + + for key, val := range env.ToMap() { + if err := os.Setenv(key, val); err != nil { + return fmt.Errorf("failed to set %q to %q: %w", key, val, err) + } + } + + garageChildrenPmuxProcConfigs, err := garageChildrenPmuxProcConfigs(env) + if err != nil { + return fmt.Errorf("generating garage children configs: %w", err) + } + + pmuxConfig := pmuxlib.Config{ + Processes: append( + []pmuxlib.ProcessConfig{ + nebulaEntrypointPmuxProcConfig(), + garageApplyLayoutDiffPmuxProcConfig(env), + }, + garageChildrenPmuxProcConfigs..., + ), + } + + ctx, cancel := context.WithCancel(env.Context) + pmuxDoneCh := make(chan struct{}) + + go func() { + pmuxlib.Run(ctx, pmuxConfig) + close(pmuxDoneCh) + }() + + defer func() { + cancel() + <-pmuxDoneCh + }() + + globalBucketCreds := garage.S3APICredentials{} // TODO + + // TODO wait for garage to be confirmed as booted up + // TODO apply layout + + if err := garageInitializeGlobalBucket(env, globalBucketCreds); err != nil { + return fmt.Errorf("initializing shared global bucket: %w", err) + } + + panic("TODO: create and output admin.tgz") + }, +} + var subCmdAdminMakeBootstrap = subCmd{ name: "make-bootstrap", descr: "Creates a new bootstrap.tgz file for a particular host and writes it to stdout", @@ -92,7 +330,12 @@ var subCmdAdminMakeBootstrap = subCmd{ 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) + ip := net.ParseIP(host.Nebula.IP) + if ip == nil { + return fmt.Errorf("invalid IP stored with host %q: %q", *name, host.Nebula.IP) + } + + nebulaHostCert, err := nebula.NewHostCert(adm.NebulaCACert, host.Name, ip) if err != nil { return fmt.Errorf("creating new nebula host key/cert: %w", err) } diff --git a/go-workspace/src/nebula/nebula.go b/go-workspace/src/nebula/nebula.go index eab83e9..3d39ab3 100644 --- a/go-workspace/src/nebula/nebula.go +++ b/go-workspace/src/nebula/nebula.go @@ -14,15 +14,6 @@ import ( "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 { @@ -41,7 +32,7 @@ type CACert struct { // NewHostCert generates a new key/cert for a nebula host using the CA key // which will be found in the adminFS. func NewHostCert( - caCert CACert, hostName, hostIP string, + caCert CACert, hostName string, ip net.IP, ) ( HostCert, error, ) { @@ -66,14 +57,9 @@ func NewHostCert( expireAt := caCrt.Details.NotAfter.Add(-1 * time.Second) - ip := net.ParseIP(hostIP) - if ip == nil { - return HostCert{}, fmt.Errorf("invalid host ip %q", hostIP) - } - - ipNet := &net.IPNet{ - IP: ip, - Mask: ipCIDRMask, + subnet := caCrt.Details.Subnets[0] + if !subnet.Contains(ip) { + return HostCert{}, fmt.Errorf("invalid ip %q, not contained by network subnet %q", ip, subnet) } var hostPub, hostKey []byte @@ -88,8 +74,11 @@ func NewHostCert( hostCrt := cert.NebulaCertificate{ Details: cert.NebulaCertificateDetails{ - Name: hostName, - Ips: []*net.IPNet{ipNet}, + Name: hostName, + Ips: []*net.IPNet{{ + IP: ip, + Mask: subnet.Mask, + }}, NotBefore: time.Now(), NotAfter: expireAt, PublicKey: hostPub, @@ -122,7 +111,7 @@ func NewHostCert( // NewCACert generates a CACert. The domain should be the network's root domain, // and is included in the signing certificate's Name field. -func NewCACert(domain string) (CACert, error) { +func NewCACert(domain string, subnet *net.IPNet) (CACert, error) { pubKey, privKey, err := ed25519.GenerateKey(rand.Reader) if err != nil { @@ -135,6 +124,7 @@ func NewCACert(domain string) (CACert, error) { caCrt := cert.NebulaCertificate{ Details: cert.NebulaCertificateDetails{ Name: fmt.Sprintf("%s cryptic-net root cert", domain), + Subnets: []*net.IPNet{subnet}, NotBefore: now, NotAfter: expireAt, PublicKey: pubKey,