Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 4 additions & 11 deletions app/bootstrap/cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0

// package main is the main entry point of SPIKE Bootstrap.
package main

import (
Expand All @@ -15,21 +16,17 @@ import (
sdkErrors "github.com/spiffe/spike-sdk-go/errors"
"github.com/spiffe/spike-sdk-go/log"
"github.com/spiffe/spike-sdk-go/security/mem"
"github.com/spiffe/spike/app/bootstrap/internal/state"

"github.com/spiffe/spike/app/bootstrap/internal/lifecycle"
"github.com/spiffe/spike/app/bootstrap/internal/net"
"github.com/spiffe/spike/app/bootstrap/internal/state"
"github.com/spiffe/spike/internal/config"
)

const appName = "SPIKE Bootstrap"

func main() {
log.Info(
appName,
"message", "starting",
"version", config.BootstrapVersion,
)
log.Info(appName, "message", "starting", "version", config.BootstrapVersion)

// Hard timeout for the entire bootstrap process.
// A value of 0 means no timeout (infinite).
Expand Down Expand Up @@ -71,11 +68,7 @@ func main() {
return
}

log.Info(
appName,
"message", "FIPS 140.3 Status",
"enabled", fips140.Enabled(),
)
log.Info(appName, "message", "FIPS Status", "enabled", fips140.Enabled())

// Panics if it cannot acquire the source.
src := net.AcquireSource()
Expand Down
22 changes: 10 additions & 12 deletions app/bootstrap/internal/lifecycle/lifecycle.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,19 +49,13 @@ func ShouldBootstrap() bool {

// Memory backend doesn't need bootstrap.
if env.BackendStoreTypeVal() == env.Memory {
log.Info(
fName,
"message", "skipping bootstrap for in-memory backend",
)
log.Info(fName, "message", "skipping bootstrap for in-memory backend")
return false
}

// Lite backend doesn't need bootstrap.
if env.BackendStoreTypeVal() == env.Lite {
log.Info(
fName,
"message", "skipping bootstrap for lite backend",
)
log.Info(fName, "message", "skipping bootstrap for lite backend")
return false
}

Expand Down Expand Up @@ -95,7 +89,7 @@ func ShouldBootstrap() bool {
return false
}

// We're in Kubernetes---check the ConfigMap
// We're in Kubernetes: Check the ConfigMap
clientset, clientErr := kubernetes.NewForConfig(cfg)
if clientErr != nil {
failErr := sdkErrors.ErrK8sClientFailed.Clone()
Expand All @@ -107,7 +101,9 @@ func ShouldBootstrap() bool {

namespace := "spike"
// Read namespace from the service account if not specified
if nsBytes, readErr := os.ReadFile(k8sServiceAccountNamespace); readErr == nil {
if nsBytes, readErr := os.ReadFile(
k8sServiceAccountNamespace,
); readErr == nil {
namespace = string(nsBytes)
}

Expand All @@ -118,7 +114,7 @@ func ShouldBootstrap() bool {
)
if getErr != nil {
failErr := sdkErrors.ErrK8sReconciliationFailed.Wrap(getErr)
// ConfigMap doesn't exist or can't read it - proceed with bootstrap
// ConfigMap doesn't exist or can't read it: Proceed with bootstrap
failErr.Msg = "failed to get ConfigMap: proceeding with bootstrap"
log.WarnErr(fName, *failErr)
return true
Expand All @@ -140,10 +136,12 @@ func ShouldBootstrap() bool {
keyBootstrapCompletedByPod, completedByPod,
"reason", reason,
)

// Bootstrap is complete: Skip further bootstraps
return false
}

// Bootstrap is not completed: proceed with bootstrap
// Bootstrap is not completed: Proceed with bootstrap
return true
}

Expand Down
111 changes: 1 addition & 110 deletions app/bootstrap/internal/net/broadcast.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,7 @@ import (
"encoding/hex"
"fmt"
"io"
"time"

"github.com/cloudflare/circl/secretsharing"
"github.com/spiffe/go-spiffe/v2/workloadapi"
spike "github.com/spiffe/spike-sdk-go/api"
"github.com/spiffe/spike-sdk-go/config/env"
Expand Down Expand Up @@ -48,7 +46,6 @@ import (
// Parameters:
// - ctx: Context for cancellation and timeout control
// - api: SPIKE API client for communicating with keepers

func BroadcastKeepers(ctx context.Context, api *spike.API) {
const fName = "BroadcastKeepers"

Expand Down Expand Up @@ -97,61 +94,6 @@ func BroadcastKeepers(ctx context.Context, api *spike.API) {
}
}

// broadcastToKeeper sends a root key share to a single SPIKE Keeper instance.
// It creates a timeout context for the operation and retries with exponential
// backoff until success or the maximum retry attempts are exhausted.
//
// Parameters:
// - ctx: Parent context for cancellation
// - api: SPIKE API client for communicating with keepers
// - rs: Root key shares generated by Shamir secret sharing
// - keeperID: Identifier of the target keeper
// - keeperURL: URL of the target keeper
// - timeout: Timeout duration for the operation
// - maxRetries: Maximum number of retry attempts
//
// Returns:
// - *sdkErrors.SDKError if the keeper cannot be reached, nil on success
func broadcastToKeeper(
ctx context.Context, api *spike.API,
rs []secretsharing.Share, keeperID, keeperURL string,
timeout time.Duration, maxRetries int,
) *sdkErrors.SDKError {
const fName = "broadcastToKeeper"

keeperShare := state.KeeperShare(rs, keeperID)

keeperCtx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()

_, err := retry.WithMaxAttempts(keeperCtx, maxRetries,
func() (bool, *sdkErrors.SDKError) {
log.Info(fName,
"message", "sending shard to keeper",
"keeper_id", keeperID,
"keeper_url", keeperURL,
)

contributeErr := contributeWithContext(
keeperCtx, api, keeperShare, keeperID,
)
if contributeErr != nil {
warnErr := sdkErrors.ErrAPIPostFailed.Wrap(contributeErr)
warnErr.Msg = "failed to send shard: will retry"
log.WarnErr(fName, *warnErr)
return false, warnErr
}
return true, nil
},
retry.WithBackOffOptions(
retry.WithInitialInterval(env.BootstrapKeeperRetryInitialIntervalVal()),
retry.WithMaxInterval(env.BootstrapKeeperRetryMaxIntervalVal()),
),
)

return err
}

// VerifyInitialization confirms that the SPIKE Nexus initialization was
// successful by performing an end-to-end encryption test. The function
// generates a random 32-byte value, encrypts it using AES-GCM with the root
Expand Down Expand Up @@ -210,7 +152,7 @@ func VerifyInitialization(ctx context.Context, api *spike.API) {
// give up if we cannot verify the initialization in a timely manner.

_, retryErr := retry.Do(ctx, func() (bool, *sdkErrors.SDKError) {
verifyErr := api.Verify(randomText, nonce, ciphertext)
verifyErr := api.Verify(ctx, randomText, nonce, ciphertext)
if verifyErr != nil {
failErr := sdkErrors.ErrCryptoCipherVerificationFailed.Wrap(verifyErr)
failErr.Msg = "failed to verify initialization: will retry"
Expand Down Expand Up @@ -264,54 +206,3 @@ func AcquireSource() *workloadapi.X509Source {

return src
}

// contributeWithContext wraps api.Contribute so cancellation/timeouts can be
// enforced. The call is executed in a goroutine and the function waits for
// either the contribution result or ctx.Done, returning ctx.Err() when the
// context ends first.
//
// TODO: fix the API layer as soon as possible.
//
// IMPORTANT: This is a workaround, not a proper fix. When the context times
// out, this function returns early but the underlying api.Contribute call
// continues running in the background. This means:
// - The HTTP request is NOT actually cancelled
// - On retries, multiple concurrent requests may be in flight to the same
// keeper
// - Resources (goroutines, connections) are leaked until the orphaned calls
// complete
//
// The proper fix is to update the SDK so that api.Contribute accepts a
// context.Context parameter and uses http.NewRequestWithContext internally.
// This would allow the HTTP client to respect cancellation and timeouts.
//
// Parameters:
// - ctx: Context that controls cancellation/deadline for the contribution
// - api: SPIKE API client used to send the share
// - share: Keeper share being contributed
// - keeperID: Identifier of the target keeper
func contributeWithContext(
ctx context.Context, api *spike.API,
share secretsharing.Share, keeperID string,
) error {
done := make(chan error, 1)

go func() {
contributeErr := api.Contribute(share, keeperID)
// Explicitly send nil to avoid the nil-interface-with-nil-pointer issue.
// If we send a nil *SDKError directly, it becomes a non-nil error interface
// holding a nil pointer, causing "err != nil" checks to incorrectly pass.
if contributeErr == nil {
done <- nil
return
}
done <- contributeErr
}()

select {
case err := <-done:
return err
case <-ctx.Done():
return ctx.Err()
}
}
81 changes: 81 additions & 0 deletions app/bootstrap/internal/net/dispatch.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/
// \\\\\ Copyright 2024-present SPIKE contributors.
// \\\\\\\ SPDX-License-Identifier: Apache-2.0

package net

import (
"context"
"time"

"github.com/cloudflare/circl/secretsharing"
spike "github.com/spiffe/spike-sdk-go/api"
"github.com/spiffe/spike-sdk-go/config/env"
sdkErrors "github.com/spiffe/spike-sdk-go/errors"
"github.com/spiffe/spike-sdk-go/log"
"github.com/spiffe/spike-sdk-go/retry"

"github.com/spiffe/spike/app/bootstrap/internal/state"
)

// broadcastToKeeper sends a root key share to a single SPIKE Keeper instance.
// It creates a timeout context for the operation and retries with exponential
// backoff until success or the maximum retry attempts are exhausted.
//
// Parameters:
// - ctx: Parent context for cancellation
// - api: SPIKE API client for communicating with keepers
// - rs: Root key shares generated by Shamir secret sharing
// - keeperID: Identifier of the target keeper
// - keeperURL: URL of the target keeper
// - timeout: Timeout duration for the operation
// - maxRetries: Maximum number of retry attempts
//
// Returns:
// - *sdkErrors.SDKError if the keeper cannot be reached, nil on success
func broadcastToKeeper(
ctx context.Context, api *spike.API,
rs []secretsharing.Share, keeperID, keeperURL string,
timeout time.Duration, maxRetries int,
) *sdkErrors.SDKError {
const fName = "broadcastToKeeper"

keeperShare := state.KeeperShare(rs, keeperID)

// A zero timeout means no timeout (infinite). We must handle this explicitly
// because context.WithTimeout(ctx, 0) creates an already-expired context,
// not an infinite one.
keeperCtx := ctx
var cancel context.CancelFunc = func() {}
if timeout > 0 {
keeperCtx, cancel = context.WithTimeout(ctx, timeout)
}
defer cancel()

_, err := retry.WithMaxAttempts(keeperCtx, maxRetries,
func() (bool, *sdkErrors.SDKError) {
log.Info(fName,
"message", "sending shard to keeper",
"keeper_id", keeperID,
"keeper_url", keeperURL,
)

if contributeErr := api.Contribute(
keeperCtx, keeperShare, keeperID,
); contributeErr != nil {
warnErr := sdkErrors.ErrAPIPostFailed.Wrap(contributeErr)
warnErr.Msg = "failed to send shard: will retry"
log.WarnErr(fName, *warnErr)
return false, warnErr
}

return true, nil
},
retry.WithBackOffOptions(
retry.WithInitialInterval(env.BootstrapKeeperRetryInitialIntervalVal()),
retry.WithMaxInterval(env.BootstrapKeeperRetryMaxIntervalVal()),
),
)

return err
}
1 change: 1 addition & 0 deletions app/bootstrap/internal/state/global.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
var (
// rootKeySeed stores the root key seed generated during initialization.
// It is kept in memory to allow encryption operations during bootstrap.
// It is cleared immediately after a successful bootstrap.
rootKeySeed [crypto.AES256KeySize]byte
// rootKeySeedMu provides mutual exclusion for access to the root key seed.
rootKeySeedMu sync.RWMutex
Expand Down
7 changes: 5 additions & 2 deletions app/demo/cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
package main

import (
"context"
"fmt"

spike "github.com/spiffe/spike-sdk-go/api"
Expand Down Expand Up @@ -38,9 +39,11 @@ func main() {
// The path to store/retrieve/update the secret.
path := "tenants/demo/db/creds"

ctx := context.Background()

// Create a Secret
// https://pkg.go.dev/github.com/spiffe/spike-sdk-go/api#PutSecret
putErr := api.PutSecret(path, map[string]string{
putErr := api.PutSecret(ctx, path, map[string]string{
"username": "SPIKE",
"password": "SPIKE_Rocks",
})
Expand All @@ -51,7 +54,7 @@ func main() {

// Read the Secret
// https://pkg.go.dev/github.com/spiffe/spike-sdk-go/api#GetSecret
secret, getErr := api.GetSecret(path)
secret, getErr := api.GetSecret(ctx, path)
if getErr != nil {
fmt.Println("Error reading secret:", getErr.Error())
return
Expand Down
Loading
Loading