State CA signing key in secrets store, eliminate admin bundle

This commit is contained in:
Brian Picciano 2024-07-14 13:11:18 +02:00
parent 9d5c8ea4db
commit d2710db8f1
15 changed files with 87 additions and 185 deletions

View File

@ -1,37 +0,0 @@
// Package admin deals with the parsing and creation of admin.json files.
package admin
import (
"encoding/json"
"io"
"isle/nebula"
)
// 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 {
ID string
Name string
Domain string
}
// Admin is used for accessing all information contained within an admin.json.
type Admin struct {
CreationParams CreationParams
Nebula struct {
CACredentials nebula.CACredentials
}
}
// FromReader reads an admin.json from the given io.Reader.
func FromReader(r io.Reader) (Admin, error) {
var a Admin
err := json.NewDecoder(r).Decode(&a)
return a, err
}
// WriteTo writes the Admin as an admin.json to the given io.Writer.
func (a Admin) WriteTo(into io.Writer) error {
return json.NewEncoder(into).Encode(a)
}

View File

@ -6,7 +6,6 @@ import (
"crypto/sha512"
"encoding/json"
"fmt"
"isle/admin"
"isle/nebula"
"net/netip"
"path/filepath"
@ -25,11 +24,19 @@ func AppDirPath(appDirPath string) string {
return filepath.Join(appDirPath, "share/bootstrap.json")
}
// 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 {
ID string
Name string
Domain string
}
// Bootstrap contains all information which is needed by a host daemon to join a
// network on boot.
type Bootstrap struct {
AdminCreationParams admin.CreationParams
CAPublicCredentials nebula.CAPublicCredentials
NetworkCreationParams CreationParams
CAPublicCredentials nebula.CAPublicCredentials
PrivateCredentials nebula.HostPrivateCredentials
HostAssigned `json:"-"`
@ -42,7 +49,7 @@ type Bootstrap struct {
// function assigns Hosts an empty map.
func New(
caCreds nebula.CACredentials,
adminCreationParams admin.CreationParams,
adminCreationParams CreationParams,
name nebula.HostName,
ip netip.Addr,
) (
@ -66,12 +73,12 @@ func New(
}
return Bootstrap{
AdminCreationParams: adminCreationParams,
CAPublicCredentials: caCreds.Public,
PrivateCredentials: hostPrivCreds,
HostAssigned: assigned,
SignedHostAssigned: signedAssigned,
Hosts: map[nebula.HostName]Host{},
NetworkCreationParams: adminCreationParams,
CAPublicCredentials: caCreds.Public,
PrivateCredentials: hostPrivCreds,
HostAssigned: assigned,
SignedHostAssigned: signedAssigned,
Hosts: map[nebula.HostName]Host{},
}, nil
}

View File

@ -1,28 +0,0 @@
package main
import (
"fmt"
"isle/admin"
"os"
)
func readAdmin(path string) (admin.Admin, error) {
if path == "-" {
adm, err := admin.FromReader(os.Stdin)
if err != nil {
return admin.Admin{}, fmt.Errorf("parsing admin.json from stdin: %w", err)
}
return adm, nil
}
f, err := os.Open(path)
if err != nil {
return admin.Admin{}, fmt.Errorf("opening file: %w", err)
}
defer f.Close()
return admin.FromReader(f)
}

View File

@ -33,35 +33,22 @@ var subCmdHostsCreate = subCmd{
textUnmarshalerFlag{&ip}, "ip", "i", "IP of the new host",
)
adminPath := flags.StringP(
"admin-path", "a", "",
`Path to admin.json file. If the given path is "-" then stdin is used.`,
)
if err := flags.Parse(subCmdCtx.args); err != nil {
return fmt.Errorf("parsing flags: %w", err)
}
if !hostNameF.Changed ||
!ipF.Changed ||
*adminPath == "" {
return errors.New("--hostname, --ip, and --admin-path are required")
}
adm, err := readAdmin(*adminPath)
if err != nil {
return fmt.Errorf("reading admin.json with --admin-path of %q: %w", *adminPath, err)
if !hostNameF.Changed || !ipF.Changed {
return errors.New("--hostname and --ip are required")
}
var res daemon.CreateHostResult
err = subCmdCtx.daemonRCPClient.Call(
err := subCmdCtx.daemonRCPClient.Call(
subCmdCtx.ctx,
&res,
"CreateHost",
daemon.CreateHostRequest{
CASigningPrivateKey: adm.Nebula.CACredentials.SigningPrivateKey,
HostName: hostName,
IP: ip,
HostName: hostName,
IP: ip,
},
)
if err != nil {

View File

@ -24,11 +24,6 @@ var subCmdNebulaCreateCert = subCmd{
"Name of the host to generate a certificate for",
)
adminPath := flags.StringP(
"admin-path", "a", "",
`Path to admin.json 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.`,
@ -38,17 +33,10 @@ var subCmdNebulaCreateCert = subCmd{
return fmt.Errorf("parsing flags: %w", err)
}
if !hostNameF.Changed ||
*adminPath == "" ||
*pubKeyPath == "" {
if !hostNameF.Changed || *pubKeyPath == "" {
return errors.New("--hostname, --admin-path, and --pub-key-path are required")
}
adm, err := readAdmin(*adminPath)
if err != nil {
return fmt.Errorf("reading admin.json 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)
@ -65,7 +53,6 @@ var subCmdNebulaCreateCert = subCmd{
&res,
"CreateNebulaCertificate",
daemon.CreateNebulaCertificateRequest{
CASigningPrivateKey: adm.Nebula.CACredentials.SigningPrivateKey,
HostName: hostName,
HostEncryptingPublicKey: hostPub,
},

View File

@ -3,15 +3,13 @@ package main
import (
"errors"
"fmt"
"isle/admin"
"isle/daemon"
"isle/jsonutil"
"os"
)
var subCmdNetworkCreate = subCmd{
name: "create",
descr: "Create's a new network, with this host being the first host in that network. The resulting admin.json is output to stdout.",
descr: "Create's a new network, with this host being the first host in that network.",
do: func(subCmdCtx subCmdCtx) error {
var (
ctx = subCmdCtx.ctx
@ -53,16 +51,11 @@ var subCmdNetworkCreate = subCmd{
return errors.New("--name, --domain, --ip-net, and --hostname are required")
}
var adm admin.Admin
err := subCmdCtx.daemonRCPClient.Call(ctx, &adm, "CreateNetwork", req)
err := subCmdCtx.daemonRCPClient.Call(ctx, nil, "CreateNetwork", req)
if err != nil {
return fmt.Errorf("creating network: %w", err)
}
if err := adm.WriteTo(os.Stdout); err != nil {
return fmt.Errorf("writing admin.json to stdout")
}
return nil
},
}

View File

@ -33,7 +33,7 @@ func dnsmasqPmuxProcConfig(
confData := dnsmasq.ConfData{
Resolvers: daemonConfig.DNS.Resolvers,
Domain: hostBootstrap.AdminCreationParams.Domain,
Domain: hostBootstrap.NetworkCreationParams.Domain,
IP: hostBootstrap.ThisHost().IP().String(),
Hosts: hostsSlice,
}

View File

@ -9,7 +9,6 @@ import (
"fmt"
"io"
"io/fs"
"isle/admin"
"isle/bootstrap"
"isle/jsonutil"
"isle/nebula"
@ -35,15 +34,13 @@ type Daemon interface {
// become this first host's IP.
// - hostName: The name of this first host in the network.
//
// An Admin instance is returned, which is necessary to perform admin
// actions in the future.
// The daemon on which this is called will become the first host in the
// network, and will have full administrative privileges.
CreateNetwork(
ctx context.Context, name, domain string,
ipNet nebula.IPNet,
hostName nebula.HostName,
) (
admin.Admin, error,
)
) error
// JoinNetwork joins the Daemon to an existing network using the given
// Bootstrap.
@ -66,7 +63,6 @@ type Daemon interface {
// address.
CreateHost(
ctx context.Context,
caSigningPrivateKey nebula.SigningPrivateKey, // TODO load from secrets storage
hostName nebula.HostName,
ip netip.Addr, // TODO automatically choose IP address
) (
@ -81,7 +77,6 @@ type Daemon interface {
// - ErrHostNotFound
CreateNebulaCertificate(
ctx context.Context,
caSigningPrivateKey nebula.SigningPrivateKey, // TODO load from secrets storage
hostName nebula.HostName,
hostPubKey nebula.EncryptingPublicKey,
) (
@ -519,21 +514,17 @@ func (d *daemon) restartLoop(ctx context.Context, readyCh chan<- struct{}) {
func (d *daemon) CreateNetwork(
ctx context.Context,
name, domain string, ipNet nebula.IPNet, hostName nebula.HostName,
) (
admin.Admin, error,
) {
) error {
nebulaCACreds, err := nebula.NewCACredentials(domain, ipNet)
if err != nil {
return admin.Admin{}, fmt.Errorf("creating nebula CA cert: %w", err)
return fmt.Errorf("creating nebula CA cert: %w", err)
}
var (
adm = admin.Admin{
CreationParams: admin.CreationParams{
ID: randStr(32),
Name: name,
Domain: domain,
},
creationParams = bootstrap.CreationParams{
ID: randStr(32),
Name: name,
Domain: domain,
}
garageRPCSecret = randStr(32)
@ -541,29 +532,34 @@ func (d *daemon) CreateNetwork(
err = setGarageRPCSecret(ctx, d.secretsStore, garageRPCSecret)
if err != nil {
return admin.Admin{}, fmt.Errorf("storing garage RPC secret: %w", err)
return fmt.Errorf("setting garage RPC secret: %w", err)
}
err = setNebulaCASigningPrivateKey(ctx, d.secretsStore, nebulaCACreds.SigningPrivateKey)
if err != nil {
return fmt.Errorf("setting nebula CA signing key secret: %w", err)
}
hostBootstrap, err := bootstrap.New(
nebulaCACreds,
adm.CreationParams,
creationParams,
hostName,
ipNet.FirstAddr(),
)
if err != nil {
return adm, fmt.Errorf("initializing bootstrap data: %w", err)
return fmt.Errorf("initializing bootstrap data: %w", err)
}
d.l.Lock()
if d.state != daemonStateNoNetwork {
d.l.Unlock()
return adm, ErrAlreadyJoined
return ErrAlreadyJoined
}
if len(d.daemonConfig.Storage.Allocations) < 3 {
d.l.Unlock()
return adm, ErrInvalidConfig.WithData(
return ErrInvalidConfig.WithData(
"At least three storage allocations are required.",
)
}
@ -573,18 +569,16 @@ func (d *daemon) CreateNetwork(
err = d.initialize(hostBootstrap, readyCh)
d.l.Unlock()
if err != nil {
return adm, fmt.Errorf("initializing daemon: %w", err)
return fmt.Errorf("initializing daemon: %w", err)
}
select {
case <-readyCh:
case <-ctx.Done():
return adm, ctx.Err()
return ctx.Err()
}
adm.Nebula.CACredentials = nebulaCACreds
return adm, nil
return nil
}
func (d *daemon) JoinNetwork(
@ -674,7 +668,6 @@ func makeCACreds(
func (d *daemon) CreateHost(
ctx context.Context,
caSigningPrivateKey nebula.SigningPrivateKey,
hostName nebula.HostName,
ip netip.Addr,
) (
@ -685,14 +678,17 @@ func (d *daemon) CreateHost(
) (
JoiningBootstrap, error,
) {
var (
joiningBootstrap JoiningBootstrap
err error
caSigningPrivateKey, err := getNebulaCASigningPrivateKey(
ctx, d.secretsStore,
)
if err != nil {
return JoiningBootstrap{}, fmt.Errorf("getting CA signing key: %w", err)
}
var joiningBootstrap JoiningBootstrap
joiningBootstrap.Bootstrap, err = bootstrap.New(
makeCACreds(currBootstrap, caSigningPrivateKey),
currBootstrap.AdminCreationParams,
currBootstrap.NetworkCreationParams,
hostName,
ip,
)
@ -722,7 +718,6 @@ func (d *daemon) CreateHost(
func (d *daemon) CreateNebulaCertificate(
ctx context.Context,
caSigningPrivateKey nebula.SigningPrivateKey,
hostName nebula.HostName,
hostPubKey nebula.EncryptingPublicKey,
) (
@ -738,6 +733,13 @@ func (d *daemon) CreateNebulaCertificate(
return nebula.Certificate{}, ErrHostNotFound
}
caSigningPrivateKey, err := getNebulaCASigningPrivateKey(
ctx, d.secretsStore,
)
if err != nil {
return nebula.Certificate{}, fmt.Errorf("getting CA signing key: %w", err)
}
caCreds := makeCACreds(currBootstrap, caSigningPrivateKey)
return nebula.NewHostCert(caCreds, hostPubKey, hostName, host.IP())

View File

@ -4,7 +4,6 @@ import (
"cmp"
"context"
"fmt"
"isle/admin"
"isle/bootstrap"
"isle/nebula"
"net/netip"
@ -47,9 +46,9 @@ type CreateNetworkRequest struct {
func (r *RPC) CreateNetwork(
ctx context.Context, req CreateNetworkRequest,
) (
admin.Admin, error,
struct{}, error,
) {
return r.daemon.CreateNetwork(
return struct{}{}, r.daemon.CreateNetwork(
ctx, req.Name, req.Domain, req.IPNet, req.HostName,
)
}
@ -130,9 +129,8 @@ func (r *RPC) RemoveHost(ctx context.Context, req RemoveHostRequest) (struct{},
//
// All fields are required.
type CreateHostRequest struct {
CASigningPrivateKey nebula.SigningPrivateKey // TODO load from secrets storage
HostName nebula.HostName
IP netip.Addr
HostName nebula.HostName
IP netip.Addr
}
// CreateHostResult wraps the results from the CreateHost RPC method.
@ -148,7 +146,7 @@ func (r *RPC) CreateHost(
CreateHostResult, error,
) {
joiningBootstrap, err := r.daemon.CreateHost(
ctx, req.CASigningPrivateKey, req.HostName, req.IP,
ctx, req.HostName, req.IP,
)
if err != nil {
return CreateHostResult{}, err
@ -162,7 +160,6 @@ func (r *RPC) CreateHost(
//
// All fields are required.
type CreateNebulaCertificateRequest struct {
CASigningPrivateKey nebula.SigningPrivateKey // TODO load from secrets storage
HostName nebula.HostName
HostEncryptingPublicKey nebula.EncryptingPublicKey
}
@ -181,7 +178,7 @@ func (r *RPC) CreateNebulaCertificate(
CreateNebulaCertificateResult, error,
) {
cert, err := r.daemon.CreateNebulaCertificate(
ctx, req.CASigningPrivateKey, req.HostName, req.HostEncryptingPublicKey,
ctx, req.HostName, req.HostEncryptingPublicKey,
)
if err != nil {
return CreateNebulaCertificateResult{}, err

View File

@ -3,13 +3,26 @@ package daemon
import (
"fmt"
"isle/garage"
"isle/nebula"
"isle/secrets"
)
const (
secretsNSNebula = "nebula"
secretsNSGarage = "garage"
)
////////////////////////////////////////////////////////////////////////////////
// Nebula-related secrets
var (
nebulaCASigningPrivateKeySecretID = secrets.NewID(secretsNSNebula, "ca-signing-private-key")
)
var getNebulaCASigningPrivateKey, setNebulaCASigningPrivateKey = secrets.GetSetFunctions[nebula.SigningPrivateKey](
nebulaCASigningPrivateKeySecretID,
)
////////////////////////////////////////////////////////////////////////////////
// Garage-related secrets

View File

@ -1,14 +0,0 @@
package nebula
import (
"isle/secrets"
)
var (
caSigningPrivateKeySecretID = secrets.NewID("nebula", "ca-signing-private-key")
)
// Get/Set functions for the CA SigningPrivateKey secret.
var GetCASigningPrivateKey, SetCASigningPrivateKey = secrets.GetSetFunctions[SigningPrivateKey](
caSigningPrivateKeySecretID,
)

View File

@ -4,7 +4,8 @@ source "$UTILS"/with-1-data-1-empty-node-network.sh
adminBS="$XDG_STATE_HOME"/isle/bootstrap.json
bs="$secondus_bootstrap" # set in with-1-data-1-empty-node-network.sh
[ "$(jq -r <"$bs" '.Bootstrap.AdminCreationParams')" = "$(jq -r <admin.json '.CreationParams')" ]
[ "$(jq -r <"$bs" '.Bootstrap.NetworkCreationParams.Domain')" = "shared.test" ]
[ "$(jq -r <"$bs" '.Bootstrap.NetworkCreationParams.Name')" = "testing" ]
[ "$(jq -r <"$bs" '.Bootstrap.SignedHostAssigned.Body.Name')" = "secondus" ]
[ "$(jq -r <"$bs" '.Bootstrap.Hosts.primus.PublicCredentials')" \

View File

@ -6,14 +6,12 @@ cat pubkey
(
isle nebula create-cert \
--admin-path admin.json \
--hostname non-esiste \
--public-key-path pubkey \
2>&1 || true \
) | grep '\[6\] Host not found'
isle nebula create-cert \
--admin-path admin.json \
--hostname primus \
--public-key-path pubkey \
| grep -- '-----BEGIN NEBULA CERTIFICATE-----'

View File

@ -5,10 +5,8 @@ source "$UTILS"/with-1-data-1-empty-node-network.sh
[ "$(cat b/meta/isle/rpc_port)" = "3910" ]
[ "$(cat c/meta/isle/rpc_port)" = "3920" ]
[ "$(jq -r <admin.json '.CreationParams.ID')" != "" ]
[ "$(jq -r <admin.json '.CreationParams.Name')" = "testing" ]
[ "$(jq -r <admin.json '.CreationParams.Domain')" = "shared.test" ]
[ "$(jq -r <"$BOOTSTRAP_FILE" '.NetworkCreationParams.ID')" != "" ]
[ "$(jq -r <"$BOOTSTRAP_FILE" '.NetworkCreationParams.Name')" = "testing" ]
[ "$(jq -r <"$BOOTSTRAP_FILE" '.NetworkCreationParams.Domain')" = "shared.test" ]
[ "$(jq -rc <"$BOOTSTRAP_FILE" '.AdminCreationParams')" = "$(jq -rc <admin.json '.CreationParams')" ]
[ "$(jq -rc <"$BOOTSTRAP_FILE" '.CAPublicCredentials')" = "$(jq -rc <admin.json '.Nebula.CACredentials.Public')" ]
[ "$(jq -r <"$BOOTSTRAP_FILE" '.SignedHostAssigned.Body.Name')" = "primus" ]

View File

@ -63,12 +63,10 @@ EOF
--domain shared.test \
--hostname primus \
--ip-net "$ipNet" \
--name "testing" \
> admin.json
--name "testing"
echo "Creating secondus bootstrap"
isle hosts create \
--admin-path admin.json \
--hostname secondus \
--ip "$secondus_ip" \
> "$secondus_bootstrap"