Compare commits

...

5 Commits

Author SHA1 Message Date
c645a8c767 Refactor how signing/encryption keys are typed and (un)marshaled 2024-06-15 23:02:24 +02:00
65fa208a34 Move garage admin API calls into garage package 2024-06-12 10:55:55 +02:00
842c169169 Separate garage server logic into its own package 2024-06-12 10:18:33 +02:00
dee4af012e Fix tests.sh verbose output redirection 2024-06-11 16:57:31 +02:00
68f417b5ba Upgrade garage to v1.0.0
This required switching all garage admin API calls to the new v1
versions, and redoing how the global bucket key is created so it is
created via the "create key" API call.
2024-06-11 16:57:31 +02:00
29 changed files with 709 additions and 509 deletions

View File

@ -69,7 +69,7 @@ storage:
# the meta directories can be anywhere (ideally on an SSD). # the meta directories can be anywhere (ideally on an SSD).
# #
# Capacity declares how many gigabytes can be stored in each allocation, and # Capacity declares how many gigabytes can be stored in each allocation, and
# is required. It must be a multiple of 100. # is required.
# #
# The ports are all _optional_, and will be automatically assigned if they are # The ports are all _optional_, and will be automatically assigned if they are
# not specified. If ports any ports are specified then all should be # not specified. If ports any ports are specified then all should be

View File

@ -69,7 +69,7 @@ in rec {
''; '';
}; };
vendorHash = "sha256-P1TXG0fG8/6n37LmM5ApYctqoZzJFlvFAO2Zl85SVvk="; vendorHash = "sha256-33gwBj+6x9I/yz0Qf4G8YXRgC/HfwHCedqzrCE4FHHk=";
subPackages = [ subPackages = [
"./cmd/entrypoint" "./cmd/entrypoint"
@ -90,7 +90,7 @@ in rec {
inherit buildSystem; inherit buildSystem;
hostSystem = "${hostPlatform.cpu.name}-unknown-${hostPlatform.kernel.name}-musl"; hostSystem = "${hostPlatform.cpu.name}-unknown-${hostPlatform.kernel.name}-musl";
#pkgsSrc = pkgsNix.src; pkgsSrc = pkgsNix.src;
}; };

View File

@ -35,7 +35,7 @@ storage:
meta_path: /mnt/drive1/isle/meta meta_path: /mnt/drive1/isle/meta
capacity: 1200 capacity: 1200
# 100 GB (the minimum) are being shared from drive2 # 100 GB are being shared from drive2
- data_path: /mnt/drive2/isle/data - data_path: /mnt/drive2/isle/data
meta_path: /mnt/drive2/isle/meta meta_path: /mnt/drive2/isle/meta
capacity: 100 capacity: 100

View File

@ -30,10 +30,13 @@ func AppDirPath(appDirPath string) string {
// Garage contains parameters needed to connect to and use the garage cluster. // Garage contains parameters needed to connect to and use the garage cluster.
type Garage struct { type Garage struct {
// TODO RPCSecret and GlobalBucketS3APICredentials are duplicated here and // TODO this should be part of some new configuration section related to
// in AdminCreationParams, might as well just use them from there // secrets which may or may not be granted to this host
RPCSecret string RPCSecret string
AdminToken string AdminToken string
// TODO this should be part of admin.CreationParams
GlobalBucketS3APICredentials garage.S3APICredentials GlobalBucketS3APICredentials garage.S3APICredentials
} }

View File

@ -27,17 +27,6 @@ func (b Bootstrap) GaragePeers() []garage.RemotePeer {
return peers return peers
} }
// GarageRPCPeerAddrs returns the full RPC peer address for each known garage
// instance in the network.
func (b Bootstrap) GarageRPCPeerAddrs() []string {
var addrs []string
for _, peer := range b.GaragePeers() {
addr := peer.RPCPeerAddr()
addrs = append(addrs, addr)
}
return addrs
}
// ChooseGaragePeer returns a Peer for a garage instance from the network. It // ChooseGaragePeer returns a Peer for a garage instance from the network. It
// will prefer a garage instance on this particular host, if there is one, but // will prefer a garage instance on this particular host, if there is one, but
// will otherwise return a random endpoint. // will otherwise return a random endpoint.

View File

@ -78,10 +78,5 @@ type Host struct {
// This assumes that the Host and its data has already been verified against the // This assumes that the Host and its data has already been verified against the
// CA signing key. // CA signing key.
func (h Host) IP() net.IP { func (h Host) IP() net.IP {
ip, err := nebula.IPFromHostCertPEM(h.PublicCredentials.CertPEM) return h.PublicCredentials.Cert.Details.Ips[0].IP
if err != nil {
panic(fmt.Errorf("could not parse IP out of cert for host %q: %w", h.Name, err))
}
return ip
} }

View File

@ -151,7 +151,6 @@ var subCmdAdminCreateNetwork = subCmd{
garageBootstrap := bootstrap.Garage{ garageBootstrap := bootstrap.Garage{
RPCSecret: randStr(32), RPCSecret: randStr(32),
AdminToken: randStr(32), AdminToken: randStr(32),
GlobalBucketS3APICredentials: garage.NewS3APICredentials(),
} }
hostBootstrap, err := bootstrap.New( hostBootstrap, err := bootstrap.New(
@ -216,7 +215,17 @@ var subCmdAdminCreateNetwork = subCmd{
} }
logger.Info(ctx, "initializing garage shared global bucket") logger.Info(ctx, "initializing garage shared global bucket")
err = garageInitializeGlobalBucket(ctx, logger, hostBootstrap, daemonConfig) garageGlobalBucketCreds, err := garageInitializeGlobalBucket(
ctx, logger, hostBootstrap, daemonConfig,
)
hostBootstrap.Garage.GlobalBucketS3APICredentials = garageGlobalBucketCreds
// rewrite the bootstrap now that the global bucket creds have been
// added to it.
if err := writeBootstrapToDataDir(hostBootstrap); err != nil {
return fmt.Errorf("writing bootstrap file: %w", err)
}
if cErr := (garage.AdminClientError{}); errors.As(err, &cErr) && cErr.StatusCode == 409 { if cErr := (garage.AdminClientError{}); errors.As(err, &cErr) && cErr.StatusCode == 409 {
return fmt.Errorf("shared global bucket has already been created, are the storage allocations from a previously initialized isle being used?") return fmt.Errorf("shared global bucket has already been created, are the storage allocations from a previously initialized isle being used?")
@ -372,13 +381,23 @@ var subCmdAdminCreateNebulaCert = subCmd{
return fmt.Errorf("reading public key from %q: %w", *pubKeyPath, err) return fmt.Errorf("reading public key from %q: %w", *pubKeyPath, err)
} }
nebulaHostCertPEM, err := nebula.NewHostCertPEM( var hostPub nebula.EncryptingPublicKey
adm.Nebula.CACredentials, string(hostPubPEM), *hostName, ip, if err := hostPub.UnmarshalNebulaPEM(hostPubPEM); err != nil {
return fmt.Errorf("unmarshaling public key as PEM: %w", err)
}
nebulaHostCert, err := nebula.NewHostCert(
adm.Nebula.CACredentials, hostPub, *hostName, ip,
) )
if err != nil { if err != nil {
return fmt.Errorf("creating cert: %w", err) return fmt.Errorf("creating cert: %w", err)
} }
nebulaHostCertPEM, err := nebulaHostCert.MarshalToPEM()
if err != nil {
return fmt.Errorf("marshaling cert to PEM: %w", err)
}
if _, err := os.Stdout.Write([]byte(nebulaHostCertPEM)); err != nil { if _, err := os.Stdout.Write([]byte(nebulaHostCertPEM)); err != nil {
return fmt.Errorf("writing to stdout: %w", err) return fmt.Errorf("writing to stdout: %w", err)
} }

View File

@ -5,7 +5,7 @@ import (
"fmt" "fmt"
"isle/bootstrap" "isle/bootstrap"
"isle/daemon" "isle/daemon"
"isle/garage" "isle/garage/garagesrv"
"time" "time"
) )
@ -29,7 +29,7 @@ func coalesceDaemonConfigAndBootstrap(
for i, alloc := range allocs { for i, alloc := range allocs {
id, rpcPort, err := garage.InitAlloc(alloc.MetaPath, alloc.RPCPort) id, rpcPort, err := garagesrv.InitAlloc(alloc.MetaPath, alloc.RPCPort)
if err != nil { if err != nil {
return bootstrap.Bootstrap{}, daemon.Config{}, fmt.Errorf("initializing alloc at %q: %w", alloc.MetaPath, err) return bootstrap.Bootstrap{}, daemon.Config{}, fmt.Errorf("initializing alloc at %q: %w", alloc.MetaPath, err)
} }

View File

@ -2,10 +2,11 @@ package main
import ( import (
"context" "context"
"fmt"
"isle/bootstrap" "isle/bootstrap"
"isle/daemon" "isle/daemon"
"isle/garage" "isle/garage"
"fmt" "isle/garage/garagesrv"
"net" "net"
"path/filepath" "path/filepath"
"strconv" "strconv"
@ -30,12 +31,12 @@ func newGarageAdminClient(
thisHost := hostBootstrap.ThisHost() thisHost := hostBootstrap.ThisHost()
return garage.NewAdminClient( return garage.NewAdminClient(
garageAdminClientLogger(logger),
net.JoinHostPort( net.JoinHostPort(
thisHost.IP().String(), thisHost.IP().String(),
strconv.Itoa(daemonConfig.Storage.Allocations[0].AdminPort), strconv.Itoa(daemonConfig.Storage.Allocations[0].AdminPort),
), ),
hostBootstrap.Garage.AdminToken, hostBootstrap.Garage.AdminToken,
garageAdminClientLogger(logger),
) )
} }
@ -68,9 +69,9 @@ func waitForGarageAndNebula(
) )
adminClient := garage.NewAdminClient( adminClient := garage.NewAdminClient(
adminClientLogger,
adminAddr, adminAddr,
hostBootstrap.Garage.AdminToken, hostBootstrap.Garage.AdminToken,
adminClientLogger,
) )
ctx := mctx.Annotate(ctx, "garageAdminAddr", adminAddr) ctx := mctx.Annotate(ctx, "garageAdminAddr", adminAddr)
@ -128,18 +129,15 @@ func garageWriteChildConfig(
envRuntimeDirPath, fmt.Sprintf("garage-%d.toml", alloc.RPCPort), envRuntimeDirPath, fmt.Sprintf("garage-%d.toml", alloc.RPCPort),
) )
err := garage.WriteGarageTomlFile(garageTomlPath, garage.GarageTomlData{ err := garagesrv.WriteGarageTomlFile(garageTomlPath, garagesrv.GarageTomlData{
MetaPath: alloc.MetaPath, MetaPath: alloc.MetaPath,
DataPath: alloc.DataPath, DataPath: alloc.DataPath,
RPCSecret: hostBootstrap.Garage.RPCSecret, RPCSecret: hostBootstrap.Garage.RPCSecret,
AdminToken: hostBootstrap.Garage.AdminToken, AdminToken: hostBootstrap.Garage.AdminToken,
RPCAddr: peer.RPCAddr(), LocalPeer: peer,
S3APIAddr: peer.S3APIAddr(), BootstrapPeers: hostBootstrap.GaragePeers(),
AdminAddr: peer.AdminAddr(),
BootstrapPeers: hostBootstrap.GarageRPCPeerAddrs(),
}) })
if err != nil { if err != nil {
@ -184,62 +182,36 @@ func garageInitializeGlobalBucket(
logger *mlog.Logger, logger *mlog.Logger,
hostBootstrap bootstrap.Bootstrap, hostBootstrap bootstrap.Bootstrap,
daemonConfig daemon.Config, daemonConfig daemon.Config,
) error { ) (
garage.S3APICredentials, error,
) {
adminClient := newGarageAdminClient(logger, hostBootstrap, daemonConfig)
var ( creds, err := adminClient.CreateS3APICredentials(
adminClient = newGarageAdminClient(logger, hostBootstrap, daemonConfig) ctx, garage.GlobalBucketS3APICredentialsName,
globalBucketCreds = hostBootstrap.Garage.GlobalBucketS3APICredentials
) )
// 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 { if err != nil {
return fmt.Errorf("importing global bucket key into garage: %w", err) return creds, fmt.Errorf("creating global bucket credentials: %w", err)
} }
// create global bucket bucketID, err := adminClient.CreateBucket(ctx, garage.GlobalBucket)
err = adminClient.Do(ctx, nil, "POST", "/v0/bucket", map[string]string{
"globalAlias": garage.GlobalBucket,
})
if err != nil { if err != nil {
return fmt.Errorf("creating global bucket: %w", err) return creds, fmt.Errorf("creating global bucket: %w", err)
} }
// retrieve newly created bucket's id if err := adminClient.GrantBucketPermissions(
var getBucketRes struct { ctx,
ID string `json:"id"` bucketID,
} creds.ID,
garage.BucketPermissionRead,
err = adminClient.Do( garage.BucketPermissionWrite,
ctx, &getBucketRes, ); err != nil {
"GET", "/v0/bucket?globalAlias="+garage.GlobalBucket, nil, return creds, fmt.Errorf(
"granting permissions to shared global bucket key: %w", err,
) )
if err != nil {
return fmt.Errorf("fetching global bucket id: %w", err)
} }
// allow shared global bucket key to perform all operations return creds, nil
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
} }
func garageApplyLayout( func garageApplyLayout(
@ -254,18 +226,10 @@ func garageApplyLayout(
thisHost = hostBootstrap.ThisHost() thisHost = hostBootstrap.ThisHost()
hostName = thisHost.Name hostName = thisHost.Name
allocs = daemonConfig.Storage.Allocations allocs = daemonConfig.Storage.Allocations
peers = make([]garage.PeerLayout, len(allocs))
) )
type peerLayout struct { for i, alloc := range allocs {
Capacity int `json:"capacity"`
Zone string `json:"zone"`
Tags []string `json:"tags"`
}
{
clusterLayout := map[string]peerLayout{}
for _, alloc := range allocs {
id := bootstrapGarageHostForAlloc(thisHost, alloc).ID id := bootstrapGarageHostForAlloc(thisHost, alloc).ID
@ -274,42 +238,13 @@ func garageApplyLayout(
zone = alloc.Zone zone = alloc.Zone
} }
clusterLayout[id] = peerLayout{ peers[i] = garage.PeerLayout{
Capacity: alloc.Capacity, ID: id,
Capacity: alloc.Capacity * 1_000_000_000,
Zone: zone, Zone: zone,
Tags: []string{}, Tags: []string{},
} }
} }
err := adminClient.Do(ctx, nil, "POST", "/v0/layout", clusterLayout) return adminClient.ApplyLayout(ctx, peers)
if err != nil {
return fmt.Errorf("staging layout changes: %w", err)
}
}
var clusterLayout struct {
Version int `json:"version"`
StagedRoleChanges map[string]peerLayout `json:"stagedRoleChanges"`
}
if err := adminClient.Do(ctx, &clusterLayout, "GET", "/v0/layout", nil); err != nil {
return fmt.Errorf("retrieving staged layout change: %w", err)
}
if len(clusterLayout.StagedRoleChanges) == 0 {
return nil
}
applyClusterLayout := struct {
Version int `json:"version"`
}{
Version: clusterLayout.Version + 1,
}
err := adminClient.Do(ctx, nil, "POST", "/v0/layout/apply", applyClusterLayout)
if err != nil {
return fmt.Errorf("applying new layout (new version:%d): %w", applyClusterLayout.Version, err)
}
return nil
} }

View File

@ -4,8 +4,6 @@ import (
"fmt" "fmt"
"isle/jsonutil" "isle/jsonutil"
"os" "os"
"github.com/slackhq/nebula/cert"
) )
var subCmdNebulaShow = subCmd{ var subCmdNebulaShow = subCmd{
@ -23,10 +21,10 @@ var subCmdNebulaShow = subCmd{
return fmt.Errorf("loading host bootstrap: %w", err) return fmt.Errorf("loading host bootstrap: %w", err)
} }
caPublicCreds := hostBootstrap.CAPublicCredentials caCert := hostBootstrap.CAPublicCredentials.Cert
caCert, _, err := cert.UnmarshalNebulaCertificateFromPEM([]byte(caPublicCreds.CertPEM)) caCertPEM, err := caCert.MarshalToPEM()
if err != nil { if err != nil {
return fmt.Errorf("unmarshaling ca.crt: %w", err) return fmt.Errorf("marshaling CA cert to PEM: %w", err)
} }
if len(caCert.Details.Subnets) != 1 { if len(caCert.Details.Subnets) != 1 {
@ -48,7 +46,7 @@ var subCmdNebulaShow = subCmd{
SubnetCIDR string SubnetCIDR string
Lighthouses []outLighthouse Lighthouses []outLighthouse
}{ }{
CACert: caPublicCreds.CertPEM, CACert: string(caCertPEM),
SubnetCIDR: subnet.String(), SubnetCIDR: subnet.String(),
} }

View File

@ -10,6 +10,7 @@ import (
"path/filepath" "path/filepath"
"code.betamike.com/micropelago/pmux/pmuxlib" "code.betamike.com/micropelago/pmux/pmuxlib"
"github.com/slackhq/nebula/cert"
) )
// waitForNebula waits for the nebula interface to have been started up. It does // waitForNebula waits for the nebula interface to have been started up. It does
@ -56,11 +57,29 @@ func nebulaPmuxProcConfig(
staticHostMap[ip] = []string{host.Nebula.PublicAddr} staticHostMap[ip] = []string{host.Nebula.PublicAddr}
} }
caCertPEM, err := hostBootstrap.CAPublicCredentials.Cert.MarshalToPEM()
if err != nil {
return pmuxlib.ProcessConfig{}, fmt.Errorf(
"marshaling CA cert to PEM: :%w", err,
)
}
hostCertPEM, err := hostBootstrap.PublicCredentials.Cert.MarshalToPEM()
if err != nil {
return pmuxlib.ProcessConfig{}, fmt.Errorf(
"marshaling host cert to PEM: :%w", err,
)
}
hostKeyPEM := cert.MarshalX25519PrivateKey(
hostBootstrap.PrivateCredentials.EncryptingPrivateKey.Bytes(),
)
config := map[string]interface{}{ config := map[string]interface{}{
"pki": map[string]string{ "pki": map[string]string{
"ca": hostBootstrap.CAPublicCredentials.CertPEM, "ca": string(caCertPEM),
"cert": hostBootstrap.PublicCredentials.CertPEM, "cert": string(hostCertPEM),
"key": hostBootstrap.PrivateCredentials.PrivateKeyPEM, "key": string(hostKeyPEM),
}, },
"static_host_map": staticHostMap, "static_host_map": staticHostMap,
"punchy": map[string]bool{ "punchy": map[string]bool{

View File

@ -14,7 +14,22 @@ import (
"github.com/mediocregopher/mediocre-go-lib/v2/mlog" "github.com/mediocregopher/mediocre-go-lib/v2/mlog"
) )
// AdminClientError gets returned from AdminClient's Do method for non-200 // BucketID is a unique identifier for a bucket in garage. It is different than
// the bucket's alias, since a bucket may have multiple aliases.
type BucketID string
// BucketPermission describes a permission an S3APICredentials may have when
// interacting with a bucket.
type BucketPermission string
// Enumeration of BucketPermissions.
const (
BucketPermissionRead BucketPermission = "read"
BucketPermissionWrite BucketPermission = "write"
BucketPermissionOwner BucketPermission = "owner"
)
// AdminClientError gets returned from AdminClient Do methods for non-200
// errors. // errors.
type AdminClientError struct { type AdminClientError struct {
StatusCode int StatusCode int
@ -28,32 +43,32 @@ func (e AdminClientError) Error() string {
// AdminClient is a helper type for performing actions against the garage admin // AdminClient is a helper type for performing actions against the garage admin
// interface. // interface.
type AdminClient struct { type AdminClient struct {
logger *mlog.Logger
c *http.Client c *http.Client
addr string addr string
adminToken string adminToken string
logger *mlog.Logger
} }
// NewAdminClient initializes and returns an AdminClient which will use the // NewAdminClient initializes and returns an AdminClient which will use the
// given address and adminToken for all requests made. // given address and adminToken for all requests made.
// //
// If Logger is nil then logs will be suppressed. // If Logger is nil then logs will be suppressed.
func NewAdminClient(addr, adminToken string, logger *mlog.Logger) *AdminClient { func NewAdminClient(logger *mlog.Logger, addr, adminToken string) *AdminClient {
return &AdminClient{ return &AdminClient{
logger: logger,
c: &http.Client{ c: &http.Client{
Transport: http.DefaultTransport.(*http.Transport).Clone(), Transport: http.DefaultTransport.(*http.Transport).Clone(),
}, },
addr: addr, addr: addr,
adminToken: adminToken, adminToken: adminToken,
logger: logger,
} }
} }
// Do performs an HTTP request with the given method (GET, POST) and path, and // do performs an HTTP request with the given method (GET, POST) and path, and
// using the json marshaling of the given body as the request body (unless body // using the json marshaling of the given body as the request body (unless body
// is nil). It will JSON unmarshal the response into rcv, unless rcv is nil. // is nil). It will JSON unmarshal the response into rcv, unless rcv is nil.
func (c *AdminClient) Do( func (c *AdminClient) do(
ctx context.Context, rcv interface{}, method, path string, body interface{}, ctx context.Context, rcv any, method, path string, body any,
) error { ) error {
var bodyR io.Reader var bodyR io.Reader
@ -112,7 +127,6 @@ func (c *AdminClient) Do(
} }
if rcv == nil { if rcv == nil {
if _, err := io.Copy(io.Discard, res.Body); err != nil { if _, err := io.Copy(io.Discard, res.Body); err != nil {
return fmt.Errorf("discarding response body: %w", err) return fmt.Errorf("discarding response body: %w", err)
} }
@ -138,13 +152,14 @@ func (c *AdminClient) Wait(ctx context.Context) error {
time.Sleep(250 * time.Millisecond) time.Sleep(250 * time.Millisecond)
} }
// https://garagehq.deuxfleurs.fr/api/garage-admin-v1.html#tag/Nodes/operation/GetNodes
var clusterStatus struct { var clusterStatus struct {
KnownNodes map[string]struct { Nodes []struct {
IsUp bool `json:"is_up"` IsUp bool `json:"isUp"`
} `json:"knownNodes"` } `json:"nodes"`
} }
err := c.Do(ctx, &clusterStatus, "GET", "/v0/status", nil) err := c.do(ctx, &clusterStatus, "GET", "/v1/status", nil)
if ctxErr := ctx.Err(); ctxErr != nil { if ctxErr := ctx.Err(); ctxErr != nil {
return ctxErr return ctxErr
@ -156,14 +171,14 @@ func (c *AdminClient) Wait(ctx context.Context) error {
var numUp int var numUp int
for _, knownNode := range clusterStatus.KnownNodes { for _, node := range clusterStatus.Nodes {
if knownNode.IsUp { if node.IsUp {
numUp++ numUp++
} }
} }
ctx := mctx.Annotate(ctx, ctx := mctx.Annotate(ctx,
"numKnownNodes", len(clusterStatus.KnownNodes), "numNodes", len(clusterStatus.Nodes),
"numUp", numUp, "numUp", numUp,
) )
@ -175,3 +190,134 @@ func (c *AdminClient) Wait(ctx context.Context) error {
c.logger.Debug(ctx, "instance not online yet, will continue waiting") c.logger.Debug(ctx, "instance not online yet, will continue waiting")
} }
} }
// CreateS3APICredentials creates an S3APICredentials with the given name. The
// credentials must be associated with a bucket via a call to
// GrantBucketPermissions in order to be useful.
func (c *AdminClient) CreateS3APICredentials(
ctx context.Context, name string,
) (
S3APICredentials, error,
) {
// https://garagehq.deuxfleurs.fr/api/garage-admin-v1.html#tag/Key/operation/AddKey
var res struct {
ID string `json:"accessKeyId"`
Secret string `json:"secretAccessKey"`
}
// first attempt to import the key
err := c.do(
ctx, &res, "POST", "/v1/key", map[string]string{"name": name},
)
return S3APICredentials{
ID: res.ID,
Secret: res.Secret,
}, err
}
// CreateBucket creates a bucket with the given global alias, returning its ID.
func (c *AdminClient) CreateBucket(
ctx context.Context, globalAlias string,
) (
BucketID, error,
) {
// https://garagehq.deuxfleurs.fr/api/garage-admin-v1.html#tag/Bucket/operation/PutBucketGlobalAlias
var res struct {
ID string `json:"id"`
}
err := c.do(
ctx, &res, "POST", "/v1/bucket", map[string]string{
"globalAlias": globalAlias,
},
)
return BucketID(res.ID), err
}
// GrantBucketPermissions grants the S3APICredentials with the given ID
// permission(s) to interact with the bucket of the given ID.
func (c *AdminClient) GrantBucketPermissions(
ctx context.Context,
bucketID BucketID,
credentialsID string,
perms ...BucketPermission,
) error {
permsMap := map[BucketPermission]bool{}
for _, perm := range perms {
permsMap[perm] = true
}
// https://garagehq.deuxfleurs.fr/api/garage-admin-v1.html#tag/Bucket/operation/AllowBucketKey
return c.do(ctx, nil, "POST", "/v1/bucket/allow", map[string]any{
"bucketId": bucketID,
"accessKeyId": credentialsID,
"permissions": permsMap,
})
}
// PeerLayout describes the properties of a garage peer in the context of the
// layout of the cluster.
type PeerLayout struct {
ID string
Capacity int // Gb (SI units)
Zone string
Tags []string
}
// ApplyLayout modifies the layout of the garage cluster. Only layout of the
// given peers will be modified/created, other peers are not affected.
func (c *AdminClient) ApplyLayout(
ctx context.Context, peers []PeerLayout,
) error {
type peerLayout struct {
ID string `json:"id"`
Capacity int `json:"capacity"`
Zone string `json:"zone"`
Tags []string `json:"tags"`
}
{
// https://garagehq.deuxfleurs.fr/api/garage-admin-v1.html#tag/Layout/operation/ApplyLayout
clusterLayout := make([]peerLayout, len(peers))
for i := range peers {
clusterLayout[i] = peerLayout(peers[i])
}
err := c.do(ctx, nil, "POST", "/v1/layout", clusterLayout)
if err != nil {
return fmt.Errorf("staging layout changes: %w", err)
}
}
// https://garagehq.deuxfleurs.fr/api/garage-admin-v1.html#tag/Layout/operation/GetLayout
var clusterLayout struct {
Version int `json:"version"`
StagedRoleChanges []peerLayout `json:"stagedRoleChanges"`
}
if err := c.do(ctx, &clusterLayout, "GET", "/v1/layout", nil); err != nil {
return fmt.Errorf("retrieving staged layout change: %w", err)
}
if len(clusterLayout.StagedRoleChanges) == 0 {
return nil
}
// https://garagehq.deuxfleurs.fr/api/garage-admin-v1.html#tag/Layout/operation/ApplyLayout
applyClusterLayout := struct {
Version int `json:"version"`
}{
Version: clusterLayout.Version + 1,
}
err := c.do(ctx, nil, "POST", "/v1/layout/apply", applyClusterLayout)
if err != nil {
return fmt.Errorf("applying new layout (new version:%d): %w", applyClusterLayout.Version, err)
}
return nil
}

View File

@ -1,8 +1,6 @@
package garage package garage
import ( import (
"crypto/rand"
"encoding/hex"
"errors" "errors"
"fmt" "fmt"
@ -10,14 +8,6 @@ import (
"github.com/minio/minio-go/v7/pkg/credentials" "github.com/minio/minio-go/v7/pkg/credentials"
) )
func randStr(l int) string {
b := make([]byte, l)
if _, err := rand.Read(b); err != nil {
panic(err)
}
return hex.EncodeToString(b)
}
// IsKeyNotFound returns true if the given error is the result of a key not // IsKeyNotFound returns true if the given error is the result of a key not
// being found in a bucket. // being found in a bucket.
func IsKeyNotFound(err error) bool { func IsKeyNotFound(err error) bool {
@ -35,14 +25,6 @@ type S3APICredentials struct {
Secret string Secret string
} }
// NewS3APICredentials returns a new usable instance of S3APICredentials.
func NewS3APICredentials() S3APICredentials {
return S3APICredentials{
ID: randStr(8),
Secret: randStr(32),
}
}
// NewS3APIClient returns a minio client configured to use the given garage S3 API // NewS3APIClient returns a minio client configured to use the given garage S3 API
// endpoint. // endpoint.
func NewS3APIClient(addr string, creds S3APICredentials) S3APIClient { func NewS3APIClient(addr string, creds S3APICredentials) S3APIClient {

View File

@ -1,17 +1,7 @@
// Package garage contains helper functions and types which are useful for // Package garage contains types and helpers related to interacting with garage
// setting up garage configs, processes, and deployments. // processes via garage's APIs.
package garage package garage
import (
"encoding/hex"
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
"strconv"
)
const ( const (
// Region is the region which garage is configured with. // Region is the region which garage is configured with.
@ -21,133 +11,9 @@ const (
// accessible to all hosts in the network. // accessible to all hosts in the network.
GlobalBucket = "global-shared" GlobalBucket = "global-shared"
GlobalBucketS3APICredentialsName = "global-shared-key"
// ReplicationFactor indicates the replication factor set on the garage // ReplicationFactor indicates the replication factor set on the garage
// cluster. We currently only support a factor of 3. // cluster. We currently only support a factor of 3.
ReplicationFactor = 3 ReplicationFactor = 3
) )
func nodeKeyPath(metaDirPath string) string {
return filepath.Join(metaDirPath, "node_key")
}
func nodeKeyPubPath(metaDirPath string) string {
return filepath.Join(metaDirPath, "node_key.pub")
}
func nodeRPCPortPath(metaDirPath string) string {
return filepath.Join(metaDirPath, "isle", "rpc_port")
}
// loadAllocID returns the peer ID (ie the public key) of the node at the given
// meta directory.
func loadAllocID(metaDirPath string) (string, error) {
nodeKeyPubPath := nodeKeyPubPath(metaDirPath)
pubKey, err := os.ReadFile(nodeKeyPubPath)
if err != nil {
return "", fmt.Errorf("reading %q: %w", nodeKeyPubPath, err)
}
return hex.EncodeToString(pubKey), nil
}
// InitAlloc initializes the meta directory and keys for a particular
// allocation, if it hasn't been done so already. It returns the peer ID (ie the
// public key) and the rpc port in any case.
func InitAlloc(metaDirPath string, initRPCPort int) (string, int, error) {
initDirFor := func(path string) error {
dir := filepath.Dir(path)
return os.MkdirAll(dir, 0750)
}
var err error
exists := func(path string) bool {
if err != nil {
return false
} else if _, err = os.Stat(path); errors.Is(err, fs.ErrNotExist) {
err = nil
return false
} else if err != nil {
err = fmt.Errorf("checking if %q exists: %w", path, err)
return false
}
return true
}
nodeKeyPath := nodeKeyPath(metaDirPath)
nodeKeyPubPath := nodeKeyPubPath(metaDirPath)
nodeRPCPortPath := nodeRPCPortPath(metaDirPath)
nodeKeyPathExists := exists(nodeKeyPath)
nodeKeyPubPathExists := exists(nodeKeyPubPath)
nodeRPCPortPathExists := exists(nodeRPCPortPath)
if err != nil {
return "", 0, err
} else if nodeKeyPubPathExists != nodeKeyPathExists {
return "", 0, fmt.Errorf("%q or %q exist without the other existing", nodeKeyPath, nodeKeyPubPath)
}
var (
pubKeyStr string
rpcPort int
)
if nodeKeyPathExists {
if pubKeyStr, err = loadAllocID(metaDirPath); err != nil {
return "", 0, fmt.Errorf("reading node public key file: %w", err)
}
} else {
if err := initDirFor(nodeKeyPath); err != nil {
return "", 0, fmt.Errorf("creating directory for %q: %w", nodeKeyPath, err)
}
pubKey, privKey := GeneratePeerKey()
if err := os.WriteFile(nodeKeyPath, privKey, 0400); err != nil {
return "", 0, fmt.Errorf("writing private key to %q: %w", nodeKeyPath, err)
} else if err := os.WriteFile(nodeKeyPubPath, pubKey, 0440); err != nil {
return "", 0, fmt.Errorf("writing public key to %q: %w", nodeKeyPubPath, err)
}
pubKeyStr = hex.EncodeToString(pubKey)
}
if nodeRPCPortPathExists {
if rpcPortStr, err := os.ReadFile(nodeRPCPortPath); err != nil {
return "", 0, fmt.Errorf("reading rpc port from %q: %w", nodeRPCPortPath, err)
} else if rpcPort, err = strconv.Atoi(string(rpcPortStr)); err != nil {
return "", 0, fmt.Errorf("parsing rpc port %q from %q: %w", rpcPortStr, nodeRPCPortPath, err)
}
} else {
if err := initDirFor(nodeRPCPortPath); err != nil {
return "", 0, fmt.Errorf("creating directory for %q: %w", nodeRPCPortPath, err)
}
rpcPortStr := strconv.Itoa(initRPCPort)
if err := os.WriteFile(nodeRPCPortPath, []byte(rpcPortStr), 0440); err != nil {
return "", 0, fmt.Errorf("writing rpc port %q to %q: %w", rpcPortStr, nodeRPCPortPath, err)
}
rpcPort = initRPCPort
}
return pubKeyStr, rpcPort, nil
}

View File

@ -0,0 +1,152 @@
// Package garage contains helper functions and types which are useful for
// setting up garage configs, processes, and deployments.
package garagesrv
import (
"crypto/ed25519"
"crypto/rand"
"encoding/hex"
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
"strconv"
)
func nodeKeyPath(metaDirPath string) string {
return filepath.Join(metaDirPath, "node_key")
}
func nodeKeyPubPath(metaDirPath string) string {
return filepath.Join(metaDirPath, "node_key.pub")
}
func nodeRPCPortPath(metaDirPath string) string {
return filepath.Join(metaDirPath, "isle", "rpc_port")
}
// loadAllocID returns the peer ID (ie the public key) of the node at the given
// meta directory.
func loadAllocID(metaDirPath string) (string, error) {
nodeKeyPubPath := nodeKeyPubPath(metaDirPath)
pubKey, err := os.ReadFile(nodeKeyPubPath)
if err != nil {
return "", fmt.Errorf("reading %q: %w", nodeKeyPubPath, err)
}
return hex.EncodeToString(pubKey), nil
}
// generatePeerKey generates and returns a public/private key pair for a garage
// instance.
func generatePeerKey() (pubKey, privKey []byte) {
pubKey, privKey, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
panic(err)
}
return pubKey, privKey
}
// InitAlloc initializes the meta directory and keys for a particular
// allocation, if it hasn't been done so already. It returns the peer ID (ie the
// public key) and the rpc port in any case.
func InitAlloc(metaDirPath string, initRPCPort int) (string, int, error) {
initDirFor := func(path string) error {
dir := filepath.Dir(path)
return os.MkdirAll(dir, 0750)
}
var err error
exists := func(path string) bool {
if err != nil {
return false
} else if _, err = os.Stat(path); errors.Is(err, fs.ErrNotExist) {
err = nil
return false
} else if err != nil {
err = fmt.Errorf("checking if %q exists: %w", path, err)
return false
}
return true
}
nodeKeyPath := nodeKeyPath(metaDirPath)
nodeKeyPubPath := nodeKeyPubPath(metaDirPath)
nodeRPCPortPath := nodeRPCPortPath(metaDirPath)
nodeKeyPathExists := exists(nodeKeyPath)
nodeKeyPubPathExists := exists(nodeKeyPubPath)
nodeRPCPortPathExists := exists(nodeRPCPortPath)
if err != nil {
return "", 0, err
} else if nodeKeyPubPathExists != nodeKeyPathExists {
return "", 0, fmt.Errorf("%q or %q exist without the other existing", nodeKeyPath, nodeKeyPubPath)
}
var (
pubKeyStr string
rpcPort int
)
if nodeKeyPathExists {
if pubKeyStr, err = loadAllocID(metaDirPath); err != nil {
return "", 0, fmt.Errorf("reading node public key file: %w", err)
}
} else {
if err := initDirFor(nodeKeyPath); err != nil {
return "", 0, fmt.Errorf("creating directory for %q: %w", nodeKeyPath, err)
}
pubKey, privKey := generatePeerKey()
if err := os.WriteFile(nodeKeyPath, privKey, 0400); err != nil {
return "", 0, fmt.Errorf("writing private key to %q: %w", nodeKeyPath, err)
} else if err := os.WriteFile(nodeKeyPubPath, pubKey, 0440); err != nil {
return "", 0, fmt.Errorf("writing public key to %q: %w", nodeKeyPubPath, err)
}
pubKeyStr = hex.EncodeToString(pubKey)
}
if nodeRPCPortPathExists {
if rpcPortStr, err := os.ReadFile(nodeRPCPortPath); err != nil {
return "", 0, fmt.Errorf("reading rpc port from %q: %w", nodeRPCPortPath, err)
} else if rpcPort, err = strconv.Atoi(string(rpcPortStr)); err != nil {
return "", 0, fmt.Errorf("parsing rpc port %q from %q: %w", rpcPortStr, nodeRPCPortPath, err)
}
} else {
if err := initDirFor(nodeRPCPortPath); err != nil {
return "", 0, fmt.Errorf("creating directory for %q: %w", nodeRPCPortPath, err)
}
rpcPortStr := strconv.Itoa(initRPCPort)
if err := os.WriteFile(nodeRPCPortPath, []byte(rpcPortStr), 0440); err != nil {
return "", 0, fmt.Errorf("writing rpc port %q to %q: %w", rpcPortStr, nodeRPCPortPath, err)
}
rpcPort = initRPCPort
}
return pubKeyStr, rpcPort, nil
}

View File

@ -1,10 +1,13 @@
package garage package garagesrv
import ( import (
"fmt" "fmt"
"io" "io"
"os" "os"
"strconv"
"text/template" "text/template"
"isle/garage"
) )
// GarageTomlData describes all fields needed for rendering a garage.toml // GarageTomlData describes all fields needed for rendering a garage.toml
@ -16,11 +19,8 @@ type GarageTomlData struct {
RPCSecret string RPCSecret string
AdminToken string AdminToken string
RPCAddr string garage.LocalPeer
S3APIAddr string BootstrapPeers []garage.RemotePeer
AdminAddr string
BootstrapPeers []string
} }
var garageTomlTpl = template.Must(template.New("").Parse(` var garageTomlTpl = template.Must(template.New("").Parse(`
@ -28,14 +28,14 @@ var garageTomlTpl = template.Must(template.New("").Parse(`
metadata_dir = "{{ .MetaPath }}" metadata_dir = "{{ .MetaPath }}"
data_dir = "{{ .DataPath }}" data_dir = "{{ .DataPath }}"
replication_mode = "3" replication_mode = "` + strconv.Itoa(garage.ReplicationFactor) + `"
rpc_secret = "{{ .RPCSecret }}" rpc_secret = "{{ .RPCSecret }}"
rpc_bind_addr = "{{ .RPCAddr }}" rpc_bind_addr = "{{ .RPCAddr }}"
rpc_public_addr = "{{ .RPCAddr }}" rpc_public_addr = "{{ .RPCAddr }}"
bootstrap_peers = [{{- range .BootstrapPeers }} bootstrap_peers = [{{- range .BootstrapPeers }}
"{{ . }}", "{{ .RPCPeerAddr }}",
{{ end -}}] {{ end -}}]
[s3_api] [s3_api]

View File

@ -1,8 +1,6 @@
package garage package garage
import ( import (
"crypto/ed25519"
"crypto/rand"
"fmt" "fmt"
"net" "net"
"strconv" "strconv"
@ -24,17 +22,6 @@ type LocalPeer struct {
AdminPort int AdminPort int
} }
// GeneratePeerKey generates and returns a public/private key pair for a garage
// instance.
func GeneratePeerKey() (pubKey, privKey []byte) {
pubKey, privKey, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
panic(err)
}
return pubKey, privKey
}
// RPCAddr returns the address of the peer's RPC port. // RPCAddr returns the address of the peer's RPC port.
func (p RemotePeer) RPCAddr() string { func (p RemotePeer) RPCAddr() string {
return net.JoinHostPort(p.IP, strconv.Itoa(p.RPCPort)) return net.JoinHostPort(p.IP, strconv.Itoa(p.RPCPort))

View File

@ -23,6 +23,7 @@ require (
github.com/gopherjs/gopherjs v1.17.2 // indirect github.com/gopherjs/gopherjs v1.17.2 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/jtolds/gls v4.20.0+incompatible // indirect github.com/jtolds/gls v4.20.0+incompatible // indirect
github.com/jxskiss/base62 v1.1.0 // indirect
github.com/klauspost/compress v1.13.5 // indirect github.com/klauspost/compress v1.13.5 // indirect
github.com/klauspost/cpuid v1.3.1 // indirect github.com/klauspost/cpuid v1.3.1 // indirect
github.com/minio/md5-simd v1.1.0 // indirect github.com/minio/md5-simd v1.1.0 // indirect

View File

@ -24,6 +24,8 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/jxskiss/base62 v1.1.0 h1:A5zbF8v8WXx2xixnAKD2w+abC+sIzYJX+nxmhA6HWFw=
github.com/jxskiss/base62 v1.1.0/go.mod h1:HhWAlUXvxKThfOlZbcuFzsqwtF5TcqS9ru3y5GfjWAc=
github.com/klauspost/compress v1.13.5 h1:9O69jUPDcsT9fEm74W92rZL9FQY7rCdaXVneq+yyzl4= github.com/klauspost/compress v1.13.5 h1:9O69jUPDcsT9fEm74W92rZL9FQY7rCdaXVneq+yyzl4=
github.com/klauspost/compress v1.13.5/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/compress v1.13.5/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/cpuid v1.2.3/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/klauspost/cpuid v1.2.3/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=

117
go/nebula/encrypting_key.go Normal file
View File

@ -0,0 +1,117 @@
package nebula
import (
"crypto/ecdh"
"crypto/rand"
"fmt"
"github.com/slackhq/nebula/cert"
)
var (
encPrivKeyPrefix = []byte("x0")
encPubKeyPrefix = []byte("X0")
x25519 = ecdh.X25519()
)
// EncryptingPublicKey wraps an X25519-based ECDH public key to provide
// convenient text (un)marshaling methods.
type EncryptingPublicKey struct{ inner *ecdh.PublicKey }
// MarshalText implements the encoding.TextMarshaler interface.
func (pk EncryptingPublicKey) MarshalText() ([]byte, error) {
return encodeWithPrefix(encPubKeyPrefix, pk.inner.Bytes()), nil
}
// Bytes returns the raw bytes of the EncryptingPublicKey.
func (k EncryptingPublicKey) Bytes() []byte {
return k.inner.Bytes()
}
// UnmarshalText implements the encoding.TextUnmarshaler interface.
func (pk *EncryptingPublicKey) UnmarshalText(b []byte) error {
b, err := decodeWithPrefix(encPubKeyPrefix, b)
if err != nil {
return fmt.Errorf("unmarshaling: %w", err)
}
if pk.inner, err = x25519.NewPublicKey(b); err != nil {
return fmt.Errorf("converting bytes to public key: %w", err)
}
return nil
}
// UnmarshalNebulaPEM unmarshals the EncryptingPublicKey as a nebula host public
// key PEM.
func (pk *EncryptingPublicKey) UnmarshalNebulaPEM(b []byte) error {
b, _, err := cert.UnmarshalEd25519PublicKey(b)
if err != nil {
return fmt.Errorf("unmarshaling: %w", err)
}
if pk.inner, err = x25519.NewPublicKey(b); err != nil {
return fmt.Errorf("converting bytes to public key: %w", err)
}
return nil
}
func (pk EncryptingPublicKey) String() string {
b, err := pk.MarshalText()
if err != nil {
panic(err)
}
return string(b)
}
// EncryptingPrivateKey wraps an X25519-based ECDH private key to provide
// convenient text (un)marshaling methods.
type EncryptingPrivateKey struct{ inner *ecdh.PrivateKey }
// NewEncryptingPrivateKey generates and returns a fresh EncryptingPrivateKey.
func NewEncryptingPrivateKey() EncryptingPrivateKey {
k, err := x25519.GenerateKey(rand.Reader)
if err != nil {
panic(err)
}
return EncryptingPrivateKey{k}
}
// PublicKey returns the public key which corresponds with this private key.
func (k EncryptingPrivateKey) PublicKey() EncryptingPublicKey {
return EncryptingPublicKey{k.inner.PublicKey()}
}
// MarshalText implements the encoding.TextMarshaler interface.
func (k EncryptingPrivateKey) MarshalText() ([]byte, error) {
return encodeWithPrefix(encPrivKeyPrefix, k.inner.Bytes()), nil
}
// Bytes returns the raw bytes of the EncryptingPrivateKey.
func (k EncryptingPrivateKey) Bytes() []byte {
return k.inner.Bytes()
}
// UnmarshalText implements the encoding.TextUnmarshaler interface.
func (k *EncryptingPrivateKey) UnmarshalText(b []byte) error {
b, err := decodeWithPrefix(encPrivKeyPrefix, b)
if err != nil {
return fmt.Errorf("unmarshaling: %w", err)
}
if k.inner, err = x25519.NewPrivateKey(b); err != nil {
return fmt.Errorf("converting bytes to private key: %w", err)
}
return nil
}
func (k EncryptingPrivateKey) String() string {
b, err := k.MarshalText()
if err != nil {
panic(err)
}
return string(b)
}

View File

@ -1,72 +0,0 @@
package nebula
import (
"crypto/ed25519"
"crypto/rand"
"encoding/json"
"fmt"
"github.com/slackhq/nebula/cert"
)
// SigningPrivateKey wraps an ed25519.PrivateKey to provide convenient JSON
// (un)marshaling methods.
type SigningPrivateKey ed25519.PrivateKey
// MarshalJSON implements the json.Marshaler interface.
func (k SigningPrivateKey) MarshalJSON() ([]byte, error) {
pemStr := cert.MarshalEd25519PrivateKey(ed25519.PrivateKey(k))
return json.Marshal(string(pemStr))
}
// UnmarshalJSON implements the json.Unmarshaler interface.
func (k *SigningPrivateKey) UnmarshalJSON(b []byte) error {
var pemStr string
if err := json.Unmarshal(b, &pemStr); err != nil {
return fmt.Errorf("unmarshaling into string: %w", err)
}
key, _, err := cert.UnmarshalEd25519PrivateKey([]byte(pemStr))
if err != nil {
return fmt.Errorf("unmarshaling from PEM: %w", err)
}
*k = SigningPrivateKey(key)
return nil
}
// SigningPublicKey wraps an ed25519.PublicKey to provide convenient JSON
// (un)marshaling methods.
type SigningPublicKey ed25519.PublicKey
// MarshalJSON implements the json.Marshaler interface.
func (k SigningPublicKey) MarshalJSON() ([]byte, error) {
pemStr := cert.MarshalEd25519PublicKey(ed25519.PublicKey(k))
return json.Marshal(string(pemStr))
}
// MarshalJSON implements the json.Unmarshaler interface.
func (k *SigningPublicKey) UnmarshalJSON(b []byte) error {
var pemStr string
if err := json.Unmarshal(b, &pemStr); err != nil {
return fmt.Errorf("unmarshaling into string: %w", err)
}
key, _, err := cert.UnmarshalEd25519PublicKey([]byte(pemStr))
if err != nil {
return fmt.Errorf("unmarshaling from PEM: %w", err)
}
*k = SigningPublicKey(key)
return nil
}
// GenerateSigningPair generates and returns a new key pair which can be used
// for signing arbitrary blobs of bytes.
func GenerateSigningPair() (SigningPublicKey, SigningPrivateKey) {
pub, priv, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
panic(fmt.Errorf("generating ed25519 key: %w", err))
}
return SigningPublicKey(pub), SigningPrivateKey(priv)
}

View File

@ -3,27 +3,24 @@
package nebula package nebula
import ( import (
"crypto/rand"
"fmt" "fmt"
"io"
"net" "net"
"time" "time"
"github.com/slackhq/nebula/cert" "github.com/slackhq/nebula/cert"
"golang.org/x/crypto/curve25519"
) )
// HostPublicCredentials contains certificate and signing public keys which are // HostPublicCredentials contains certificate and signing public keys which are
// able to be broadcast publicly. // able to be broadcast publicly.
type HostPublicCredentials struct { type HostPublicCredentials struct {
CertPEM string Cert cert.NebulaCertificate
SigningKey SigningPublicKey SigningKey SigningPublicKey
} }
// HostPrivateCredentials contains the private key files which will // HostPrivateCredentials contains the private key files which will
// need to be present on a particular host. // need to be present on a particular host.
type HostPrivateCredentials struct { type HostPrivateCredentials struct {
PrivateKeyPEM string EncryptingPrivateKey EncryptingPrivateKey
SigningPrivateKey SigningPrivateKey SigningPrivateKey SigningPrivateKey
} }
@ -31,7 +28,7 @@ type HostPrivateCredentials struct {
// able to be broadcast publicly. The signing public key is the same one which // able to be broadcast publicly. The signing public key is the same one which
// is embedded into the certificate. // is embedded into the certificate.
type CAPublicCredentials struct { type CAPublicCredentials struct {
CertPEM string Cert cert.NebulaCertificate
SigningKey SigningPublicKey SigningKey SigningPublicKey
} }
@ -42,33 +39,28 @@ type CACredentials struct {
SigningPrivateKey SigningPrivateKey SigningPrivateKey SigningPrivateKey
} }
// NewHostCertPEM generates and signs a new host certificate containing the // NewHostCert generates and signs a new host certificate containing the given
// given public key. // public key.
func NewHostCertPEM( func NewHostCert(
caCreds CACredentials, hostPubPEM string, hostName string, ip net.IP, caCreds CACredentials,
hostPub EncryptingPublicKey,
hostName string,
ip net.IP,
) ( ) (
string, error, cert.NebulaCertificate, error,
) { ) {
hostPub, _, err := cert.UnmarshalX25519PublicKey([]byte(hostPubPEM)) caCert := caCreds.Public.Cert
if err != nil {
return "", fmt.Errorf("unmarshaling public key PEM: %w", err)
}
caCert, _, err := cert.UnmarshalNebulaCertificateFromPEM([]byte(caCreds.Public.CertPEM))
if err != nil {
return "", fmt.Errorf("unmarshaling ca.crt: %w", err)
}
issuer, err := caCert.Sha256Sum() issuer, err := caCert.Sha256Sum()
if err != nil { if err != nil {
return "", fmt.Errorf("getting ca.crt issuer: %w", err) return cert.NebulaCertificate{}, fmt.Errorf("getting ca.crt issuer: %w", err)
} }
expireAt := caCert.Details.NotAfter.Add(-1 * time.Second) expireAt := caCert.Details.NotAfter.Add(-1 * time.Second)
subnet := caCert.Details.Subnets[0] subnet := caCert.Details.Subnets[0]
if !subnet.Contains(ip) { if !subnet.Contains(ip) {
return "", fmt.Errorf("invalid ip %q, not contained by network subnet %q", ip, subnet) return cert.NebulaCertificate{}, fmt.Errorf("invalid ip %q, not contained by network subnet %q", ip, subnet)
} }
hostCert := cert.NebulaCertificate{ hostCert := cert.NebulaCertificate{
@ -80,26 +72,21 @@ func NewHostCertPEM(
}}, }},
NotBefore: time.Now(), NotBefore: time.Now(),
NotAfter: expireAt, NotAfter: expireAt,
PublicKey: hostPub, PublicKey: hostPub.Bytes(),
IsCA: false, IsCA: false,
Issuer: issuer, Issuer: issuer,
}, },
} }
if err := hostCert.CheckRootConstrains(caCert); err != nil { if err := hostCert.CheckRootConstrains(&caCert); err != nil {
return "", fmt.Errorf("validating certificate constraints: %w", err) return cert.NebulaCertificate{}, fmt.Errorf("validating certificate constraints: %w", err)
} }
if err := signCert(&hostCert, caCreds.SigningPrivateKey); err != nil { if err := signCert(&hostCert, caCreds.SigningPrivateKey); err != nil {
return "", fmt.Errorf("signing host cert with ca.key: %w", err) return cert.NebulaCertificate{}, fmt.Errorf("signing host cert with ca.key: %w", err)
} }
hostCertPEM, err := hostCert.MarshalToPEM() return hostCert, nil
if err != nil {
return "", fmt.Errorf("marshalling host.crt: %w", err)
}
return string(hostCertPEM), nil
} }
// NewHostCredentials generates a new key/cert for a nebula host using the CA // NewHostCredentials generates a new key/cert for a nebula host using the CA
@ -110,38 +97,26 @@ func NewHostCredentials(
pub HostPublicCredentials, priv HostPrivateCredentials, err error, pub HostPublicCredentials, priv HostPrivateCredentials, err error,
) { ) {
// The logic here is largely based on var (
// https://github.com/slackhq/nebula/blob/v1.4.0/cmd/nebula-cert/sign.go encPrivKey = NewEncryptingPrivateKey()
encPubKey = encPrivKey.PublicKey()
var hostPub, hostKey []byte signingPubKey, signingPrivKey = GenerateSigningPair()
{ )
var pubkey, privkey [32]byte
if _, err = io.ReadFull(rand.Reader, privkey[:]); err != nil {
err = fmt.Errorf("reading random bytes to form private key: %w", err)
return
}
curve25519.ScalarBaseMult(&pubkey, &privkey)
hostPub, hostKey = pubkey[:], privkey[:]
}
signingPubKey, signingPrivKey := GenerateSigningPair() hostCert, err := NewHostCert(caCreds, encPubKey, hostName, ip)
hostPubPEM := cert.MarshalX25519PublicKey(hostPub)
hostKeyPEM := cert.MarshalX25519PrivateKey(hostKey)
hostCertPEM, err := NewHostCertPEM(caCreds, string(hostPubPEM), hostName, ip)
if err != nil { if err != nil {
err = fmt.Errorf("creating host certificate: %w", err) err = fmt.Errorf("creating host certificate: %w", err)
return return
} }
pub = HostPublicCredentials{ pub = HostPublicCredentials{
CertPEM: hostCertPEM, Cert: hostCert,
SigningKey: signingPubKey, SigningKey: signingPubKey,
} }
priv = HostPrivateCredentials{ priv = HostPrivateCredentials{
PrivateKeyPEM: string(hostKeyPEM), EncryptingPrivateKey: encPrivKey,
SigningPrivateKey: signingPrivKey, SigningPrivateKey: signingPrivKey,
} }
@ -151,14 +126,11 @@ func NewHostCredentials(
// NewCACredentials generates a CACredentials. The domain should be the network's root domain, // NewCACredentials generates a CACredentials. The domain should be the network's root domain,
// and is included in the signing certificate's Name field. // and is included in the signing certificate's Name field.
func NewCACredentials(domain string, subnet *net.IPNet) (CACredentials, error) { func NewCACredentials(domain string, subnet *net.IPNet) (CACredentials, error) {
var (
// The logic here is largely based on signingPubKey, signingPrivKey = GenerateSigningPair()
// https://github.com/slackhq/nebula/blob/v1.4.0/cmd/nebula-cert/ca.go now = time.Now()
expireAt = now.Add(2 * 365 * 24 * time.Hour)
signingPubKey, signingPrivKey := GenerateSigningPair() )
now := time.Now()
expireAt := now.Add(2 * 365 * 24 * time.Hour)
caCert := cert.NebulaCertificate{ caCert := cert.NebulaCertificate{
Details: cert.NebulaCertificateDetails{ Details: cert.NebulaCertificateDetails{
@ -175,33 +147,11 @@ func NewCACredentials(domain string, subnet *net.IPNet) (CACredentials, error) {
return CACredentials{}, fmt.Errorf("signing caCert: %w", err) return CACredentials{}, fmt.Errorf("signing caCert: %w", err)
} }
certPEM, err := caCert.MarshalToPEM()
if err != nil {
return CACredentials{}, fmt.Errorf("marshaling caCert: %w", err)
}
return CACredentials{ return CACredentials{
Public: CAPublicCredentials{ Public: CAPublicCredentials{
CertPEM: string(certPEM), Cert: caCert,
SigningKey: signingPubKey, SigningKey: signingPubKey,
}, },
SigningPrivateKey: signingPrivKey, SigningPrivateKey: signingPrivKey,
}, nil }, nil
} }
// IPFromHostCertPEM is a convenience function for parsing the IP of a host out
// of its nebula cert.
func IPFromHostCertPEM(hostCertPEM string) (net.IP, error) {
hostCert, _, err := cert.UnmarshalNebulaCertificateFromPEM([]byte(hostCertPEM))
if err != nil {
return nil, fmt.Errorf("unmarshaling host certificate as PEM: %w", err)
}
ips := hostCert.Details.Ips
if len(ips) == 0 {
return nil, fmt.Errorf("malformed nebula host cert: no IPs")
}
return ips[0].IP, nil
}

78
go/nebula/signing_key.go Normal file
View File

@ -0,0 +1,78 @@
package nebula
import (
"crypto/ed25519"
"crypto/rand"
"fmt"
)
var (
sigPrivKeyPrefix = []byte("s0")
sigPubKeyPrefix = []byte("S0")
)
// SigningPrivateKey wraps an ed25519.PrivateKey to provide convenient text
// (un)marshaling methods.
type SigningPrivateKey ed25519.PrivateKey
// MarshalText implements the encoding.TextMarshaler interface.
func (k SigningPrivateKey) MarshalText() ([]byte, error) {
return encodeWithPrefix(sigPrivKeyPrefix, k), nil
}
// UnmarshalText implements the encoding.TextUnmarshaler interface.
func (k *SigningPrivateKey) UnmarshalText(b []byte) error {
b, err := decodeWithPrefix(sigPrivKeyPrefix, b)
if err != nil {
return fmt.Errorf("unmarshaling: %w", err)
}
*k = SigningPrivateKey(b)
return nil
}
func (k SigningPrivateKey) String() string {
b, err := k.MarshalText()
if err != nil {
panic(err)
}
return string(b)
}
// SigningPublicKey wraps an ed25519.PublicKey to provide convenient text
// (un)marshaling methods.
type SigningPublicKey ed25519.PublicKey
// MarshalText implements the encoding.TextMarshaler interface.
func (pk SigningPublicKey) MarshalText() ([]byte, error) {
return encodeWithPrefix(sigPubKeyPrefix, pk), nil
}
// UnmarshalText implements the encoding.TextUnmarshaler interface.
func (pk *SigningPublicKey) UnmarshalText(b []byte) error {
b, err := decodeWithPrefix(sigPubKeyPrefix, b)
if err != nil {
return fmt.Errorf("unmarshaling: %w", err)
}
*pk = SigningPublicKey(b)
return nil
}
func (pk SigningPublicKey) String() string {
b, err := pk.MarshalText()
if err != nil {
panic(err)
}
return string(b)
}
// GenerateSigningPair generates and returns a new key pair which can be used
// for signing arbitrary blobs of bytes.
func GenerateSigningPair() (SigningPublicKey, SigningPrivateKey) {
pub, priv, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
panic(fmt.Errorf("generating ed25519 key: %w", err))
}
return SigningPublicKey(pub), SigningPrivateKey(priv)
}

25
go/nebula/util.go Normal file
View File

@ -0,0 +1,25 @@
package nebula
import (
"bytes"
"errors"
"fmt"
"github.com/jxskiss/base62"
)
func encodeWithPrefix(prefix []byte, b []byte) []byte {
res := make([]byte, 0, len(prefix)+len(b)*4)
res = append(res, prefix...)
res = base62.EncodeToBuf(res, b)
return res
}
func decodeWithPrefix(prefix []byte, b []byte) ([]byte, error) {
if len(b) < len(prefix) {
return nil, errors.New("input is too short")
} else if !bytes.HasPrefix(b, prefix) {
return nil, fmt.Errorf("missing expected prefix %q", prefix)
}
return base62.Decode(b[len(prefix):])
}

View File

@ -1,20 +1,15 @@
rec { rec {
version = "0.8.1"; version = "1.0.0";
src = builtins.fetchGit { src = builtins.fetchGit {
name = "garage-v${version}"; name = "garage-v${version}";
url = "https://git.deuxfleurs.fr/Deuxfleurs/garage.git"; url = "https://git.deuxfleurs.fr/Deuxfleurs/garage.git";
rev = "76230f20282e73a5a5afa33af68152acaf732cf5"; rev = "ff093ddbb8485409f389abe7b5e569cb38d222d2";
}; };
# TODO compiling garage broke using 24.05, so for now use the pkgs pinned in
# the garage repo. Probably will revisit this when garage gets upgraded
# anyway.
pkgsSrc = (import "${src}/nix/common.nix").pkgsSrc;
package = { package = {
#pkgsSrc, pkgsSrc,
buildSystem, buildSystem,
hostSystem, hostSystem,
}: let }: let
@ -29,6 +24,16 @@ rec {
release = true; release = true;
git_version = version; git_version = version;
# subset of the default release features, as defined in:
# https://git.deuxfleurs.fr/Deuxfleurs/garage/src/commit/ff093ddbb8485409f389abe7b5e569cb38d222d2/nix/compile.nix#L171
features = [
"garage/bundled-libs"
"garage/lmdb"
"garage/sqlite"
"garage/metrics"
"garage/syslog"
];
}; };
in in

View File

@ -38,7 +38,9 @@ rec {
supportedSystems = [ supportedSystems = [
"x86_64-linux" "x86_64-linux"
"aarch64-linux" "aarch64-linux"
#"armv7l-linux-musl" # rpi, I think?
# rpi, TODO remember why this is disabled, try to re-enable it
#"armv7l-linux-musl"
"i686-linux" "i686-linux"
]; ];

View File

@ -68,14 +68,15 @@ for file in $test_files; do
if [ -z "$VERBOSE" ]; then if [ -z "$VERBOSE" ]; then
output="$TMPDIR/$file.log" output="$TMPDIR/$file.log"
mkdir -p "$(dirname "$output")" mkdir -p "$(dirname "$output")"
exec 3>"$output"
else else
output=/dev/stdout exec 3>&1
fi fi
( (
export TEST_CASE_FILE="$file" export TEST_CASE_FILE="$file"
if ! $SHELL -e -x "$root/cases/$file" >"$output" 2>&1; then if ! $SHELL -e -x "$root/cases/$file" >&3 2>&1; then
echo "$file FAILED" echo "$file FAILED"
if [ -z "$VERBOSE" ]; then if [ -z "$VERBOSE" ]; then
echo "output of test is as follows" echo "output of test is as follows"

View File

@ -40,13 +40,13 @@ if [ ! -d "$XDG_RUNTIME_DIR/isle" ]; then
allocations: allocations:
- data_path: a/data - data_path: a/data
meta_path: a/meta meta_path: a/meta
capacity: 100 capacity: 1
- data_path: b/data - data_path: b/data
meta_path: b/meta meta_path: b/meta
capacity: 100 capacity: 1
- data_path: c/data - data_path: c/data
meta_path: c/meta meta_path: c/meta
capacity: 100 capacity: 1
EOF EOF
echo "Creating 1-data-1-empty network" echo "Creating 1-data-1-empty network"