From 4965662548720de029151a19b06ad19b9e756b5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Volkan=20=C3=96z=C3=A7elik?= Date: Fri, 22 Nov 2024 11:18:07 -0800 Subject: [PATCH 1/3] hopefully ready for a new release. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Volkan Özçelik --- app/keeper/internal/route/base/factory.go | 32 ------------ app/keeper/internal/route/base/route.go | 20 +++++--- app/nexus/internal/net/{ => api}/url.go | 2 +- app/nexus/internal/net/{ => cache}/net.go | 7 +-- app/nexus/internal/poll/poll.go | 4 +- app/nexus/internal/route/auth/init_state.go | 4 +- app/nexus/internal/route/base/factory.go | 45 ----------------- app/nexus/internal/route/base/route.go | 30 ++++++++++-- app/nexus/internal/route/store/secret_get.go | 6 +-- .../internal/state/backend/memory/memory.go | 5 ++ .../internal/state/backend/sqlite/sqlite.go | 33 ++++++++++++- app/nexus/internal/state/base/init.go | 10 ++-- .../internal/state/base/{locks.go => lock.go} | 0 app/spike/cmd/main.go | 2 +- app/spike/internal/cmd/delete.go | 1 - app/spike/internal/cmd/put.go | 1 - app/spike/internal/net/{ => api}/url.go | 2 +- app/spike/internal/net/auth/login.go | 49 ------------------- app/spike/internal/net/auth/token.go | 6 ++- app/spike/internal/net/store/delete.go | 27 +++++++--- app/spike/internal/net/store/get.go | 22 +++++++-- app/spike/internal/net/store/list.go | 18 +++++-- app/spike/internal/net/store/put.go | 20 ++++++-- app/spike/internal/net/store/undelete.go | 24 +++++++-- internal/config/config.go | 6 +++ internal/entity/v1/reqres/reqres.go | 18 +++---- internal/net/factory.go | 39 +++++++++++++++ internal/net/handle.go | 17 ++++++- internal/net/post.go | 3 +- jira.xml | 8 +++ 30 files changed, 268 insertions(+), 193 deletions(-) delete mode 100644 app/keeper/internal/route/base/factory.go rename app/nexus/internal/net/{ => api}/url.go (98%) rename app/nexus/internal/net/{ => cache}/net.go (94%) delete mode 100644 app/nexus/internal/route/base/factory.go rename app/nexus/internal/state/base/{locks.go => lock.go} (100%) rename app/spike/internal/net/{ => api}/url.go (99%) delete mode 100644 app/spike/internal/net/auth/login.go create mode 100644 internal/net/factory.go diff --git a/app/keeper/internal/route/base/factory.go b/app/keeper/internal/route/base/factory.go deleted file mode 100644 index d1cba0e..0000000 --- a/app/keeper/internal/route/base/factory.go +++ /dev/null @@ -1,32 +0,0 @@ -// \\ SPIKE: Secure your secrets with SPIFFE. -// \\\\\ Copyright 2024-present SPIKE contributors. -// \\\\\\\ SPDX-License-Identifier: Apache-2.0 - -package base - -import ( - "github.com/spiffe/spike/app/keeper/internal/route/store" - "net/http" - - "github.com/spiffe/spike/internal/log" - "github.com/spiffe/spike/internal/net" -) - -func factory(p net.ApiUrl, a net.SpikeKeeperApiAction, m string) net.Handler { - log.Log().Info("route.factory", "path", p, "action", a, "method", m) - - // We only accept POST requests -- See ADR-0012. - if m != http.MethodPost { - return net.Fallback - } - - switch { - case a == net.ActionKeeperDefault && p == net.SpikeKeeperUrlKeep: - return store.RouteKeep - case a == net.ActionKeeperRead && p == net.SpikeKeeperUrlKeep: - return store.RouteShow - // Fallback route. - default: - return net.Fallback - } -} diff --git a/app/keeper/internal/route/base/route.go b/app/keeper/internal/route/base/route.go index 8bc94e0..d7b2bed 100644 --- a/app/keeper/internal/route/base/route.go +++ b/app/keeper/internal/route/base/route.go @@ -7,6 +7,7 @@ package base import ( "net/http" + "github.com/spiffe/spike/app/keeper/internal/route/store" "github.com/spiffe/spike/internal/log" "github.com/spiffe/spike/internal/net" ) @@ -19,12 +20,19 @@ import ( // Parameters: // - w: The HTTP ResponseWriter to write the response to // - r: The HTTP Request containing the client's request details -func Route( - w http.ResponseWriter, r *http.Request, audit *log.AuditEntry, -) error { - return factory( +func Route(w http.ResponseWriter, r *http.Request, a *log.AuditEntry) error { + return net.RouteFactory[net.SpikeKeeperApiAction]( net.ApiUrl(r.URL.Path), - net.SpikeKeeperApiAction(r.URL.Query().Get("action")), + net.SpikeKeeperApiAction(r.URL.Query().Get(net.KeyApiAction)), r.Method, - )(w, r, audit) + func(a net.SpikeKeeperApiAction, p net.ApiUrl) net.Handler { + switch { + case a == net.ActionKeeperDefault && p == net.SpikeKeeperUrlKeep: + return store.RouteKeep + case a == net.ActionKeeperRead && p == net.SpikeKeeperUrlKeep: + return store.RouteShow + default: + return net.Fallback + } + })(w, r, a) } diff --git a/app/nexus/internal/net/url.go b/app/nexus/internal/net/api/url.go similarity index 98% rename from app/nexus/internal/net/url.go rename to app/nexus/internal/net/api/url.go index 4657a4c..68546e6 100644 --- a/app/nexus/internal/net/url.go +++ b/app/nexus/internal/net/api/url.go @@ -2,7 +2,7 @@ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 -package net +package api import ( "net/url" diff --git a/app/nexus/internal/net/net.go b/app/nexus/internal/net/cache/net.go similarity index 94% rename from app/nexus/internal/net/net.go rename to app/nexus/internal/net/cache/net.go index 2b62ec1..6b0d66b 100644 --- a/app/nexus/internal/net/net.go +++ b/app/nexus/internal/net/cache/net.go @@ -2,7 +2,7 @@ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 -package net +package cache import ( "errors" @@ -10,6 +10,7 @@ import ( "github.com/go-jose/go-jose/v4/json" "github.com/spiffe/go-spiffe/v2/workloadapi" + "github.com/spiffe/spike/app/nexus/internal/net/api" "github.com/spiffe/spike/internal/auth" "github.com/spiffe/spike/internal/entity/v1/reqres" "github.com/spiffe/spike/internal/net" @@ -63,7 +64,7 @@ func UpdateCache( ) } - _, err = net.Post(client, UrlKeeperWrite(), md) + _, err = net.Post(client, api.UrlKeeperWrite(), md) return err } @@ -109,7 +110,7 @@ func FetchFromCache(source *workloadapi.X509Source) (string, error) { ) } - data, err := net.Post(client, UrlKeeperRead(), md) + data, err := net.Post(client, api.UrlKeeperRead(), md) var res reqres.RootKeyReadResponse if len(data) == 0 { diff --git a/app/nexus/internal/poll/poll.go b/app/nexus/internal/poll/poll.go index 948263f..711396e 100644 --- a/app/nexus/internal/poll/poll.go +++ b/app/nexus/internal/poll/poll.go @@ -10,7 +10,7 @@ import ( "github.com/spiffe/go-spiffe/v2/workloadapi" - "github.com/spiffe/spike/app/nexus/internal/net" + "github.com/spiffe/spike/app/nexus/internal/net/cache" state "github.com/spiffe/spike/app/nexus/internal/state/base" "github.com/spiffe/spike/internal/log" ) @@ -67,7 +67,7 @@ func Tick( for { select { case <-ticker.C: - err := net.UpdateCache(source, key) + err := cache.UpdateCache(source, key) if err != nil { log.Log().Error("tick", "msg", "Failed to update the cache", diff --git a/app/nexus/internal/route/auth/init_state.go b/app/nexus/internal/route/auth/init_state.go index 97056ae..eeec52b 100644 --- a/app/nexus/internal/route/auth/init_state.go +++ b/app/nexus/internal/route/auth/init_state.go @@ -18,7 +18,9 @@ import ( state "github.com/spiffe/spike/app/nexus/internal/state/base" ) -func updateStateForInit(recoveryToken string, adminTokenBytes, salt []byte) error { +func updateStateForInit( + recoveryToken string, adminTokenBytes, salt []byte, +) error { iterationCount := env.Pbkdf2IterationCount() hashLength := env.ShaHashLength() recoveryTokenHash := pbkdf2.Key( diff --git a/app/nexus/internal/route/base/factory.go b/app/nexus/internal/route/base/factory.go deleted file mode 100644 index 9b4719e..0000000 --- a/app/nexus/internal/route/base/factory.go +++ /dev/null @@ -1,45 +0,0 @@ -// \\ SPIKE: Secure your secrets with SPIFFE. -// \\\\\ Copyright 2024-present SPIKE contributors. -// \\\\\\\ SPDX-License-Identifier: Apache-2.0 - -package base - -import ( - "net/http" - - "github.com/spiffe/spike/app/nexus/internal/route/auth" - "github.com/spiffe/spike/app/nexus/internal/route/store" - "github.com/spiffe/spike/internal/log" - "github.com/spiffe/spike/internal/net" -) - -func factory(p net.ApiUrl, a net.SpikeNexusApiAction, m string) net.Handler { - log.Log().Info("route.factory", "path", p, "action", a, "method", m) - - // We only accept POST requests -- See ADR-0012. - if m != http.MethodPost { - return net.Fallback - } - - switch { - //case a == net.ActionNexusAdminLogin && p == net.SpikeNexusUrlLogin: - // return auth.RouteAdminLogin - case a == net.ActionNexusDefault && p == net.SpikeNexusUrlInit: - return auth.RouteInit - case a == net.ActionNexusCheck && p == net.SpikeNexusUrlInit: - return auth.RouteInitCheck - case a == net.ActionNexusDefault && p == net.SpikeNexusUrlSecrets: - return store.RoutePutSecret - case a == net.ActionNexusGet && p == net.SpikeNexusUrlSecrets: - return store.RouteGetSecret - case a == net.ActionNexusDelete && p == net.SpikeNexusUrlSecrets: - return store.RouteDeleteSecret - case a == net.ActionNexusUndelete && p == net.SpikeNexusUrlSecrets: - return store.RouteUndeleteSecret - case a == net.ActionNexusList && p == net.SpikeNexusUrlSecrets: - return store.RouteListPaths - // Fallback route. - default: - return net.Fallback - } -} diff --git a/app/nexus/internal/route/base/route.go b/app/nexus/internal/route/base/route.go index 0ebb1df..f12096e 100644 --- a/app/nexus/internal/route/base/route.go +++ b/app/nexus/internal/route/base/route.go @@ -5,9 +5,12 @@ package base import ( + "net/http" + + "github.com/spiffe/spike/app/nexus/internal/route/auth" + "github.com/spiffe/spike/app/nexus/internal/route/store" "github.com/spiffe/spike/internal/log" "github.com/spiffe/spike/internal/net" - "net/http" ) // Route handles all incoming HTTP requests by dynamically selecting and @@ -19,11 +22,30 @@ import ( // - w: The HTTP ResponseWriter to write the response to // - r: The HTTP Request containing the client's request details func Route( - w http.ResponseWriter, r *http.Request, audit *log.AuditEntry, + w http.ResponseWriter, r *http.Request, a *log.AuditEntry, ) error { - return factory( + return net.RouteFactory[net.SpikeNexusApiAction]( net.ApiUrl(r.URL.Path), net.SpikeNexusApiAction(r.URL.Query().Get(net.KeyApiAction)), r.Method, - )(w, r, audit) + func(a net.SpikeNexusApiAction, p net.ApiUrl) net.Handler { + switch { + case a == net.ActionNexusDefault && p == net.SpikeNexusUrlInit: + return auth.RouteInit + case a == net.ActionNexusCheck && p == net.SpikeNexusUrlInit: + return auth.RouteInitCheck + case a == net.ActionNexusDefault && p == net.SpikeNexusUrlSecrets: + return store.RoutePutSecret + case a == net.ActionNexusGet && p == net.SpikeNexusUrlSecrets: + return store.RouteGetSecret + case a == net.ActionNexusDelete && p == net.SpikeNexusUrlSecrets: + return store.RouteDeleteSecret + case a == net.ActionNexusUndelete && p == net.SpikeNexusUrlSecrets: + return store.RouteUndeleteSecret + case a == net.ActionNexusList && p == net.SpikeNexusUrlSecrets: + return store.RouteListPaths + default: + return net.Fallback + } + })(w, r, a) } diff --git a/app/nexus/internal/route/store/secret_get.go b/app/nexus/internal/route/store/secret_get.go index d6687ae..1625f2c 100644 --- a/app/nexus/internal/route/store/secret_get.go +++ b/app/nexus/internal/route/store/secret_get.go @@ -112,11 +112,7 @@ func RouteGetSecret( return err } - // res := reqres.SecretReadResponse{Data: secret} - - res := reqres.SecretReadResponse{Data: secret} - - responseBody := net.MarshalBody(res, w) + responseBody := net.MarshalBody(reqres.SecretReadResponse{Data: secret}, w) if responseBody == nil { return errors.New("failed to marshal response body") } diff --git a/app/nexus/internal/state/backend/memory/memory.go b/app/nexus/internal/state/backend/memory/memory.go index aa5ba45..cbf481c 100644 --- a/app/nexus/internal/state/backend/memory/memory.go +++ b/app/nexus/internal/state/backend/memory/memory.go @@ -18,12 +18,17 @@ import ( type NoopStore struct { } +// StoreAdminRecoveryMetadata saves the admin recovery metadata to the store. +// This implementation is a no-op and always returns nil. func (s *NoopStore) StoreAdminRecoveryMetadata( ctx context.Context, credentials data.RecoveryMetadata, ) error { return nil } +// LoadAdminRecoveryMetadata retrieves the admin recovery metadata from the +// store. This implementation always returns an empty RecoveryMetadata and +// nil error. func (s *NoopStore) LoadAdminRecoveryMetadata( ctx context.Context, ) (data.RecoveryMetadata, error) { diff --git a/app/nexus/internal/state/backend/sqlite/sqlite.go b/app/nexus/internal/state/backend/sqlite/sqlite.go index f20c29c..035fffe 100644 --- a/app/nexus/internal/state/backend/sqlite/sqlite.go +++ b/app/nexus/internal/state/backend/sqlite/sqlite.go @@ -326,7 +326,7 @@ func (s *DataStore) StoreAdminToken(ctx context.Context, token string) error { return nil } -// LoadAdminToken retrieves and decrypts the stored admin token. +// LoadAdminSigningToken retrieves and decrypts the stored admin token. // // Returns: // - ("", nil) if no token exists @@ -357,6 +357,20 @@ func (s *DataStore) LoadAdminSigningToken(ctx context.Context) (string, error) { return string(decrypted), nil } +// LoadAdminRecoveryMetadata retrieves the admin recovery metadata from the +// database. It returns an empty RecoveryMetadata struct if no record exists, or +// an error if the query fails. +// +// The method is thread-safe and uses a read lock when accessing the database. +// It queries the admin_recovery_metadata table for the single record with id=1, +// containing the token hash, encrypted root key, and salt used for admin +// recovery. +// +// Returns: +// - RecoveryMetadata: The retrieved credentials containing recovery token +// hash, encrypted root key, and salt +// - error: nil on success, sql.ErrNoRows if no record exists, or wrapped +// error on query failure func (s *DataStore) LoadAdminRecoveryMetadata(ctx context.Context) (data.RecoveryMetadata, error) { s.mu.RLock() defer s.mu.RUnlock() @@ -380,6 +394,23 @@ func (s *DataStore) LoadAdminRecoveryMetadata(ctx context.Context) (data.Recover return creds, nil } +// StoreAdminRecoveryMetadata saves or updates the admin recovery metadata in +// the database. The operation is performed atomically within a serializable +// transaction. +// +// The method is thread-safe and uses a write lock when accessing the database. +// It uses REPLACE INTO to ensure only one record exists in the +// admin_recovery_metadata table with id=1. The record includes token hash, +// encrypted root key, salt, and creation timestamp. +// +// Parameters: +// - ctx: Context for the database operation +// - credentials: RecoveryMetadata containing the token hash, encrypted root +// key, and salt to be stored +// +// Returns: +// - error: nil on success, or wrapped error describing the failure +// (transaction, query, or commit errors) func (s *DataStore) StoreAdminRecoveryMetadata( ctx context.Context, credentials data.RecoveryMetadata, ) error { diff --git a/app/nexus/internal/state/base/init.go b/app/nexus/internal/state/base/init.go index afdb127..ab17245 100644 --- a/app/nexus/internal/state/base/init.go +++ b/app/nexus/internal/state/base/init.go @@ -6,12 +6,12 @@ package base import ( "errors" - "github.com/spiffe/spike/internal/log" + "github.com/spiffe/spike/app/nexus/internal/net/cache" "github.com/spiffe/go-spiffe/v2/workloadapi" - "github.com/spiffe/spike/app/nexus/internal/net" "github.com/spiffe/spike/app/nexus/internal/state/persist" + "github.com/spiffe/spike/internal/log" "github.com/spiffe/spike/pkg/crypto" ) @@ -41,7 +41,7 @@ func Bootstrap(source *workloadapi.X509Source) error { existingRootKey := RootKey() if existingRootKey == "" { // Check if SPIKE Keeper has a cached root key first: - key, err := net.FetchFromCache(source) + key, err := cache.FetchFromCache(source) if err != nil { return err } @@ -65,7 +65,9 @@ func Bootstrap(source *workloadapi.X509Source) error { return ErrAlreadyInitialized } - log.Log().Info("boostrap", "msg", "first time initialization: generating new root key") + log.Log().Info("boostrap", + "msg", "first time initialization: generating new root key", + ) r, err := crypto.Aes256Seed() if err != nil { diff --git a/app/nexus/internal/state/base/locks.go b/app/nexus/internal/state/base/lock.go similarity index 100% rename from app/nexus/internal/state/base/locks.go rename to app/nexus/internal/state/base/lock.go diff --git a/app/spike/cmd/main.go b/app/spike/cmd/main.go index e2e07ea..c636599 100644 --- a/app/spike/cmd/main.go +++ b/app/spike/cmd/main.go @@ -6,11 +6,11 @@ package main import ( "context" - "github.com/spiffe/spike/pkg/spiffe" "github.com/spiffe/spike/app/spike/internal/cmd" "github.com/spiffe/spike/app/spike/internal/trust" "github.com/spiffe/spike/internal/log" + "github.com/spiffe/spike/pkg/spiffe" ) func main() { diff --git a/app/spike/internal/cmd/delete.go b/app/spike/internal/cmd/delete.go index fa312b0..b4bcaf5 100644 --- a/app/spike/internal/cmd/delete.go +++ b/app/spike/internal/cmd/delete.go @@ -6,7 +6,6 @@ package cmd import ( "fmt" - "strconv" "strings" diff --git a/app/spike/internal/cmd/put.go b/app/spike/internal/cmd/put.go index 8c1b7c7..4cd785a 100644 --- a/app/spike/internal/cmd/put.go +++ b/app/spike/internal/cmd/put.go @@ -6,7 +6,6 @@ package cmd import ( "fmt" - "strings" "github.com/spf13/cobra" diff --git a/app/spike/internal/net/url.go b/app/spike/internal/net/api/url.go similarity index 99% rename from app/spike/internal/net/url.go rename to app/spike/internal/net/api/url.go index 42a9b58..2ea4e3e 100644 --- a/app/spike/internal/net/url.go +++ b/app/spike/internal/net/api/url.go @@ -2,7 +2,7 @@ // \\\\\ Copyright 2024-present SPIKE contributors. // \\\\\\\ SPDX-License-Identifier: Apache-2.0 -package net +package api import ( "net/url" diff --git a/app/spike/internal/net/auth/login.go b/app/spike/internal/net/auth/login.go deleted file mode 100644 index 8e6bff5..0000000 --- a/app/spike/internal/net/auth/login.go +++ /dev/null @@ -1,49 +0,0 @@ -// \\ SPIKE: Secure your secrets with SPIFFE. -// \\\\\ Copyright 2024-present SPIKE contributors. -// \\\\\\\ SPDX-License-Identifier: Apache-2.0 - -package auth - -import ( - "encoding/json" - "errors" - - "github.com/spiffe/go-spiffe/v2/workloadapi" - api "github.com/spiffe/spike/app/spike/internal/net" - "github.com/spiffe/spike/internal/auth" - "github.com/spiffe/spike/internal/entity/v1/reqres" - "github.com/spiffe/spike/internal/net" -) - -// TODO: this flow will change; add documentation once the flow finalizes. -func Login(source *workloadapi.X509Source, password string) (string, error) { - r := reqres.AdminLoginRequest{ - Password: password, - } - mr, err := json.Marshal(r) - if err != nil { - return "", errors.Join( - errors.New("login: I am having problem generating the payload"), - err, - ) - } - - client, err := net.CreateMtlsClient(source, auth.CanTalkToPilot) - - body, err := net.Post(client, api.UrlAdminLogin(), mr) - if err != nil { - return "", errors.Join( - errors.New("login: I am having problem sending the request"), err) - } - - var res reqres.AdminLoginResponse - err = json.Unmarshal(body, &res) - if err != nil { - return "", errors.Join( - errors.New("login: Problem parsing response body"), - err, - ) - } - - return res.Token, nil -} diff --git a/app/spike/internal/net/auth/token.go b/app/spike/internal/net/auth/token.go index 7fe0bb7..01fd86e 100644 --- a/app/spike/internal/net/auth/token.go +++ b/app/spike/internal/net/auth/token.go @@ -12,7 +12,7 @@ import ( "github.com/spiffe/go-spiffe/v2/workloadapi" - api "github.com/spiffe/spike/app/spike/internal/net" + "github.com/spiffe/spike/app/spike/internal/net/api" "github.com/spiffe/spike/internal/auth" "github.com/spiffe/spike/internal/config" "github.com/spiffe/spike/internal/entity/data" @@ -67,7 +67,9 @@ func CheckInitState(source *workloadapi.X509Source) (data.InitState, error) { mr, err := json.Marshal(r) if err != nil { return data.NotInitialized, errors.Join( - errors.New("checkInitState: I am having problem generating the payload"), + errors.New( + "checkInitState: I am having problem generating the payload", + ), err, ) } diff --git a/app/spike/internal/net/store/delete.go b/app/spike/internal/net/store/delete.go index df651b1..ece20da 100644 --- a/app/spike/internal/net/store/delete.go +++ b/app/spike/internal/net/store/delete.go @@ -7,19 +7,34 @@ package store import ( "encoding/json" "errors" - net2 "github.com/spiffe/spike/app/spike/internal/net" "strconv" "github.com/spiffe/go-spiffe/v2/workloadapi" - + "github.com/spiffe/spike/app/spike/internal/net/api" "github.com/spiffe/spike/internal/auth" "github.com/spiffe/spike/internal/entity/v1/reqres" "github.com/spiffe/spike/internal/net" ) -// TODO: maybe more detailed godocs here. - -// DeleteSecret deletes a secret from SPIKE Nexus. +// DeleteSecret deletes specified versions of a secret at the given path using +// mTLS authentication. +// +// It converts string version numbers to integers, constructs a delete request, +// and sends it to the secrets API endpoint. If no versions are specified or +// conversion fails, no versions will be deleted. +// +// Parameters: +// - source: X509Source for mTLS client authentication +// - path: Path to the secret to delete +// - versions: String array of version numbers to delete +// +// Returns: +// - error: nil on success, unauthorized error if not logged in, or wrapped +// error on request/parsing failure +// +// Example: +// +// err := DeleteSecret(x509Source, "secret/path", []string{"1", "2"}) func DeleteSecret(source *workloadapi.X509Source, path string, versions []string) error { var vv []int @@ -55,7 +70,7 @@ func DeleteSecret(source *workloadapi.X509Source, return err } - _, err = net.Post(client, net2.UrlSecretDelete(), mr) + _, err = net.Post(client, api.UrlSecretDelete(), mr) if errors.Is(err, net.ErrUnauthorized) { return errors.New( `unauthorized. Please login first with 'spike login'`, diff --git a/app/spike/internal/net/store/get.go b/app/spike/internal/net/store/get.go index c53ed24..0ada197 100644 --- a/app/spike/internal/net/store/get.go +++ b/app/spike/internal/net/store/get.go @@ -7,17 +7,32 @@ package store import ( "encoding/json" "errors" - net2 "github.com/spiffe/spike/app/spike/internal/net" "github.com/spiffe/go-spiffe/v2/workloadapi" "github.com/spiffe/spike/app/spike/internal/entity/data" + "github.com/spiffe/spike/app/spike/internal/net/api" "github.com/spiffe/spike/internal/auth" "github.com/spiffe/spike/internal/entity/v1/reqres" "github.com/spiffe/spike/internal/net" ) -// GetSecret retrieves a secret from SPIKE Nexus. +// GetSecret retrieves a specific version of a secret at the given path using +// mTLS authentication. +// +// Parameters: +// - source: X509Source for mTLS client authentication +// - path: Path to the secret to retrieve +// - version: Version number of the secret to retrieve +// +// Returns: +// - *Secret: Secret data if found, nil if secret not found +// - error: nil on success, unauthorized error if not logged in, or +// wrapped error on request/parsing failure +// +// Example: +// +// secret, err := GetSecret(x509Source, "secret/path", 1) func GetSecret(source *workloadapi.X509Source, path string, version int) (*data.Secret, error) { r := reqres.SecretReadRequest{ @@ -38,7 +53,7 @@ func GetSecret(source *workloadapi.X509Source, return nil, err } - body, err := net.Post(client, net2.UrlSecretGet(), mr) + body, err := net.Post(client, api.UrlSecretGet(), mr) if errors.Is(err, net.ErrNotFound) { return nil, nil } @@ -57,6 +72,5 @@ func GetSecret(source *workloadapi.X509Source, ) } - // TODO: this is from SecretReadResponse, so maybe its entitiy should be somewhere common too. return &data.Secret{Data: res.Data}, nil } diff --git a/app/spike/internal/net/store/list.go b/app/spike/internal/net/store/list.go index 935cf92..db2ca12 100644 --- a/app/spike/internal/net/store/list.go +++ b/app/spike/internal/net/store/list.go @@ -7,16 +7,28 @@ package store import ( "encoding/json" "errors" - net2 "github.com/spiffe/spike/app/spike/internal/net" "github.com/spiffe/go-spiffe/v2/workloadapi" + "github.com/spiffe/spike/app/spike/internal/net/api" "github.com/spiffe/spike/internal/auth" "github.com/spiffe/spike/internal/entity/v1/reqres" "github.com/spiffe/spike/internal/net" ) -// ListSecretKeys lists the keys of all secrets in SPIKE Nexus. +// ListSecretKeys retrieves all secret keys using mTLS authentication. +// +// Parameters: +// - source: X509Source for mTLS client authentication +// +// Returns: +// - []string: Array of secret keys if found, empty array if none found +// - error: nil on success, unauthorized error if not logged in, or +// wrapped error on request/parsing failure +// +// Example: +// +// keys, err := ListSecretKeys(x509Source) func ListSecretKeys(source *workloadapi.X509Source) ([]string, error) { r := reqres.SecretListRequest{} mr, err := json.Marshal(r) @@ -34,7 +46,7 @@ func ListSecretKeys(source *workloadapi.X509Source) ([]string, error) { return []string{}, err } - body, err := net.Post(client, net2.UrlSecretList(), mr) + body, err := net.Post(client, api.UrlSecretList(), mr) if errors.Is(err, net.ErrNotFound) { return []string{}, nil } diff --git a/app/spike/internal/net/store/put.go b/app/spike/internal/net/store/put.go index cd13786..e3bed22 100644 --- a/app/spike/internal/net/store/put.go +++ b/app/spike/internal/net/store/put.go @@ -7,16 +7,30 @@ package store import ( "encoding/json" "errors" - net2 "github.com/spiffe/spike/app/spike/internal/net" "github.com/spiffe/go-spiffe/v2/workloadapi" + "github.com/spiffe/spike/app/spike/internal/net/api" "github.com/spiffe/spike/internal/auth" "github.com/spiffe/spike/internal/entity/v1/reqres" "github.com/spiffe/spike/internal/net" ) -// PutSecret upserts a secret to SPIKE Nexus. +// PutSecret creates or updates a secret at the specified path with the given +// values using mTLS authentication. +// +// Parameters: +// - source: X509Source for mTLS client authentication +// - path: Path where the secret should be stored +// - values: Map of key-value pairs representing the secret data +// +// Returns: +// - error: nil on success, unauthorized error if not logged in, or +// wrapped error on request/parsing failure +// +// Example: +// +// err := PutSecret(x509Source, "secret/path", map[string]string{"key": "value"}) func PutSecret(source *workloadapi.X509Source, path string, values map[string]string) error { @@ -38,7 +52,7 @@ func PutSecret(source *workloadapi.X509Source, return err } - _, err = net.Post(client, net2.UrlSecretPut(), mr) + _, err = net.Post(client, api.UrlSecretPut(), mr) if errors.Is(err, net.ErrUnauthorized) { return errors.New(`unauthorized. Please login first with 'spike login'`) } diff --git a/app/spike/internal/net/store/undelete.go b/app/spike/internal/net/store/undelete.go index 30bdfcb..f9d24f7 100644 --- a/app/spike/internal/net/store/undelete.go +++ b/app/spike/internal/net/store/undelete.go @@ -7,17 +7,33 @@ package store import ( "encoding/json" "errors" - net2 "github.com/spiffe/spike/app/spike/internal/net" - "github.com/spiffe/spike/internal/auth" + "strconv" "github.com/spiffe/go-spiffe/v2/workloadapi" + "github.com/spiffe/spike/app/spike/internal/net/api" + "github.com/spiffe/spike/internal/auth" "github.com/spiffe/spike/internal/entity/v1/reqres" "github.com/spiffe/spike/internal/net" ) -// UndeleteSecret undeletes a secret from SPIKE Nexus. +// UndeleteSecret restores previously deleted versions of a secret at the +// specified path using mTLS authentication. +// +// Parameters: +// - source: X509Source for mTLS client authentication +// - path: Path to the secret to restore +// - versions: String array of version numbers to restore. Empty array +// attempts no restoration +// +// Returns: +// - error: nil on success, unauthorized error if not logged in, or +// wrapped error on request/parsing failure +// +// Example: +// +// err := UndeleteSecret(x509Source, "secret/path", []string{"1", "2"}) func UndeleteSecret(source *workloadapi.X509Source, path string, versions []string) error { var vv []int @@ -53,7 +69,7 @@ func UndeleteSecret(source *workloadapi.X509Source, return err } - _, err = net.Post(client, net2.UrlSecretUndelete(), mr) + _, err = net.Post(client, api.UrlSecretUndelete(), mr) if errors.Is(err, net.ErrUnauthorized) { return errors.New(`unauthorized. Please login first with 'spike login'`) } diff --git a/internal/config/config.go b/internal/config/config.go index 49e3069..71c9f80 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -17,6 +17,8 @@ const NexusIssuer = "spike-nexus" const NexusAdminSubject = "spike-admin" const NexusAdminTokenId = "spike-admin-jwt" +// SpikeNexusDataFolder returns the path to the directory where Nexus stores +// its encrypted backup for its secrets and other data. func SpikeNexusDataFolder() string { homeDir, err := os.UserHomeDir() if err != nil { @@ -37,6 +39,8 @@ func SpikeNexusDataFolder() string { return filepath.Join(spikeDir, "/data") } +// SpikePilotRecoveryFolder returns the path to the directory where Pilot stores +// recovery material for its root key. func SpikePilotRecoveryFolder() string { homeDir, err := os.UserHomeDir() if err != nil { @@ -57,6 +61,8 @@ func SpikePilotRecoveryFolder() string { return filepath.Join(spikeDir, "/recovery") } +// SpikePilotRootKeyRecoveryFile returns the path to the file where Pilot stores +// the root key recovery file. func SpikePilotRootKeyRecoveryFile() string { folder := SpikePilotRecoveryFolder() diff --git a/internal/entity/v1/reqres/reqres.go b/internal/entity/v1/reqres/reqres.go index d800e1c..b81812b 100644 --- a/internal/entity/v1/reqres/reqres.go +++ b/internal/entity/v1/reqres/reqres.go @@ -5,8 +5,9 @@ package reqres import ( - "github.com/spiffe/spike/internal/entity/data" "time" + + "github.com/spiffe/spike/internal/entity/data" ) // RootKeyCacheRequest is to cache the generated root key in SPIKE Keep. @@ -24,6 +25,7 @@ var ErrLowEntropy = ErrorCode("low_entropy") var ErrAlreadyInitialized = ErrorCode("already_initialized") var ErrNotFound = ErrorCode("not_found") +// FallbackResponse is a generic response for any error. type FallbackResponse struct { Err ErrorCode `json:"err,omitempty"` } @@ -54,33 +56,27 @@ type AdminTokenWriteResponse struct { Err ErrorCode `json:"err,omitempty"` } +// CheckInitStateRequest is to check if the SPIKE Keep is initialized. type CheckInitStateRequest struct { } +// CheckInitStateResponse is to check if the SPIKE Keep is initialized. type CheckInitStateResponse struct { State data.InitState `json:"state"` Err ErrorCode `json:"err,omitempty"` } +// InitRequest is to initialize SPIKE as a superuser. type InitRequest struct { // Password string `json:"password"` } +// InitResponse is to initialize SPIKE as a superuser. type InitResponse struct { RecoveryToken string `json:"token"` Err ErrorCode `json:"err,omitempty"` } -type AdminLoginRequest struct { - Password string `json:"password"` -} - -type AdminLoginResponse struct { - Token string `json:"token"` - Signature string `json:"signature"` - Err ErrorCode `json:"err,omitempty"` -} - // SecretResponseMetadata is meta information about secrets for internal // tracking. type SecretResponseMetadata struct { diff --git a/internal/net/factory.go b/internal/net/factory.go new file mode 100644 index 0000000..8bb1804 --- /dev/null +++ b/internal/net/factory.go @@ -0,0 +1,39 @@ +// \\ SPIKE: Secure your secrets with SPIFFE. +// \\\\\ Copyright 2024-present SPIKE contributors. +// \\\\\\\ SPDX-License-Identifier: Apache-2.0 + +package net + +import ( + "net/http" + + "github.com/spiffe/spike/internal/log" +) + +// RouteFactory creates HTTP route handlers for API endpoints using a generic +// switching function. It enforces POST-only methods per ADR-0012 and logs +// route creation details. +// +// Type Parameters: +// - ApiAction: Type representing the API action to be handled +// +// Parameters: +// - p: API URL for the route +// - a: API action instance +// - m: HTTP method +// - switchyard: Function that returns appropriate handler based on +// action and URL +// +// Returns: +// - Handler: Route handler function or Fallback for non-POST methods +func RouteFactory[ApiAction any](p ApiUrl, a ApiAction, m string, + switchyard func(a ApiAction, p ApiUrl) Handler) Handler { + log.Log().Info("route.factory", "path", p, "action", a, "method", m) + + // We only accept POST requests -- See ADR-0012. + if m != http.MethodPost { + return Fallback + } + + return switchyard(a, p) +} diff --git a/internal/net/handle.go b/internal/net/handle.go index 619d0b7..84257ff 100644 --- a/internal/net/handle.go +++ b/internal/net/handle.go @@ -8,10 +8,23 @@ import ( "github.com/spiffe/spike/pkg/crypto" ) -// TODO: document +// Handler is a function type that processes HTTP requests with audit +// logging support. type Handler func(http.ResponseWriter, *http.Request, *log.AuditEntry) error -// TODO: document +// HandleRoute wraps an HTTP handler with audit logging functionality. +// It creates and manages audit log entries for the request lifecycle, +// including: +// - Generating unique trail IDs +// - Recording timestamps and durations +// - Tracking request status (created, success, error) +// - Capturing error information +// +// The wrapped handler is mounted at the root path ("/") and automatically +// logs entry and exit audit events for all requests. +// +// Parameters: +// - h: Handler function to wrap with audit logging func HandleRoute(h Handler) { http.HandleFunc("/", func( writer http.ResponseWriter, request *http.Request, diff --git a/internal/net/post.go b/internal/net/post.go index 8a5bd24..0b47323 100644 --- a/internal/net/post.go +++ b/internal/net/post.go @@ -7,9 +7,10 @@ package net import ( "bytes" "errors" - "github.com/spiffe/spike/internal/log" "io" "net/http" + + "github.com/spiffe/spike/internal/log" ) var ErrNotFound = errors.New("not found") diff --git a/jira.xml b/jira.xml index 0d2d139..28634cf 100644 --- a/jira.xml +++ b/jira.xml @@ -202,10 +202,18 @@ it gives a lot of options on just how secure you want to try and make things vs how painful it is to recover + + this is from SecretReadResponse, so maybe its entity should be somewhere common too. + return &data.Secret{Data: res.Data}, nil + based on the following, maybe move SQLite "create table" ddls to a separate file. Still a "tool" or a "job" can do that out-of-band. + update: for SQLite it does not matter as SQLite does not have a concept + of RBAC; creating a db is equivalent to creating a file. + For other databases, it can be considered, so maybe write an ADR for that. + ADR: It's generally considered better security practice to create the schema out-of-band (separate from the application) for several reasons: From e56d79d254f18fb68e86f01dee7fdf7ca17c6627 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Volkan=20=C3=96z=C3=A7elik?= Date: Fri, 22 Nov 2024 14:21:49 -0800 Subject: [PATCH 2/3] Ready for release v0.2.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Volkan Özçelik --- app/keeper/cmd/main.go | 5 +++-- app/nexus/cmd/main.go | 9 ++++++--- docs/quickstart.md | 11 +++++++++++ hack/register-leonardo.sh | 2 ++ internal/config/config.go | 10 +++------- internal/log/log.go | 3 +++ 6 files changed, 28 insertions(+), 12 deletions(-) diff --git a/app/keeper/cmd/main.go b/app/keeper/cmd/main.go index e2248df..e0ca834 100644 --- a/app/keeper/cmd/main.go +++ b/app/keeper/cmd/main.go @@ -21,6 +21,8 @@ import ( const appName = "SPIKE Keeper" func main() { + log.Log().Info(appName, "msg", appName, "version", config.KeeperVersion) + ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -33,9 +35,8 @@ func main() { trust.Authenticate(spiffeid) log.Log().Info(appName, - "msg", fmt.Sprintf("Starting service: %s v%s", + "msg", fmt.Sprintf("Started service: %s v%s", appName, config.KeeperVersion)) - if err := net.Serve( source, handle.InitializeRoutes, auth.CanTalkToKeeper, diff --git a/app/nexus/cmd/main.go b/app/nexus/cmd/main.go index 8d4d7ca..8760633 100644 --- a/app/nexus/cmd/main.go +++ b/app/nexus/cmd/main.go @@ -7,6 +7,7 @@ package main import ( "context" "errors" + "fmt" "time" "github.com/spiffe/spike/app/nexus/internal/env" @@ -24,6 +25,8 @@ import ( const appName = "SPIKE Nexus" func main() { + log.Log().Info(appName, "msg", appName, "version", config.NexusVersion) + ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -53,9 +56,9 @@ func main() { defer ticker.Stop() go poll.Tick(ctx, source, ticker) - log.Log().Info(appName, "msg", - "Starting service.", "version", config.NexusVersion) - + log.Log().Info(appName, + "msg", fmt.Sprintf("Started service: %s v%s", + appName, config.NexusVersion)) if err := net.Serve( source, handle.InitializeRoutes, auth.CanTalkToNexus, diff --git a/docs/quickstart.md b/docs/quickstart.md index 8be5b56..4c51481 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -111,6 +111,14 @@ cd $WORKSPACE/spike Once SPIRE Server is running, start the SPIRE Agent: ```bash +# The below script will ask for your password to give privileges to SPIRE Agent. +# +# This will allow SPIRE agent to introspect the system and get information +# about the running processes (path information, user information, SHA256 of the +# binary, etc.). This is part of the SPIRE Agent's workload attestation process. +# Without this, SPIRE Agent will have limited access to the system and may not +# be able to fully attest the workloads. + ./hack/spire-agent-starter.sh ``` @@ -130,6 +138,9 @@ window. Start the workloads: ```bash +# Optional: Increase the log level to debug: +export SPIKE_SYSTEM_LOG_LEVEL=debug + cd $WORKSPACE/spike ./nexus # Nexus ./keeper # Keeper diff --git a/hack/register-leonardo.sh b/hack/register-leonardo.sh index f245a43..29f27c8 100755 --- a/hack/register-leonardo.sh +++ b/hack/register-leonardo.sh @@ -7,7 +7,9 @@ PILOT_PATH="$(pwd)/spike" PILOT_SHA=$(sha256sum "$PILOT_PATH" | cut -d' ' -f1) +echo "Copying SPIKE pilot to /usr/local/bin/spike..." sudo cp "$PILOT_PATH" /usr/local/bin/spike +echo "Copied SPIKE pilot to /usr/local/bin/spike." PILOT_PATH="$(pwd)/spike" PILOT_SHA=$(sha256sum "$PILOT_PATH" | cut -d' ' -f1) diff --git a/internal/config/config.go b/internal/config/config.go index 71c9f80..d3d8763 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -9,13 +9,9 @@ import ( "path/filepath" ) -const NexusVersion = "0.1.0" -const PilotVersion = "0.1.0" -const KeeperVersion = "0.1.0" - -const NexusIssuer = "spike-nexus" -const NexusAdminSubject = "spike-admin" -const NexusAdminTokenId = "spike-admin-jwt" +const NexusVersion = "0.2.0" +const PilotVersion = "0.2.0" +const KeeperVersion = "0.2.0" // SpikeNexusDataFolder returns the path to the directory where Nexus stores // its encrypted backup for its secrets and other data. diff --git a/internal/log/log.go b/internal/log/log.go index 50bdd6f..c5efcf8 100644 --- a/internal/log/log.go +++ b/internal/log/log.go @@ -5,6 +5,7 @@ package log import ( + "fmt" "log" "log/slog" "os" @@ -32,6 +33,8 @@ func Log() *slog.Logger { Level: env.LogLevel(), } + fmt.Println("loglevel: ", opts.Level) + handler := slog.NewJSONHandler(os.Stdout, opts) logger = slog.New(handler) From 7a920c69a490c66f882e38d2d8687e627e6d5b78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Volkan=20=C3=96z=C3=A7elik?= Date: Fri, 22 Nov 2024 14:41:08 -0800 Subject: [PATCH 3/3] Update documentation snapshots MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Volkan Özçelik --- docs/tracking/changelog.md | 17 +++++++++++++---- docs/tracking/snapshots.md | 3 ++- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/docs/tracking/changelog.md b/docs/tracking/changelog.md index 30fb987..c43f8e6 100644 --- a/docs/tracking/changelog.md +++ b/docs/tracking/changelog.md @@ -2,17 +2,26 @@ ## Recent +TBD + +## [0.2.0] - 2024-11-22 + +### Added + * Added configuration options for SPIKE Nexus and SPIKE Keeper. -* Updated quickstart guide. +* Documentation updates. * Max secret versions is now configurable. * Introduced standard and configurable logging. -* Add community section and snapshots to the documentation. -* Added sqlite3 as a database backend. +* Added sqlite3 as a backing store. * Enabled cross-compilation and SHA checksums. -* Now admin users can use jwt authentication and short-lived session tokens. * Enhanced audit trails and error logging. * Created initial smoke/integration tests. * Stability improvements. + +### Changed + +* Removed password authentication for admin users. Admin users' SVIDs + are good enough to authenticate them. * Implemented passwordless admin login flow (*the neat thing about passwords is: you don't need them*). diff --git a/docs/tracking/snapshots.md b/docs/tracking/snapshots.md index ed6ea1b..2680e26 100644 --- a/docs/tracking/snapshots.md +++ b/docs/tracking/snapshots.md @@ -5,4 +5,5 @@ The **GitHub** repository contains the latest documentation of **SPIKE** already Here are the links to point-in-time documentation snapshots at each release: * [current](https://github.com/spiffe/spike/tree/main/docs) -* [v0.1.0](https://github.com/spiffe/spike/tree/v0.1.0/docs) \ No newline at end of file +* [v0.2.0](https://github.com/spiffe/spike/tree/v0.2.0/docs) +* [v0.1.0](https://github.com/spiffe/spike/tree/v0.1.0/docs)