From 18890eba37beea6af5d4332a101d180a8d2b670e Mon Sep 17 00:00:00 2001 From: Thijs Heijligenberg Date: Mon, 11 Aug 2025 15:07:33 +0200 Subject: [PATCH 01/18] Add Key Management Store to the SSH capability This is done via a .env flag and directory indication. We use the kms property of the user_auth field in CACAO for the reference to the key. The key identifier is just the name of the file in the directory as given in .env. This is convenient for persistence and interaction with other tasks. The keys are cached. There is room for an api interacting with the system. There is also some extra information in the ssh logging, which helps with identifying errors in the ssh layer. --- .env.example | 5 + internal/controller/controller.go | 9 ++ pkg/core/capability/ssh/keymanagement.go | 158 +++++++++++++++++++++++ pkg/core/capability/ssh/ssh.go | 41 ++++-- 4 files changed, 202 insertions(+), 11 deletions(-) create mode 100644 pkg/core/capability/ssh/keymanagement.go diff --git a/.env.example b/.env.example index 83a5bc2f3..3af2fe226 100644 --- a/.env.example +++ b/.env.example @@ -35,3 +35,8 @@ AUTH_ENABLED: false OIDC_PROVIDER: "https://authentikuri:9443/application/o/soarca/" OIDC_CLIENT_ID: "some client ID" OIDC_SKIP_TLS_VERIFY: false + +# SSH Key Management +ENABLE_SSH_KMS: false +SSH_KMS_DIR: "/ssh-kms/" + diff --git a/internal/controller/controller.go b/internal/controller/controller.go index 1d95dd4e4..6c5494f33 100644 --- a/internal/controller/controller.go +++ b/internal/controller/controller.go @@ -269,6 +269,15 @@ func initializeCore(app *gin.Engine) error { return err } + if utils.GetEnv("ENABLE_SSH_KMS", "false") == "true" { + kms_dir := utils.GetEnv("SSH_KMS_DIR", ".ssh/") + err = ssh.InitKeyManagement(kms_dir) + if err != nil { + log.Error(err) + return err + } + } + // Manual capability native routes routes.Manual(app, mainInteraction) diff --git a/pkg/core/capability/ssh/keymanagement.go b/pkg/core/capability/ssh/keymanagement.go new file mode 100644 index 000000000..9e238c66b --- /dev/null +++ b/pkg/core/capability/ssh/keymanagement.go @@ -0,0 +1,158 @@ +package ssh + +import ( + "fmt" + "os" + "path" + "slices" + "soarca/pkg/utils" + "strings" + + "golang.org/x/crypto/ssh" +) + +type KeyPair struct { + Public ssh.PublicKey + Private ssh.Signer +} +type KeyManagement struct { + underlying_dir string + cached_keys map[string]KeyPair +} + +var keyManagement *KeyManagement + +func InitKeyManagement(underlying_dir string) error { + keyManagement = &KeyManagement{ + underlying_dir: underlying_dir, + } + return keyManagement.Refresh() +} + +func (management *KeyManagement) GetKeyPair(name string) (*KeyPair, error) { + keypair, ok := management.cached_keys[name] + if !ok { + return nil, fmt.Errorf("Could not find keypair for %s", name) + } + return &keypair, nil +} +func (management *KeyManagement) GetPrivate(name string) (ssh.Signer, error) { + keypair, err := management.GetKeyPair(name) + if err != nil { + return nil, err + } + return keypair.Private, nil +} +func parsePrivateKey(filename string) (ssh.Signer, error) { + log.Tracef("Opening private key from path %s", filename) + file, err := os.Open(filename) + if err != nil { + return nil, err + } + passphrase := utils.GetEnv("SSH_KMS_PASSPHRASE", "") + file_buffer := make([]byte, 2048) + if _, err := file.Read(file_buffer); err != nil { + return nil, err + } + if passphrase == "" { + return ssh.ParsePrivateKey(file_buffer) + } else { + return ssh.ParsePrivateKeyWithPassphrase(file_buffer, []byte(passphrase)) + } +} +func parsePublicKey(filename string) (ssh.PublicKey, error) { + log.Tracef("Opening public key from path %s", filename) + file, err := os.Open(filename) + if err != nil { + return nil, err + } + file_buffer := make([]byte, 2048) + if _, err := file.Read(file_buffer); err != nil { + return nil, err + } + return ssh.ParsePublicKey(file_buffer) + +} +func (management *KeyManagement) Refresh() error { + management.cached_keys = make(map[string]KeyPair) + dir, err := os.Open(management.underlying_dir) + if err != nil { + return err + } + filenames, err := dir.Readdirnames(0) + if err != nil { + return err + } + for _, filename := range filenames { + if strings.HasSuffix(filename, ".pub") { + private_filename := strings.TrimSuffix(filename, ".pub") + if !slices.Contains(filenames, private_filename) { + return fmt.Errorf("found public key %s without corresponding private key (%s)", filename, private_filename) + } + private, err := parsePrivateKey(private_filename) + if err != nil { + return err + } + public, err := parsePublicKey(filename) + if err != nil { + return err + } + management.cached_keys[private_filename] = KeyPair{Public: public, Private: private} + } + } + return nil +} + +func (management *KeyManagement) Insert(public string, private string, name string) error { + for key, _ := range management.cached_keys { + if key == name { + return fmt.Errorf("Inserting key with name %s: already present!", name) + } + } + public_filename := path.Clean(name) + if strings.HasPrefix(public_filename, "..") { + return fmt.Errorf("Cannot insert key in parent of Key Management directory") + } + public_filename = path.Join(management.underlying_dir, public_filename) + private_filename := public_filename + ".pub" + public_file, err := os.Open(public_filename) + if err != nil { + return err + } + private_file, err := os.Open(private_filename) + if err != nil { + return err + } + n_read, err := public_file.WriteString(public) + if err != nil { + return err + } + if n_read < len(public) { + return fmt.Errorf("Write error: did not write whole public key file") + } + if _, err := private_file.WriteString(private); err != nil { + return err + } + if n_read < len(private) { + return fmt.Errorf("Write error: did not write whole private key file") + } + public_file.Close() + private_file.Close() + return management.insertInternal(public, private, name) +} + +func (management *KeyManagement) insertInternal(public string, private string, name string) error { + public_key, err := ssh.ParsePublicKey([]byte(public)) + if err != nil { + return err + } + passphrase := utils.GetEnv("SSH_KMS_PASSPHRASE", "") + var private_key ssh.Signer + if passphrase == "" { + private_key, err = ssh.ParsePrivateKey([]byte(private)) + } else { + private_key, err = ssh.ParsePrivateKeyWithPassphrase([]byte(private), []byte(passphrase)) + } + management.cached_keys[name] = KeyPair{Public: public_key, Private: private_key} + return nil +} diff --git a/pkg/core/capability/ssh/ssh.go b/pkg/core/capability/ssh/ssh.go index 25f3498ae..fea9e20ab 100644 --- a/pkg/core/capability/ssh/ssh.go +++ b/pkg/core/capability/ssh/ssh.go @@ -2,6 +2,7 @@ package ssh import ( "errors" + "fmt" "reflect" "soarca/pkg/core/capability" "soarca/pkg/models/cacao" @@ -69,7 +70,7 @@ func executeCommand(session *ssh.Session, response, err := session.Output(StripSshPrepend(command.Command)) if err != nil { - log.Error(err) + log.Errorf("Output: %s", err) return cacao.NewVariables(), err } results := cacao.NewVariables(cacao.Variable{Type: cacao.VariableTypeString, @@ -77,10 +78,11 @@ func executeCommand(session *ssh.Session, Value: string(response)}) log.Trace("Finished ssh execution will return the variables: ", results) sessionErr := session.Close() - if sessionErr != nil { - log.Error(sessionErr) + if sessionErr != nil && sessionErr.Error() != "EOF" { + // The ssh api is subtle, and it can happen that we get EOF as an error. + // This is likely not an error, as the session can also be closed by the host. + log.Errorf("Close: %s", sessionErr) } - return results, err } @@ -91,8 +93,22 @@ func getConfig(authentication cacao.AuthenticationInformation) (ssh.ClientConfig switch authentication.Type { case "user-auth": - config.Auth = []ssh.AuthMethod{ - ssh.Password(authentication.Password)} + if authentication.Password != "" { + config.Auth = []ssh.AuthMethod{ + ssh.Password(authentication.Password)} + } + if authentication.Kms { + if authentication.KmsKeyIdentifier == "" { + return config, fmt.Errorf("KMS indicated, but no kms_key_identifier given") + } + private_key, err := keyManagement.GetPrivate(authentication.KmsKeyIdentifier) + if err != nil { + return config, err + } + config.Auth = []ssh.AuthMethod{ + ssh.PublicKeys(private_key), + } + } return config, nil case "private-key": @@ -115,13 +131,13 @@ func getSession(config ssh.ClientConfig, target cacao.AgentTarget) (*ssh.Session host := CombinePortAndAddress(target.Address, target.Port) client, err := ssh.Dial("tcp", host, &config) if err != nil { - log.Error(err) + log.Errorf("Dialing: %s", err) return nil, nil, err } session, err := client.NewSession() if err != nil { - log.Error(err) + log.Errorf("Session: %s", err) close(client) return nil, nil, err } @@ -156,8 +172,11 @@ func CheckSshAuthenticationInfo(authentication cacao.AuthenticationInformation) switch authentication.Type { case "user-auth": - if strings.TrimSpace(authentication.Password) == "" { - return errors.New("password is empty") + if strings.TrimSpace(authentication.Password) == "" && !authentication.Kms { + return errors.New("password is empty and KMS is not indicated") + } + if authentication.Kms && strings.TrimSpace(authentication.KmsKeyIdentifier) == "" { + return errors.New("KMS is indicated but no identifier is given") } case "private-key": if strings.TrimSpace(authentication.PrivateKey) == "" { @@ -173,7 +192,7 @@ func close(client *ssh.Client) { if client != nil { err := client.Close() if err != nil { - log.Error(err) + log.Errorf("Closing: %s", err) } } } From 0a65bcf237711c893bc752843b07c58bd15cac11 Mon Sep 17 00:00:00 2001 From: Thijs Heijligenberg Date: Mon, 11 Aug 2025 15:31:54 +0200 Subject: [PATCH 02/18] Added example for the ssh KMS There is now an example playbook using the KMS system. There is also a testing docker config. This does not use the same setup as the existing ssh example unfortunately. The example requires some work to set up the KMS folder. You should first set up an example keypair in the deployment example, and then set up a folder for the KMS system containing those keys (either copying or using the deployment location). --- .../testing/ssh-kms-test/docker-compose.yml | 25 ++++ examples/ssh-kms-playbook.json | 117 ++++++++++++++++++ 2 files changed, 142 insertions(+) create mode 100644 deployments/docker/testing/ssh-kms-test/docker-compose.yml create mode 100644 examples/ssh-kms-playbook.json diff --git a/deployments/docker/testing/ssh-kms-test/docker-compose.yml b/deployments/docker/testing/ssh-kms-test/docker-compose.yml new file mode 100644 index 000000000..e818e4625 --- /dev/null +++ b/deployments/docker/testing/ssh-kms-test/docker-compose.yml @@ -0,0 +1,25 @@ +services: + openssh-server: + image: lscr.io/linuxserver/openssh-server:latest + container_name: openssh-server + hostname: openssh-server #optional + environment: + - PUID=1000 + - PGID=1000 + - TZ=Etc/UTC + - PUBLIC_KEY=yourpublickey #optional + - PUBLIC_KEY_FILE=/path/to/file #optional + - PUBLIC_KEY_DIR=/path/to/directory/containing/_only_/pubkeys #optional + - PUBLIC_KEY_URL=https://github.com/username.keys #optional + - SUDO_ACCESS=false #optional + - PASSWORD_ACCESS=false #optional + - USER_PASSWORD=password #optional + - USER_PASSWORD_FILE=/path/to/file #optional + - USER_NAME=linuxserver.io #optional + - LOG_STDOUT= ssh.log#optional + volumes: + - /home/thijs/Code/SOARCA/kms/deployments/docker/ssh/config:/config + ports: + - 2222:2222 + restart: unless-stopped + diff --git a/examples/ssh-kms-playbook.json b/examples/ssh-kms-playbook.json new file mode 100644 index 000000000..b4b7af4d2 --- /dev/null +++ b/examples/ssh-kms-playbook.json @@ -0,0 +1,117 @@ +{ + "type": "playbook", + "spec_version": "cacao-2.0", + "id": "playbook--300270f9-0e64-42c8-93cc-0927edbe3ae7", + "name": "Example ssh", + "description": "This playbook demonstrates ssh functionality", + "playbook_types": [ + "notification" + ], + "created_by": "identity--96abab60-238a-44ff-8962-5806aa60cbce", + "created": "2023-11-20T15:56:00.123456Z", + "modified": "2023-11-20T15:56:00.123456Z", + "valid_from": "2023-11-20T15:56:00.123456Z", + "valid_until": "2123-11-20T15:56:00.123456Z", + "priority": 1, + "severity": 1, + "impact": 1, + "labels": [ + "soarca", + "ssh", + "example" + ], + "authentication_info_definitions": { + "user-auth--b7ddc2ea-9f6a-4e82-8eaa-be202e942090": { + "type": "user-auth", + "kms":true, + "kms_key_identifier": "test", + "username": "linuxserver.io" + } + }, + "agent_definitions": { + "soarca--00010001-1000-1000-a000-000100010001": { + "type": "soarca", + "name": "soarca-ssh" + } + }, + "target_definitions": { + "ssh--1c3900b4-f86b-430d-b415-12312b9e31f4": { + "type": "ssh", + "name": "system 1", + "address": { + "ipv4": [ + "127.0.0.1" + ] + }, + "port": "2222", + "authentication_info": "user-auth--b7ddc2ea-9f6a-4e82-8eaa-be202e942090" + } + }, + "external_references": [ + { + "name": "TNO COSSAS", + "description": "TNO COSSAS", + "source": "TNO COSSAS", + "url": "https://cossas-project.org" + } + ], + "workflow_start": "start--9e7d62b2-88ac-4656-94e1-dbd4413ba008", + "workflow_exception": "end--a6f0b81e-affb-4bca-b4f6-a2d5af908958", + "workflow": { + "start--9e7d62b2-88ac-4656-94e1-dbd4413ba008": { + "type": "start", + "name": "Start ssh example", + "on_completion": "action--eb9372d4-d524-49fc-bf24-be26ea084779" + }, + "action--eb9372d4-d524-49fc-bf24-be26ea084779": { + "type": "action", + "name": "Execute command", + "description": "Execute command specified in variable", + "on_completion": "action--88f4c4df-fa96-44e6-b310-1c06d193ea55", + "commands": [ + { + "type": "ssh", + "command": "__command__:value" + } + ], + "targets": [ + "ssh--1c3900b4-f86b-430d-b415-12312b9e31f4" + ], + "agent": "soarca--00010001-1000-1000-a000-000100010001", + "step_variables": { + "__command__": { + "type": "string", + "value": "ls -la", + "constant": true + } + } + }, + "action--88f4c4df-fa96-44e6-b310-1c06d193ea55": { + "type": "action", + "name": "Touch file", + "description": "Touch file at path specified by variable", + "on_completion": "end--a6f0b81e-affb-4bca-b4f6-a2d5af908958", + "commands": [ + { + "type": "ssh", + "command": "touch __path__:value" + } + ], + "targets": [ + "ssh--1c3900b4-f86b-430d-b415-12312b9e31f4" + ], + "agent": "soarca--00010001-1000-1000-a000-000100010001", + "step_variables": { + "__path__": { + "type": "string", + "value": "touchy", + "constant": true + } + } + }, + "end--a6f0b81e-affb-4bca-b4f6-a2d5af908958": { + "type": "end", + "name": "End Flow" + } + } +} From d1a8f3dc37bc04d1ff1120bcf8128c0166a61d96 Mon Sep 17 00:00:00 2001 From: Thijs Heijligenberg Date: Tue, 12 Aug 2025 17:09:37 +0200 Subject: [PATCH 03/18] Added REST API for SSH key management This makes it possible to get the list of keys, add keys manually, and revoke keys. Revoking does not delete keys but just moves and renames the underlying files to prevent irrevertible mishaps. This should serve as a starting point for consumers such as soarca-gui. --- internal/controller/controller.go | 6 +- pkg/api/api.go | 17 +++++ pkg/api/keymanagement/keymanagement_api.go | 88 ++++++++++++++++++++++ pkg/core/capability/ssh/keymanagement.go | 48 +++++++++--- pkg/core/capability/ssh/ssh.go | 2 +- pkg/models/api/keymanagement.go | 9 +++ 6 files changed, 156 insertions(+), 14 deletions(-) create mode 100644 pkg/api/keymanagement/keymanagement_api.go create mode 100644 pkg/models/api/keymanagement.go diff --git a/internal/controller/controller.go b/internal/controller/controller.go index 6c5494f33..1c4ff7942 100644 --- a/internal/controller/controller.go +++ b/internal/controller/controller.go @@ -271,11 +271,13 @@ func initializeCore(app *gin.Engine) error { if utils.GetEnv("ENABLE_SSH_KMS", "false") == "true" { kms_dir := utils.GetEnv("SSH_KMS_DIR", ".ssh/") - err = ssh.InitKeyManagement(kms_dir) + log.Trace("Setting up key management in directory ", kms_dir) + key_manager, err := ssh.InitKeyManagement(kms_dir) if err != nil { - log.Error(err) + log.Error("Failed to set up key management: ", err) return err } + routes.KeyManagement(app, key_manager) } // Manual capability native routes diff --git a/pkg/api/api.go b/pkg/api/api.go index f1dc96982..aabd95528 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -7,10 +7,12 @@ import ( "soarca/internal/controller/decomposer_controller" "soarca/internal/controller/informer" "soarca/internal/logger" + "soarca/pkg/api/keymanagement" playbook_handler "soarca/pkg/api/playbook" reporter_handler "soarca/pkg/api/reporter" status_handler "soarca/pkg/api/status" "soarca/pkg/core/capability/manual/interaction" + "soarca/pkg/core/capability/ssh" manual_handler "soarca/pkg/api/manual" @@ -53,6 +55,11 @@ func Manual(app *gin.Engine, interaction interaction.IInteractionStorage) { manualHandler := manual_handler.NewManualHandler(interaction) ManualRoutes(app, manualHandler) } +func KeyManagement(app *gin.Engine, key_manager *ssh.KeyManagement) { + log.Trace("Setting up key management routes") + keyManagement := keymanagement.NewKeyManagementHandler(key_manager) + KeyManagementRoutes(app, keyManagement) +} func Api(app *gin.Engine, controller decomposer_controller.IController, @@ -144,3 +151,13 @@ func ManualRoutes(route *gin.Engine, manualHandler *manual_handler.ManualHandler manualRoutes.POST("/continue", manualHandler.PostContinue) } } + +func KeyManagementRoutes(route *gin.Engine, keyManagementHandler *keymanagement.KeyManagementHandler) { + keyManagementRoutes := route.Group("/keymanagement") + { + keyManagementRoutes.GET("/", keyManagementHandler.GetKeys) + keyManagementRoutes.PUT("/:keyname", keyManagementHandler.AddKey) + keyManagementRoutes.POST("/refresh/", keyManagementHandler.Refresh) + keyManagementRoutes.DELETE("/:keyname", keyManagementHandler.RevokeKey) + } +} diff --git a/pkg/api/keymanagement/keymanagement_api.go b/pkg/api/keymanagement/keymanagement_api.go new file mode 100644 index 000000000..393f49119 --- /dev/null +++ b/pkg/api/keymanagement/keymanagement_api.go @@ -0,0 +1,88 @@ +package keymanagement + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "reflect" + "soarca/internal/logger" + "soarca/pkg/core/capability/ssh" + "soarca/pkg/models/api" + "strconv" + + "github.com/gin-gonic/gin" +) + +var log *logger.Log + +type Empty struct{} + +func init() { + log = logger.Logger(reflect.TypeOf(Empty{}).PkgPath(), logger.Info, "", logger.Json) +} + +type KeyManagementHandler struct { + Manager *ssh.KeyManagement +} + +func NewKeyManagementHandler(manager *ssh.KeyManagement) *KeyManagementHandler { + return &KeyManagementHandler{ + Manager: manager, + } +} + +func (handler *KeyManagementHandler) GetKeys(context *gin.Context) { + keyInfo := handler.Manager.ListAllNames() + context.JSON(http.StatusOK, keyInfo) +} + +func (handler *KeyManagementHandler) Refresh(context *gin.Context) { + err := handler.Manager.Refresh() + if err != nil { + log.Trace("Refreshing keys has failed: ", err.Error()) + SendErrorResponse(context, http.StatusBadRequest, "Failed to refresh keys", "POST /keymanagement/refresh") + return + } + context.JSON(http.StatusOK, Empty{}) +} + +func (handler *KeyManagementHandler) AddKey(context *gin.Context) { + keyname := context.Param("keyname") + jsonData, err := io.ReadAll(context.Request.Body) + if err != nil { + log.Trace("Submit key has failed: ", err.Error()) + SendErrorResponse(context, http.StatusBadRequest, "Failed to read json on server side", "POST /keymanagement") + return + } + var key api.KeyManagementKey + if err := json.Unmarshal(jsonData, &key); err != nil { + log.Trace("Submit key failed to unmarshal: ", err.Error()) + SendErrorResponse(context, http.StatusBadRequest, "Failed to marshall json on server side", "POST /keymanagement") + return + } + handler.Manager.Insert(key.Public, key.Private, keyname) + context.JSON(http.StatusOK, Empty{}) +} + +func (handler *KeyManagementHandler) RevokeKey(context *gin.Context) { + keyname := context.Param("keyname") + if err := handler.Manager.Revoke(keyname); err != nil { + log.Trace("Revoke key failed: ", err.Error()) + SendErrorResponse(context, http.StatusBadRequest, "Failed to revoke key", "DELETE /keymanagement") + return + } + context.JSON(http.StatusOK, gin.H{ + "status": 200, + "message": fmt.Sprintf("Successfuly removed key %s from SOARCA listing. To permanently remove key, delete the revoked key files in key management directory", keyname), + }) +} + +func SendErrorResponse(context *gin.Context, status int, message string, orginal_call string) { + msg := gin.H{ + "status": strconv.Itoa(status), + "message": message, + "original-call": orginal_call, + } + context.JSON(status, msg) +} diff --git a/pkg/core/capability/ssh/keymanagement.go b/pkg/core/capability/ssh/keymanagement.go index 9e238c66b..be8189b51 100644 --- a/pkg/core/capability/ssh/keymanagement.go +++ b/pkg/core/capability/ssh/keymanagement.go @@ -7,6 +7,7 @@ import ( "slices" "soarca/pkg/utils" "strings" + "time" "golang.org/x/crypto/ssh" ) @@ -20,13 +21,14 @@ type KeyManagement struct { cached_keys map[string]KeyPair } -var keyManagement *KeyManagement +var globalKeyManagement *KeyManagement -func InitKeyManagement(underlying_dir string) error { - keyManagement = &KeyManagement{ +func InitKeyManagement(underlying_dir string) (*KeyManagement, error) { + globalKeyManagement = &KeyManagement{ underlying_dir: underlying_dir, } - return keyManagement.Refresh() + err := globalKeyManagement.Refresh() + return globalKeyManagement, err } func (management *KeyManagement) GetKeyPair(name string) (*KeyPair, error) { @@ -67,10 +69,11 @@ func parsePublicKey(filename string) (ssh.PublicKey, error) { return nil, err } file_buffer := make([]byte, 2048) - if _, err := file.Read(file_buffer); err != nil { + if _, err = file.Read(file_buffer); err != nil { return nil, err } - return ssh.ParsePublicKey(file_buffer) + key, _, _, _, err := ssh.ParseAuthorizedKey(file_buffer) + return key, err } func (management *KeyManagement) Refresh() error { @@ -89,13 +92,14 @@ func (management *KeyManagement) Refresh() error { if !slices.Contains(filenames, private_filename) { return fmt.Errorf("found public key %s without corresponding private key (%s)", filename, private_filename) } - private, err := parsePrivateKey(private_filename) + log.Trace("Found public key named ", private_filename) + private, err := parsePrivateKey(path.Join(management.underlying_dir, private_filename)) if err != nil { - return err + return fmt.Errorf("Private key parsing error: %s", err) } - public, err := parsePublicKey(filename) + public, err := parsePublicKey(path.Join(management.underlying_dir, filename)) if err != nil { - return err + return fmt.Errorf("Public key parsing error: %s", err) } management.cached_keys[private_filename] = KeyPair{Public: public, Private: private} } @@ -142,7 +146,7 @@ func (management *KeyManagement) Insert(public string, private string, name stri } func (management *KeyManagement) insertInternal(public string, private string, name string) error { - public_key, err := ssh.ParsePublicKey([]byte(public)) + public_key, _, _, _, err := ssh.ParseAuthorizedKey([]byte(public)) if err != nil { return err } @@ -156,3 +160,25 @@ func (management *KeyManagement) insertInternal(public string, private string, n management.cached_keys[name] = KeyPair{Public: public_key, Private: private_key} return nil } +func (management *KeyManagement) ListAllNames() []string { + keys := make([]string, len(management.cached_keys)) + index := 0 + for key := range management.cached_keys { + keys[index] = key + index++ + } + return keys +} +func (management *KeyManagement) Revoke(keyname string) error { + if _, ok := management.cached_keys[keyname]; !ok { + return fmt.Errorf("Unknown key %s", keyname) + } + public_filename := path.Join(management.underlying_dir, keyname+".pub") + private_filename := path.Join(management.underlying_dir, keyname) + now := time.Now() + suffix := fmt.Sprintf(".revoked_%d-%d_%d-%s-%d", now.Second(), now.Hour(), now.Day(), now.Month().String(), now.Year()) + os.Rename(public_filename, public_filename+suffix) + os.Rename(private_filename, private_filename+suffix) + delete(management.cached_keys, keyname) + return nil +} diff --git a/pkg/core/capability/ssh/ssh.go b/pkg/core/capability/ssh/ssh.go index fea9e20ab..9f2068eb0 100644 --- a/pkg/core/capability/ssh/ssh.go +++ b/pkg/core/capability/ssh/ssh.go @@ -101,7 +101,7 @@ func getConfig(authentication cacao.AuthenticationInformation) (ssh.ClientConfig if authentication.KmsKeyIdentifier == "" { return config, fmt.Errorf("KMS indicated, but no kms_key_identifier given") } - private_key, err := keyManagement.GetPrivate(authentication.KmsKeyIdentifier) + private_key, err := globalKeyManagement.GetPrivate(authentication.KmsKeyIdentifier) if err != nil { return config, err } diff --git a/pkg/models/api/keymanagement.go b/pkg/models/api/keymanagement.go new file mode 100644 index 000000000..6ce88ccca --- /dev/null +++ b/pkg/models/api/keymanagement.go @@ -0,0 +1,9 @@ +package api + +type KeyManagementKeyList struct { + Keys []string `json:"keys"` +} +type KeyManagementKey struct { + Private string `json:"private"` + Public string `json:"public"` +} From ab82f187ecd410e3a09af75e81ae8498cca3f342 Mon Sep 17 00:00:00 2001 From: Thijs Heijligenberg Date: Wed, 13 Aug 2025 11:42:07 +0200 Subject: [PATCH 04/18] Added key management unit tests This also removes some bugs from the existing code --- .gitignore | 1 + pkg/core/capability/ssh/keymanagement.go | 71 ++++++++++++++-- pkg/core/capability/ssh/keymanagement_test.go | 82 +++++++++++++++++++ pkg/core/capability/ssh/ssh_test.go | 4 +- test/unittest/mocks/mock_utils/test-key | 7 ++ test/unittest/mocks/mock_utils/test-key.pub | 1 + 6 files changed, 157 insertions(+), 9 deletions(-) create mode 100644 pkg/core/capability/ssh/keymanagement_test.go create mode 100644 test/unittest/mocks/mock_utils/test-key create mode 100644 test/unittest/mocks/mock_utils/test-key.pub diff --git a/.gitignore b/.gitignore index 387a33678..4be741be4 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ bin/* api/* **.env test/cacao/flatfile-db-example.json +pkg/core/capability/ssh/.ssh_test/ docs/public diff --git a/pkg/core/capability/ssh/keymanagement.go b/pkg/core/capability/ssh/keymanagement.go index be8189b51..84f37fd6d 100644 --- a/pkg/core/capability/ssh/keymanagement.go +++ b/pkg/core/capability/ssh/keymanagement.go @@ -2,6 +2,7 @@ package ssh import ( "fmt" + "io" "os" "path" "slices" @@ -108,7 +109,7 @@ func (management *KeyManagement) Refresh() error { } func (management *KeyManagement) Insert(public string, private string, name string) error { - for key, _ := range management.cached_keys { + for key := range management.cached_keys { if key == name { return fmt.Errorf("Inserting key with name %s: already present!", name) } @@ -121,11 +122,25 @@ func (management *KeyManagement) Insert(public string, private string, name stri private_filename := public_filename + ".pub" public_file, err := os.Open(public_filename) if err != nil { - return err + if os.IsNotExist(err) { + public_file, err = os.Create(public_filename) + if err != nil { + return err + } + } else { + return err + } } private_file, err := os.Open(private_filename) if err != nil { - return err + if os.IsNotExist(err) { + private_file, err = os.Create(private_filename) + if err != nil { + return err + } + } else { + return err + } } n_read, err := public_file.WriteString(public) if err != nil { @@ -169,16 +184,58 @@ func (management *KeyManagement) ListAllNames() []string { } return keys } + +const revoked_directory string = ".revoked" + +func moveFile(source string, dest string) error { + source_file, err := os.Open(source) + if err != nil { + return err + } + defer source_file.Close() + dest_file, err := os.OpenFile(dest, os.O_WRONLY, 0) + if err != nil { + if os.IsNotExist(err) { + dest_file, err = os.Create(dest) + if err != nil { + return err + } + } else { + return err + } + } + defer dest_file.Close() + if _, err := io.Copy(dest_file, source_file); err != nil { + return err + } + return os.Remove(source) +} func (management *KeyManagement) Revoke(keyname string) error { if _, ok := management.cached_keys[keyname]; !ok { return fmt.Errorf("Unknown key %s", keyname) } - public_filename := path.Join(management.underlying_dir, keyname+".pub") - private_filename := path.Join(management.underlying_dir, keyname) + if _, err := os.ReadDir(path.Join(management.underlying_dir, revoked_directory)); err != nil { + if err := os.Mkdir(path.Join(management.underlying_dir, revoked_directory), 0777); err != nil { + return fmt.Errorf("Error while creating directory for revoked keys: %s", err) + } + } now := time.Now() suffix := fmt.Sprintf(".revoked_%d-%d_%d-%s-%d", now.Second(), now.Hour(), now.Day(), now.Month().String(), now.Year()) - os.Rename(public_filename, public_filename+suffix) - os.Rename(private_filename, private_filename+suffix) + + public_filename := path.Join(management.underlying_dir, keyname+".pub") + public_target := path.Join(management.underlying_dir, revoked_directory, keyname+".pub"+suffix) + log.Infof("Moving %s to %s", public_filename, public_target) + if err := moveFile(public_filename, public_target); err != nil { + return fmt.Errorf("Error while moving key: %s", err) + } + + private_filename := path.Join(management.underlying_dir, keyname) + private_target := path.Join(management.underlying_dir, revoked_directory, keyname+suffix) + log.Infof("Moving %s to %s", private_filename, private_target) + if err := moveFile(private_filename, private_target); err != nil { + return fmt.Errorf("Error while moving key: %s", err) + } + delete(management.cached_keys, keyname) return nil } diff --git a/pkg/core/capability/ssh/keymanagement_test.go b/pkg/core/capability/ssh/keymanagement_test.go new file mode 100644 index 000000000..6f931445f --- /dev/null +++ b/pkg/core/capability/ssh/keymanagement_test.go @@ -0,0 +1,82 @@ +package ssh + +import ( + "os" + "path" + "slices" + "testing" + + "github.com/stretchr/testify/assert" +) + +const testdir string = ".ssh_test" +const testkey string = "test-key" + +func testkey_dir() string { + return path.Join("..", "..", "..", "..", "test", "unittest", "mocks", "mock_utils") +} + +func init() { + err := os.Mkdir(testdir, 0777) + if err != nil { + if err.(*os.PathError).Err.Error() == "file exists" { + if err := os.RemoveAll(testdir); err != nil { + panic(err) + } + if err := os.Mkdir(testdir, 0777); err != nil { + panic(err) + } + } else { + panic(err) + } + } + if _, err = InitKeyManagement(testdir); err != nil { + panic(err) + } +} + +func TestRevoke(t *testing.T) { + addKey(t, testkey) + assert.True(t, slices.Contains(globalKeyManagement.ListAllNames(), testkey)) + assert.Nil(t, globalKeyManagement.Revoke(testkey)) +} +func addKey(t *testing.T, keyname string) { + pubkey_path := path.Join(testkey_dir(), "test-key.pub") + privkey_path := path.Join(testkey_dir(), "test-key") + pubkey_file, err := os.Open(pubkey_path) + assert.Nil(t, err) + defer pubkey_file.Close() + + privkey_file, err := os.Open(privkey_path) + assert.Nil(t, err) + defer privkey_file.Close() + pubkey_buf := make([]byte, 2048) + privkey_buf := make([]byte, 2048) + _, err = pubkey_file.Read(pubkey_buf) + assert.Nil(t, err) + _, err = privkey_file.Read(privkey_buf) + assert.Nil(t, err) + assert.Nil(t, globalKeyManagement.Insert(string(pubkey_buf), string(privkey_buf), keyname)) +} + +func TestAddKey(t *testing.T) { + addKey(t, testkey) + assert.True(t, slices.Equal(globalKeyManagement.ListAllNames(), []string{testkey})) +} + +func copyFile(src string, dst string) error { + data, err := os.ReadFile(src) + if err != nil { + return err + } + return os.WriteFile(dst, data, 0666) +} +func TestRefresh(t *testing.T) { + assert.False(t, slices.Contains(globalKeyManagement.ListAllNames(), testkey+"1")) + pubkey_path := path.Join(testkey_dir(), "test-key.pub") + privkey_path := path.Join(testkey_dir(), "test-key") + copyFile(pubkey_path, path.Join(testdir, testkey+"1.pub")) + copyFile(privkey_path, path.Join(testdir, testkey+"1")) + globalKeyManagement.Refresh() + assert.True(t, slices.Contains(globalKeyManagement.ListAllNames(), testkey+"1")) +} diff --git a/pkg/core/capability/ssh/ssh_test.go b/pkg/core/capability/ssh/ssh_test.go index f5aeb5aa0..2ba58aed1 100644 --- a/pkg/core/capability/ssh/ssh_test.go +++ b/pkg/core/capability/ssh/ssh_test.go @@ -29,14 +29,14 @@ func TestAuthenticationValidationUserAuth(t *testing.T) { func TestAuthenticationValidationUserAuthMissingPassword(t *testing.T) { auth := cacao.AuthenticationInformation{Type: "user-auth", Username: "root"} result := CheckSshAuthenticationInfo(auth) - err := errors.New("password is empty") + err := errors.New("password is empty and KMS is not indicated") assert.Equal(t, result, err) } func TestAuthenticationValidationUserAuthSpacesAsPassword(t *testing.T) { auth := cacao.AuthenticationInformation{Type: "user-auth", Username: "root", Password: " "} result := CheckSshAuthenticationInfo(auth) - err := errors.New("password is empty") + err := errors.New("password is empty and KMS is not indicated") assert.Equal(t, result, err) } diff --git a/test/unittest/mocks/mock_utils/test-key b/test/unittest/mocks/mock_utils/test-key new file mode 100644 index 000000000..718092253 --- /dev/null +++ b/test/unittest/mocks/mock_utils/test-key @@ -0,0 +1,7 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW +QyNTUxOQAAACBs4o06933hic/ArsSo0fs9cUTk0AHc2vON1ZqS68Vf7gAAAJhCJkxRQiZM +UQAAAAtzc2gtZWQyNTUxOQAAACBs4o06933hic/ArsSo0fs9cUTk0AHc2vON1ZqS68Vf7g +AAAEBI7PvrlDTFnWuMJHGaLuUkulSpH/Ni378Y2vLZcpldxmzijTr3feGJz8CuxKjR+z1x +ROTQAdza843VmpLrxV/uAAAADnRoaWpzQFBDLTQ0MzE4AQIDBAUGBw== +-----END OPENSSH PRIVATE KEY----- diff --git a/test/unittest/mocks/mock_utils/test-key.pub b/test/unittest/mocks/mock_utils/test-key.pub new file mode 100644 index 000000000..d6e7b2c9e --- /dev/null +++ b/test/unittest/mocks/mock_utils/test-key.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGzijTr3feGJz8CuxKjR+z1xROTQAdza843VmpLrxV/u thijs@PC-44318 From ff98f149ae6849e80e1804ac2410d01bac0ee0a7 Mon Sep 17 00:00:00 2001 From: Thijs Heijligenberg Date: Wed, 13 Aug 2025 12:05:07 +0200 Subject: [PATCH 05/18] Improve example deployment --- .../testing/ssh-kms-test/docker-compose.yml | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/deployments/docker/testing/ssh-kms-test/docker-compose.yml b/deployments/docker/testing/ssh-kms-test/docker-compose.yml index e818e4625..ee32b830b 100644 --- a/deployments/docker/testing/ssh-kms-test/docker-compose.yml +++ b/deployments/docker/testing/ssh-kms-test/docker-compose.yml @@ -2,23 +2,15 @@ services: openssh-server: image: lscr.io/linuxserver/openssh-server:latest container_name: openssh-server - hostname: openssh-server #optional + hostname: openssh-server environment: - PUID=1000 - PGID=1000 - TZ=Etc/UTC - - PUBLIC_KEY=yourpublickey #optional - - PUBLIC_KEY_FILE=/path/to/file #optional - - PUBLIC_KEY_DIR=/path/to/directory/containing/_only_/pubkeys #optional - - PUBLIC_KEY_URL=https://github.com/username.keys #optional - - SUDO_ACCESS=false #optional - - PASSWORD_ACCESS=false #optional - - USER_PASSWORD=password #optional - - USER_PASSWORD_FILE=/path/to/file #optional - - USER_NAME=linuxserver.io #optional - - LOG_STDOUT= ssh.log#optional + - USER_NAME=linuxserver.io + - LOG_STDOUT= ssh.log volumes: - - /home/thijs/Code/SOARCA/kms/deployments/docker/ssh/config:/config + - ./config:/config ports: - 2222:2222 restart: unless-stopped From 5574c2685d9ab1c8a369b00a47816664424294c1 Mon Sep 17 00:00:00 2001 From: Thijs Heijligenberg Date: Wed, 13 Aug 2025 13:58:20 +0200 Subject: [PATCH 06/18] Added helper script for KMS example setup --- .env.example | 2 +- deployments/docker/testing/ssh-kms-test/prepare_keys.sh | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100755 deployments/docker/testing/ssh-kms-test/prepare_keys.sh diff --git a/.env.example b/.env.example index 3af2fe226..c69ca0fee 100644 --- a/.env.example +++ b/.env.example @@ -38,5 +38,5 @@ OIDC_SKIP_TLS_VERIFY: false # SSH Key Management ENABLE_SSH_KMS: false -SSH_KMS_DIR: "/ssh-kms/" +SSH_KMS_DIR: "deployments/docker/testing/ssh-kms-test/ssh-keystore/" diff --git a/deployments/docker/testing/ssh-kms-test/prepare_keys.sh b/deployments/docker/testing/ssh-kms-test/prepare_keys.sh new file mode 100755 index 000000000..21edf4c95 --- /dev/null +++ b/deployments/docker/testing/ssh-kms-test/prepare_keys.sh @@ -0,0 +1,6 @@ +#!/bin/sh +set -u +mkdir -p ssh-keystore/ +ssh-keygen -q -N "" -f ssh-keystore/test +mkdir -p config/.ssh/ +cat ssh-keystore/test.pub > config/.ssh/authorized_keys From b3dbee3cca7a774169fcfa193fe49ccba7344669 Mon Sep 17 00:00:00 2001 From: Thijs Heijligenberg Date: Wed, 13 Aug 2025 14:32:21 +0200 Subject: [PATCH 07/18] Updated linting errors --- pkg/api/keymanagement/keymanagement_api.go | 7 +++- pkg/core/capability/ssh/keymanagement.go | 41 ++++++++++++------- pkg/core/capability/ssh/keymanagement_test.go | 10 ++--- 3 files changed, 37 insertions(+), 21 deletions(-) diff --git a/pkg/api/keymanagement/keymanagement_api.go b/pkg/api/keymanagement/keymanagement_api.go index 393f49119..0e0b04a9c 100644 --- a/pkg/api/keymanagement/keymanagement_api.go +++ b/pkg/api/keymanagement/keymanagement_api.go @@ -61,7 +61,12 @@ func (handler *KeyManagementHandler) AddKey(context *gin.Context) { SendErrorResponse(context, http.StatusBadRequest, "Failed to marshall json on server side", "POST /keymanagement") return } - handler.Manager.Insert(key.Public, key.Private, keyname) + if err := handler.Manager.Insert(key.Public, key.Private, keyname); err != nil { + log.Trace("Submit key failed to insert: ", err.Error()) + SendErrorResponse(context, http.StatusBadRequest, "Failed to insert key on server side", "POST /keymanagement") + return + + } context.JSON(http.StatusOK, Empty{}) } diff --git a/pkg/core/capability/ssh/keymanagement.go b/pkg/core/capability/ssh/keymanagement.go index 84f37fd6d..31e8d5e86 100644 --- a/pkg/core/capability/ssh/keymanagement.go +++ b/pkg/core/capability/ssh/keymanagement.go @@ -35,7 +35,7 @@ func InitKeyManagement(underlying_dir string) (*KeyManagement, error) { func (management *KeyManagement) GetKeyPair(name string) (*KeyPair, error) { keypair, ok := management.cached_keys[name] if !ok { - return nil, fmt.Errorf("Could not find keypair for %s", name) + return nil, fmt.Errorf("could not find keypair for %s", name) } return &keypair, nil } @@ -96,11 +96,11 @@ func (management *KeyManagement) Refresh() error { log.Trace("Found public key named ", private_filename) private, err := parsePrivateKey(path.Join(management.underlying_dir, private_filename)) if err != nil { - return fmt.Errorf("Private key parsing error: %s", err) + return fmt.Errorf("private key parsing error: %s", err) } public, err := parsePublicKey(path.Join(management.underlying_dir, filename)) if err != nil { - return fmt.Errorf("Public key parsing error: %s", err) + return fmt.Errorf("public key parsing error: %s", err) } management.cached_keys[private_filename] = KeyPair{Public: public, Private: private} } @@ -111,12 +111,12 @@ func (management *KeyManagement) Refresh() error { func (management *KeyManagement) Insert(public string, private string, name string) error { for key := range management.cached_keys { if key == name { - return fmt.Errorf("Inserting key with name %s: already present!", name) + return fmt.Errorf("inserting key with name %s: already present!", name) } } public_filename := path.Clean(name) if strings.HasPrefix(public_filename, "..") { - return fmt.Errorf("Cannot insert key in parent of Key Management directory") + return fmt.Errorf("cannot insert key in parent of Key Management directory") } public_filename = path.Join(management.underlying_dir, public_filename) private_filename := public_filename + ".pub" @@ -147,16 +147,20 @@ func (management *KeyManagement) Insert(public string, private string, name stri return err } if n_read < len(public) { - return fmt.Errorf("Write error: did not write whole public key file") + return fmt.Errorf("write error: did not write whole public key file") } if _, err := private_file.WriteString(private); err != nil { return err } if n_read < len(private) { - return fmt.Errorf("Write error: did not write whole private key file") + return fmt.Errorf("write error: did not write whole private key file") + } + if err := public_file.Close(); err != nil { + return err + } + if err := private_file.Close(); err != nil { + return err } - public_file.Close() - private_file.Close() return management.insertInternal(public, private, name) } @@ -172,6 +176,9 @@ func (management *KeyManagement) insertInternal(public string, private string, n } else { private_key, err = ssh.ParsePrivateKeyWithPassphrase([]byte(private), []byte(passphrase)) } + if err != nil { + return err + } management.cached_keys[name] = KeyPair{Public: public_key, Private: private_key} return nil } @@ -192,7 +199,6 @@ func moveFile(source string, dest string) error { if err != nil { return err } - defer source_file.Close() dest_file, err := os.OpenFile(dest, os.O_WRONLY, 0) if err != nil { if os.IsNotExist(err) { @@ -204,19 +210,24 @@ func moveFile(source string, dest string) error { return err } } - defer dest_file.Close() if _, err := io.Copy(dest_file, source_file); err != nil { return err } + if err := source_file.Close(); err != nil { + return err + } + if err := dest_file.Close(); err != nil { + return err + } return os.Remove(source) } func (management *KeyManagement) Revoke(keyname string) error { if _, ok := management.cached_keys[keyname]; !ok { - return fmt.Errorf("Unknown key %s", keyname) + return fmt.Errorf("unknown key %s", keyname) } if _, err := os.ReadDir(path.Join(management.underlying_dir, revoked_directory)); err != nil { if err := os.Mkdir(path.Join(management.underlying_dir, revoked_directory), 0777); err != nil { - return fmt.Errorf("Error while creating directory for revoked keys: %s", err) + return fmt.Errorf("error while creating directory for revoked keys: %s", err) } } now := time.Now() @@ -226,14 +237,14 @@ func (management *KeyManagement) Revoke(keyname string) error { public_target := path.Join(management.underlying_dir, revoked_directory, keyname+".pub"+suffix) log.Infof("Moving %s to %s", public_filename, public_target) if err := moveFile(public_filename, public_target); err != nil { - return fmt.Errorf("Error while moving key: %s", err) + return fmt.Errorf("error while moving key: %s", err) } private_filename := path.Join(management.underlying_dir, keyname) private_target := path.Join(management.underlying_dir, revoked_directory, keyname+suffix) log.Infof("Moving %s to %s", private_filename, private_target) if err := moveFile(private_filename, private_target); err != nil { - return fmt.Errorf("Error while moving key: %s", err) + return fmt.Errorf("error while moving key: %s", err) } delete(management.cached_keys, keyname) diff --git a/pkg/core/capability/ssh/keymanagement_test.go b/pkg/core/capability/ssh/keymanagement_test.go index 6f931445f..89e867580 100644 --- a/pkg/core/capability/ssh/keymanagement_test.go +++ b/pkg/core/capability/ssh/keymanagement_test.go @@ -45,11 +45,9 @@ func addKey(t *testing.T, keyname string) { privkey_path := path.Join(testkey_dir(), "test-key") pubkey_file, err := os.Open(pubkey_path) assert.Nil(t, err) - defer pubkey_file.Close() privkey_file, err := os.Open(privkey_path) assert.Nil(t, err) - defer privkey_file.Close() pubkey_buf := make([]byte, 2048) privkey_buf := make([]byte, 2048) _, err = pubkey_file.Read(pubkey_buf) @@ -57,6 +55,8 @@ func addKey(t *testing.T, keyname string) { _, err = privkey_file.Read(privkey_buf) assert.Nil(t, err) assert.Nil(t, globalKeyManagement.Insert(string(pubkey_buf), string(privkey_buf), keyname)) + assert.Nil(t, privkey_file.Close()) + assert.Nil(t, pubkey_file.Close()) } func TestAddKey(t *testing.T) { @@ -75,8 +75,8 @@ func TestRefresh(t *testing.T) { assert.False(t, slices.Contains(globalKeyManagement.ListAllNames(), testkey+"1")) pubkey_path := path.Join(testkey_dir(), "test-key.pub") privkey_path := path.Join(testkey_dir(), "test-key") - copyFile(pubkey_path, path.Join(testdir, testkey+"1.pub")) - copyFile(privkey_path, path.Join(testdir, testkey+"1")) - globalKeyManagement.Refresh() + assert.Nil(t, copyFile(pubkey_path, path.Join(testdir, testkey+"1.pub"))) + assert.Nil(t, copyFile(privkey_path, path.Join(testdir, testkey+"1"))) + assert.Nil(t, globalKeyManagement.Refresh()) assert.True(t, slices.Contains(globalKeyManagement.ListAllNames(), testkey+"1")) } From 7810776ce9cbf0ed684a8acb16d6c0db0310c381 Mon Sep 17 00:00:00 2001 From: Thijs Heijligenberg Date: Wed, 13 Aug 2025 14:48:44 +0200 Subject: [PATCH 08/18] Removed a public-private mixup --- pkg/core/capability/ssh/keymanagement.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pkg/core/capability/ssh/keymanagement.go b/pkg/core/capability/ssh/keymanagement.go index 31e8d5e86..de6821bad 100644 --- a/pkg/core/capability/ssh/keymanagement.go +++ b/pkg/core/capability/ssh/keymanagement.go @@ -96,11 +96,11 @@ func (management *KeyManagement) Refresh() error { log.Trace("Found public key named ", private_filename) private, err := parsePrivateKey(path.Join(management.underlying_dir, private_filename)) if err != nil { - return fmt.Errorf("private key parsing error: %s", err) + return fmt.Errorf("private key (%s) parsing error: %s", private_filename, err) } public, err := parsePublicKey(path.Join(management.underlying_dir, filename)) if err != nil { - return fmt.Errorf("public key parsing error: %s", err) + return fmt.Errorf("public key (%s) parsing error: %s", filename, err) } management.cached_keys[private_filename] = KeyPair{Public: public, Private: private} } @@ -114,12 +114,12 @@ func (management *KeyManagement) Insert(public string, private string, name stri return fmt.Errorf("inserting key with name %s: already present!", name) } } - public_filename := path.Clean(name) - if strings.HasPrefix(public_filename, "..") { + private_filename := path.Clean(name) + if strings.HasPrefix(private_filename, "..") { return fmt.Errorf("cannot insert key in parent of Key Management directory") } - public_filename = path.Join(management.underlying_dir, public_filename) - private_filename := public_filename + ".pub" + private_filename = path.Join(management.underlying_dir, private_filename) + public_filename := private_filename + ".pub" public_file, err := os.Open(public_filename) if err != nil { if os.IsNotExist(err) { From 8c7a6c95b08a8bd06c1bf2336b5b9e5126da0a13 Mon Sep 17 00:00:00 2001 From: Thijs Heijligenberg Date: Wed, 13 Aug 2025 14:53:10 +0200 Subject: [PATCH 09/18] Removed lint error --- pkg/core/capability/ssh/keymanagement.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/core/capability/ssh/keymanagement.go b/pkg/core/capability/ssh/keymanagement.go index de6821bad..ffb73e343 100644 --- a/pkg/core/capability/ssh/keymanagement.go +++ b/pkg/core/capability/ssh/keymanagement.go @@ -111,7 +111,7 @@ func (management *KeyManagement) Refresh() error { func (management *KeyManagement) Insert(public string, private string, name string) error { for key := range management.cached_keys { if key == name { - return fmt.Errorf("inserting key with name %s: already present!", name) + return fmt.Errorf("inserting key with name %s: already present", name) } } private_filename := path.Clean(name) From cb54807fa98296017dcbe8a052dbab913d9d868a Mon Sep 17 00:00:00 2001 From: Thijs Heijligenberg Date: Mon, 18 Aug 2025 12:08:25 +0200 Subject: [PATCH 10/18] Fixed the logger level configuration The Severity.fromString method did not do anything with the base it was called from. This meant that all logs took the global level instead of the per-package option. --- internal/logger/logger.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/internal/logger/logger.go b/internal/logger/logger.go index 4b4544b94..87963cc21 100644 --- a/internal/logger/logger.go +++ b/internal/logger/logger.go @@ -31,7 +31,7 @@ const ( Trace ) -func (severity Severity) fromString(name string) Severity { +func severityFromString(name string) Severity { nameToLower := strings.ToLower(name) switch nameToLower { case "panic": @@ -106,7 +106,12 @@ func Logger(name string, severity Severity, fileName FileName, format Format) *L setFormat(instance, globalLogFormat) - instance.SetLevel(logrus.Level(severity.fromString(globalLogSeverity))) + globalSeverityLevel := severityFromString(globalLogSeverity) + if globalSeverityLevel > severity { + instance.SetLevel(logrus.Level(globalSeverityLevel)) + } else { + instance.SetLevel(logrus.Level(severity)) + } if globalOperationMode == development { if fileName != "" { From a0cec490c7278be57a85e7b6803aec7623684c68 Mon Sep 17 00:00:00 2001 From: Thijs Heijligenberg Date: Mon, 18 Aug 2025 16:59:19 +0200 Subject: [PATCH 11/18] Changed KMS to use database backend --- cmd/soarca/main.go | 2 +- internal/controller/controller.go | 38 ++- .../database/keymanagement/keymanagement.go | 69 +++++ internal/database/memory/keymanagement.go | 41 +++ .../memory/{memory.go => playbook.go} | 18 +- .../{memory_test.go => playbook_test.go} | 12 +- internal/database/mongodb/mongo.go | 11 +- pkg/api/api.go | 12 +- pkg/api/keymanagement/keymanagement_api.go | 101 +++++-- pkg/api/manual/manual_api.go | 2 +- pkg/core/capability/ssh/keymanagement.go | 252 ------------------ pkg/core/capability/ssh/keymanagement_test.go | 82 ------ pkg/core/capability/ssh/ssh.go | 30 +-- pkg/keymanagement/keymanagement.go | 122 +++++++++ pkg/keymanagement/keymanagement_test.go | 53 ++++ pkg/models/api/keymanagement.go | 5 +- pkg/models/keymanagement/keymanagent.go | 8 + 17 files changed, 443 insertions(+), 415 deletions(-) create mode 100644 internal/database/keymanagement/keymanagement.go create mode 100644 internal/database/memory/keymanagement.go rename internal/database/memory/{memory.go => playbook.go} (70%) rename internal/database/memory/{memory_test.go => playbook_test.go} (96%) delete mode 100644 pkg/core/capability/ssh/keymanagement.go delete mode 100644 pkg/core/capability/ssh/keymanagement_test.go create mode 100644 pkg/keymanagement/keymanagement.go create mode 100644 pkg/keymanagement/keymanagement_test.go create mode 100644 pkg/models/keymanagement/keymanagent.go diff --git a/cmd/soarca/main.go b/cmd/soarca/main.go index fbb248131..61dfa759a 100644 --- a/cmd/soarca/main.go +++ b/cmd/soarca/main.go @@ -45,7 +45,7 @@ func main() { err := godotenv.Load(".env") if err != nil { - log.Warning("Failed to read env variable, but will continue") + log.Warning("Failed to read env variable, but will continue. Error: ", err) } Host = "localhost:" + utils.GetEnv("PORT", "8080") api.SwaggerInfo.Host = Host diff --git a/internal/controller/controller.go b/internal/controller/controller.go index 1c4ff7942..ea8a0c2f7 100644 --- a/internal/controller/controller.go +++ b/internal/controller/controller.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "reflect" + keymanagementrepository "soarca/internal/database/keymanagement" "soarca/internal/database/memory" "soarca/internal/logger" @@ -20,6 +21,7 @@ import ( "soarca/pkg/core/executors/action" "soarca/pkg/core/executors/condition" "soarca/pkg/core/executors/playbook_action" + "soarca/pkg/keymanagement" "soarca/pkg/reporting/cases" "soarca/pkg/reporting/reporter" "soarca/pkg/utils" @@ -60,8 +62,10 @@ func init() { } type Controller struct { - finController finChannelController.IFinController - playbookRepo playbookrepository.IPlaybookRepository + finController finChannelController.IFinController + playbookRepo playbookrepository.IPlaybookRepository + keyManagementRepo keymanagementrepository.IKeyManagementRepository + keyManagement *keymanagement.KeyManagement } var mainController = Controller{} @@ -74,7 +78,7 @@ const defaultCacheSize int = 10 var mainInteraction = interaction.New(registerManualIntegration()) func (controller *Controller) NewDecomposer() decomposer.IDecomposer { - ssh := new(ssh.SshCapability) + ssh := &ssh.SshCapability{Keys: controller.keyManagement} capabilities := map[string]capability.ICapability{ssh.GetType(): ssh} skip, _ := strconv.ParseBool(utils.GetEnv("HTTP_SKIP_CERT_VALIDATION", "false")) @@ -141,6 +145,7 @@ func (controller *Controller) setupDatabase() error { initMongoDatabase, _ := strconv.ParseBool(utils.GetEnv("DATABASE", "false")) if initMongoDatabase { + log.Info("Setting up mongo database") mongo.LoadComponent() @@ -158,14 +163,22 @@ func (controller *Controller) setupDatabase() error { return err } controller.playbookRepo = playbookrepository.SetupPlaybookRepository(mongo.GetCacaoRepo(), mongo.DefaultLimitOpts()) + controller.keyManagementRepo = keymanagementrepository.SetupKeyManagementRepository(mongo.GetKeyManagementRepo(), mongo.DefaultLimitOpts()) } else { // Use in memory database - controller.playbookRepo = memory.New() + log.Info("Setting up in-memory database") + controller.playbookRepo = memory.NewPlaybookDatabase() + controller.keyManagementRepo = memory.NewKeyManagementDatabase() } return nil } +func (controller *Controller) setupKeyManagement() error { + controller.keyManagement = keymanagement.InitKeyManagement(controller.keyManagementRepo) + return nil +} + func (controller *Controller) GetDatabaseInstance() playbookrepository.IPlaybookRepository { return controller.playbookRepo } @@ -249,6 +262,12 @@ func initializeCore(app *gin.Engine) error { return err } + err = mainController.setupKeyManagement() + if err != nil { + log.Error("Failed to setup key management:", err) + return err + } + err = routes.Api(app, &mainController, &mainController) if err != nil { log.Error(err) @@ -269,16 +288,7 @@ func initializeCore(app *gin.Engine) error { return err } - if utils.GetEnv("ENABLE_SSH_KMS", "false") == "true" { - kms_dir := utils.GetEnv("SSH_KMS_DIR", ".ssh/") - log.Trace("Setting up key management in directory ", kms_dir) - key_manager, err := ssh.InitKeyManagement(kms_dir) - if err != nil { - log.Error("Failed to set up key management: ", err) - return err - } - routes.KeyManagement(app, key_manager) - } + routes.KeyManagement(app, mainController.keyManagement) // Manual capability native routes routes.Manual(app, mainInteraction) diff --git a/internal/database/keymanagement/keymanagement.go b/internal/database/keymanagement/keymanagement.go new file mode 100644 index 000000000..a3f0be88c --- /dev/null +++ b/internal/database/keymanagement/keymanagement.go @@ -0,0 +1,69 @@ +package keymanagementrepository + +import ( + "errors" + "soarca/internal/database" + "soarca/pkg/models/keymanagement" +) + +type IKeyManagementRepository interface { + GetKeyNames() ([]string, error) + Create(name string, keypair keymanagement.KeyPair) error + Read(id string) (keymanagement.KeyPair, error) + Update(id string, keypair keymanagement.KeyPair) error + Delete(id string) error +} + +type KeyManagementRepository struct { + db database.Database + options database.FindOptions +} + +type KeyPairEntry struct { + name string + keypair keymanagement.KeyPair +} + +func SetupKeyManagementRepository(db database.Database, options database.FindOptions) *KeyManagementRepository { + return &KeyManagementRepository{db: db, options: options} +} + +func (keymanagementRepo *KeyManagementRepository) GetKeyNames() ([]string, error) { + keys, err := keymanagementRepo.db.Find(nil) + if err != nil { + return nil, err + } + ret := []string{} + for _, key := range keys { + ret = append(ret, key.(KeyPairEntry).name) + } + return ret, nil +} + +func (keymanagementRepo *KeyManagementRepository) Create(name string, keypair keymanagement.KeyPair) error { + return keymanagementRepo.db.Create(KeyPairEntry{name, keypair}) +} + +func (keymanagementRepo *KeyManagementRepository) Read(id string) (keymanagement.KeyPair, error) { + returnedObject, err := keymanagementRepo.db.Read(id) + if err != nil { + return keymanagement.KeyPair{}, err + } + + keypair, ok := returnedObject.(keymanagement.KeyPair) + + if !ok { + err = errors.New("could not cast lookup object to keypair type") + return keymanagement.KeyPair{}, err + } + + return keypair, nil +} + +func (keymanagementRepo *KeyManagementRepository) Update(id string, keypair keymanagement.KeyPair) error { + return keymanagementRepo.db.Update(id, keypair) +} + +func (keymanagementRepo *KeyManagementRepository) Delete(id string) error { + return keymanagementRepo.db.Delete(id) +} diff --git a/internal/database/memory/keymanagement.go b/internal/database/memory/keymanagement.go new file mode 100644 index 000000000..fb89cc458 --- /dev/null +++ b/internal/database/memory/keymanagement.go @@ -0,0 +1,41 @@ +package memory + +import ( + "fmt" + "soarca/pkg/models/keymanagement" +) + +type InMemoryKeyManagementDatabase struct { + keys map[string]keymanagement.KeyPair +} + +func NewKeyManagementDatabase() *InMemoryKeyManagementDatabase { + return &InMemoryKeyManagementDatabase{keys: make(map[string]keymanagement.KeyPair)} +} + +func (database *InMemoryKeyManagementDatabase) GetKeyNames() ([]string, error) { + ret := []string{} + for key := range database.keys { + ret = append(ret, key) + } + return ret, nil +} +func (database *InMemoryKeyManagementDatabase) Create(name string, keypair keymanagement.KeyPair) error { + database.keys[name] = keypair + return nil +} +func (database *InMemoryKeyManagementDatabase) Read(id string) (keymanagement.KeyPair, error) { + keypair, ok := database.keys[id] + if !ok { + return keymanagement.KeyPair{}, fmt.Errorf("Could not find key named %s", id) + } + return keypair, nil +} +func (database *InMemoryKeyManagementDatabase) Update(id string, keypair keymanagement.KeyPair) error { + database.keys[id] = keypair + return nil +} +func (database *InMemoryKeyManagementDatabase) Delete(id string) error { + delete(database.keys, id) + return nil +} diff --git a/internal/database/memory/memory.go b/internal/database/memory/playbook.go similarity index 70% rename from internal/database/memory/memory.go rename to internal/database/memory/playbook.go index ef0bddc8e..ad94591fd 100644 --- a/internal/database/memory/memory.go +++ b/internal/database/memory/playbook.go @@ -7,15 +7,15 @@ import ( "soarca/pkg/models/decoder" ) -type InMemoryDatabase struct { +type InMemoryPlaybookDatabase struct { playbooks map[string]cacao.Playbook } -func New() *InMemoryDatabase { - return &InMemoryDatabase{playbooks: make(map[string]cacao.Playbook)} +func NewPlaybookDatabase() *InMemoryPlaybookDatabase { + return &InMemoryPlaybookDatabase{playbooks: make(map[string]cacao.Playbook)} } -func (memory *InMemoryDatabase) GetPlaybooks() ([]cacao.Playbook, error) { +func (memory *InMemoryPlaybookDatabase) GetPlaybooks() ([]cacao.Playbook, error) { size := len(memory.playbooks) playbookList := make([]cacao.Playbook, 0, size) for _, playbook := range memory.playbooks { @@ -25,7 +25,7 @@ func (memory *InMemoryDatabase) GetPlaybooks() ([]cacao.Playbook, error) { return playbookList, nil } -func (memory *InMemoryDatabase) GetPlaybookMetas() ([]api.PlaybookMeta, error) { +func (memory *InMemoryPlaybookDatabase) GetPlaybookMetas() ([]api.PlaybookMeta, error) { size := len(memory.playbooks) playbookList := make([]api.PlaybookMeta, 0, size) for _, playbook := range memory.playbooks { @@ -40,7 +40,7 @@ func (memory *InMemoryDatabase) GetPlaybookMetas() ([]api.PlaybookMeta, error) { return playbookList, nil } -func (memory *InMemoryDatabase) Create(json *[]byte) (cacao.Playbook, error) { +func (memory *InMemoryPlaybookDatabase) Create(json *[]byte) (cacao.Playbook, error) { if json == nil { return cacao.Playbook{}, errors.New("empty input") @@ -57,7 +57,7 @@ func (memory *InMemoryDatabase) Create(json *[]byte) (cacao.Playbook, error) { return memory.playbooks[result.ID], nil } -func (memory *InMemoryDatabase) Read(id string) (cacao.Playbook, error) { +func (memory *InMemoryPlaybookDatabase) Read(id string) (cacao.Playbook, error) { playbook, ok := memory.playbooks[id] if !ok { return cacao.Playbook{}, errors.New("playbook is not in repository") @@ -65,7 +65,7 @@ func (memory *InMemoryDatabase) Read(id string) (cacao.Playbook, error) { return playbook, nil } -func (memory *InMemoryDatabase) Update(id string, json *[]byte) (cacao.Playbook, error) { +func (memory *InMemoryPlaybookDatabase) Update(id string, json *[]byte) (cacao.Playbook, error) { playbook, err := memory.Read(id) if err != nil { return playbook, err @@ -78,7 +78,7 @@ func (memory *InMemoryDatabase) Update(id string, json *[]byte) (cacao.Playbook, return *updatePlaybook, nil } -func (memory *InMemoryDatabase) Delete(id string) error { +func (memory *InMemoryPlaybookDatabase) Delete(id string) error { delete(memory.playbooks, id) return nil } diff --git a/internal/database/memory/memory_test.go b/internal/database/memory/playbook_test.go similarity index 96% rename from internal/database/memory/memory_test.go rename to internal/database/memory/playbook_test.go index d206aae52..a7a5408b6 100644 --- a/internal/database/memory/memory_test.go +++ b/internal/database/memory/playbook_test.go @@ -32,7 +32,7 @@ func TestCreate(t *testing.T) { } var workflow = decoder.DecodeValidate(byteValue) - mem := New() + mem := NewPlaybookDatabase() playbook, err := mem.Create(&byteValue) assert.Equal(t, err, nil) assert.Equal(t, playbook, workflow) @@ -56,7 +56,7 @@ func TestRead(t *testing.T) { var workflow = decoder.DecodeValidate(byteValue) - mem := New() + mem := NewPlaybookDatabase() empty, err := mem.Read(workflow.ID) assert.Equal(t, err, errors.New("playbook is not in repository")) assert.Equal(t, empty, cacao.Playbook{}) @@ -87,7 +87,7 @@ func TestUpdate(t *testing.T) { var workflow = decoder.DecodeValidate(byteValue) - mem := New() + mem := NewPlaybookDatabase() empty, err := mem.Update(workflow.ID, nil) assert.Equal(t, err, errors.New("playbook is not in repository")) assert.Equal(t, empty, cacao.Playbook{}) @@ -129,7 +129,7 @@ func TestDelete(t *testing.T) { var workflow = decoder.DecodeValidate(byteValue) - mem := New() + mem := NewPlaybookDatabase() err = mem.Delete(workflow.ID) assert.Equal(t, err, nil) @@ -166,7 +166,7 @@ func TestGetAllPlaybooks(t *testing.T) { var workflow = decoder.DecodeValidate(byteValue) - mem := New() + mem := NewPlaybookDatabase() list := []string{ "playbook--f47d4081-21ed-4f21-9d05-6b368d73da30", @@ -214,7 +214,7 @@ func TestGetAllPlaybookMetas(t *testing.T) { var workflow = decoder.DecodeValidate(byteValue) - mem := New() + mem := NewPlaybookDatabase() list := []string{ "playbook--f47d4081-21ed-4f21-9d05-6b368d73da30", diff --git a/internal/database/mongodb/mongo.go b/internal/database/mongodb/mongo.go index 15964997f..5a307f432 100644 --- a/internal/database/mongodb/mongo.go +++ b/internal/database/mongodb/mongo.go @@ -6,6 +6,7 @@ import ( "reflect" "time" + keymanagementrepository "soarca/internal/database/keymanagement" "soarca/internal/database/projections" cacao "soarca/pkg/models/cacao" @@ -18,11 +19,12 @@ const writeErrorDuplicationCode = 11000 var ( cacaoPlayBookRepo *mongoCollection[cacao.Playbook] + keyManagementRepo *mongoCollection[keymanagementrepository.KeyPairEntry] mongoclient *mongo.Client ) type dbtypes interface { - cacao.Playbook // | for other supported types + cacao.Playbook | keymanagementrepository.KeyPairEntry // | for other supported types } type mongoCollection[T dbtypes] struct { @@ -57,6 +59,9 @@ func (mongoOpts mongoFindOptions) GetProjectionByType(interface{}) interface{} { func GetCacaoRepo() *mongoCollection[cacao.Playbook] { return cacaoPlayBookRepo } +func GetKeyManagementRepo() *mongoCollection[keymanagementrepository.KeyPairEntry] { + return keyManagementRepo +} // func GetMongoClient() *mongodbClient { // return mongoclient @@ -77,6 +82,7 @@ func SetupMongodb(uri string, username string, password string) error { } cacaoPlayBookRepo, err = NewMongoCollection[cacao.Playbook](mongoclient, "soarca", "cacoa_playbook_collection") + keyManagementRepo, err = NewMongoCollection[keymanagementrepository.KeyPairEntry](mongoclient, "keymanagement", "keymanagement_collection") return err } @@ -220,7 +226,7 @@ func NewMongoCollection[T dbtypes](mongo *mongo.Client, dbName string, colName s } func InitMongoClient(mongo_uri string, username string, password string) error { - log.Trace("Trying to setup new MongoClient") + log.Trace("Trying to setup new MongoClient at uri ", mongo_uri) var err error if mongo_uri == "" { log.Error("mongo uri not valid, because empty") @@ -232,6 +238,7 @@ func InitMongoClient(mongo_uri string, username string, password string) error { return errors.New("username or password not correctly set") } + log.Trace("Logging in to mongo with username ", username, " and password ", password) clientOpts := options.Client().ApplyURI(mongo_uri).SetAuth(options.Credential{ Username: username, Password: password, diff --git a/pkg/api/api.go b/pkg/api/api.go index aabd95528..042346f3e 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -7,12 +7,12 @@ import ( "soarca/internal/controller/decomposer_controller" "soarca/internal/controller/informer" "soarca/internal/logger" - "soarca/pkg/api/keymanagement" + keymanagement_handler "soarca/pkg/api/keymanagement" playbook_handler "soarca/pkg/api/playbook" reporter_handler "soarca/pkg/api/reporter" status_handler "soarca/pkg/api/status" "soarca/pkg/core/capability/manual/interaction" - "soarca/pkg/core/capability/ssh" + "soarca/pkg/keymanagement" manual_handler "soarca/pkg/api/manual" @@ -55,9 +55,9 @@ func Manual(app *gin.Engine, interaction interaction.IInteractionStorage) { manualHandler := manual_handler.NewManualHandler(interaction) ManualRoutes(app, manualHandler) } -func KeyManagement(app *gin.Engine, key_manager *ssh.KeyManagement) { +func KeyManagement(app *gin.Engine, key_manager *keymanagement.KeyManagement) { log.Trace("Setting up key management routes") - keyManagement := keymanagement.NewKeyManagementHandler(key_manager) + keyManagement := keymanagement_handler.NewKeyManagementHandler(key_manager) KeyManagementRoutes(app, keyManagement) } @@ -152,12 +152,12 @@ func ManualRoutes(route *gin.Engine, manualHandler *manual_handler.ManualHandler } } -func KeyManagementRoutes(route *gin.Engine, keyManagementHandler *keymanagement.KeyManagementHandler) { +func KeyManagementRoutes(route *gin.Engine, keyManagementHandler *keymanagement_handler.KeyManagementHandler) { keyManagementRoutes := route.Group("/keymanagement") { keyManagementRoutes.GET("/", keyManagementHandler.GetKeys) keyManagementRoutes.PUT("/:keyname", keyManagementHandler.AddKey) - keyManagementRoutes.POST("/refresh/", keyManagementHandler.Refresh) + keyManagementRoutes.PATCH("/:keyname", keyManagementHandler.UpdateKey) keyManagementRoutes.DELETE("/:keyname", keyManagementHandler.RevokeKey) } } diff --git a/pkg/api/keymanagement/keymanagement_api.go b/pkg/api/keymanagement/keymanagement_api.go index 0e0b04a9c..16341bb9e 100644 --- a/pkg/api/keymanagement/keymanagement_api.go +++ b/pkg/api/keymanagement/keymanagement_api.go @@ -1,4 +1,4 @@ -package keymanagement +package keymanagement_api import ( "encoding/json" @@ -7,7 +7,7 @@ import ( "net/http" "reflect" "soarca/internal/logger" - "soarca/pkg/core/capability/ssh" + "soarca/pkg/keymanagement" "soarca/pkg/models/api" "strconv" @@ -19,68 +19,123 @@ var log *logger.Log type Empty struct{} func init() { - log = logger.Logger(reflect.TypeOf(Empty{}).PkgPath(), logger.Info, "", logger.Json) + log = logger.Logger(reflect.TypeOf(Empty{}).PkgPath(), logger.Trace, "", logger.Json) } type KeyManagementHandler struct { - Manager *ssh.KeyManagement + Manager *keymanagement.KeyManagement } -func NewKeyManagementHandler(manager *ssh.KeyManagement) *KeyManagementHandler { +func NewKeyManagementHandler(manager *keymanagement.KeyManagement) *KeyManagementHandler { return &KeyManagementHandler{ Manager: manager, } } +// GetKeys GET handler for obtaining all keys in the key managements system +// +// @Summary gets all keys from the KMS +// @Schemes +// @Description return all keys in the KMS +// @Tags keymanagement +// @Produce json +// @success 200 {array} string +// @failure 400 {object} api.Error +// @Router /keymanagement/ [GET] func (handler *KeyManagementHandler) GetKeys(context *gin.Context) { keyInfo := handler.Manager.ListAllNames() + log.Trace("Listing all key names") context.JSON(http.StatusOK, keyInfo) } -func (handler *KeyManagementHandler) Refresh(context *gin.Context) { - err := handler.Manager.Refresh() +// AddKey PUT handler to add key to KMS +// +// @Summary add key to KMS +// @Schemes +// @Description adds a key to the KMS; load key into cache and write to database +// @Tags keymanagement +// @Param data body api.KeyManagementKey true "key" +// @Produce json +// @success 200 {json} Empty +// @failure 400 {object} api.Error +// @Router /keymanagement/:keyname/ [PUT] +func (handler *KeyManagementHandler) AddKey(context *gin.Context) { + keyname := context.Param("keyname") + jsonData, err := io.ReadAll(context.Request.Body) if err != nil { - log.Trace("Refreshing keys has failed: ", err.Error()) - SendErrorResponse(context, http.StatusBadRequest, "Failed to refresh keys", "POST /keymanagement/refresh") + log.Trace("Submit key has failed: ", err.Error()) + SendErrorResponse(context, http.StatusBadRequest, "Failed to read json on server side", "PUT /keymanagement/:keyname") + return + } + var key api.KeyManagementKey + if err := json.Unmarshal(jsonData, &key); err != nil { + log.Trace("Submit key failed to unmarshal: ", err.Error()) + SendErrorResponse(context, http.StatusBadRequest, "Failed to marshall json on server side", "PUT /keymanagement/:keyname") return } + if err := handler.Manager.Insert(key.Public, key.Private, key.Passphrase, keyname); err != nil { + log.Trace("Submit key failed to insert: ", err.Error()) + SendErrorResponse(context, http.StatusBadRequest, "Failed to insert key on server side", "PUT /keymanagement/:keyname") + return + + } + log.Trace("Inserted key ", keyname) context.JSON(http.StatusOK, Empty{}) } -func (handler *KeyManagementHandler) AddKey(context *gin.Context) { +// UpdateKey PATCH handler to update key in KMS +// +// @Summary update key in KMS +// @Schemes +// @Description update a key in the KMS; load key into cache and write to database +// @Tags keymanagement +// @Param data body api.KeyManagementKey true "key" +// @Produce json +// @success 200 {json} Empty +// @failure 400 {object} api.Error +// @Router /keymanagement/:keyname/ [PATCH] +func (handler *KeyManagementHandler) UpdateKey(context *gin.Context) { keyname := context.Param("keyname") jsonData, err := io.ReadAll(context.Request.Body) if err != nil { - log.Trace("Submit key has failed: ", err.Error()) - SendErrorResponse(context, http.StatusBadRequest, "Failed to read json on server side", "POST /keymanagement") + log.Trace("Update key has failed: ", err.Error()) + SendErrorResponse(context, http.StatusBadRequest, "Failed to read json on server side", "PATCH /keymanagement/:keyname") return } var key api.KeyManagementKey if err := json.Unmarshal(jsonData, &key); err != nil { - log.Trace("Submit key failed to unmarshal: ", err.Error()) - SendErrorResponse(context, http.StatusBadRequest, "Failed to marshall json on server side", "POST /keymanagement") + log.Trace("Update key failed to unmarshal: ", err.Error()) + SendErrorResponse(context, http.StatusBadRequest, "Failed to marshall json on server side", "PATCH /keymanagement/:keyname") return } - if err := handler.Manager.Insert(key.Public, key.Private, keyname); err != nil { - log.Trace("Submit key failed to insert: ", err.Error()) - SendErrorResponse(context, http.StatusBadRequest, "Failed to insert key on server side", "POST /keymanagement") + if err := handler.Manager.Update(key.Public, key.Private, key.Passphrase, keyname); err != nil { + log.Trace("Update key failed to insert: ", err.Error()) + SendErrorResponse(context, http.StatusBadRequest, "Failed to update key on server side", "PATCH /keymanagement/:keyname") return } + log.Trace("Updated key ", keyname) context.JSON(http.StatusOK, Empty{}) } +// RevokeKey DELETE handler to remove key from KMS +// +// @Summary remove key from KMS +// @Schemes +// @Description revokes the key by moving it to .revoked and renaming it +// @Tags keymanagement +// @Produce json +// @success 200 {json} Empty +// @failure 400 {object} api.Error +// @Router /keymanagement/:keyname/ [DELETE] func (handler *KeyManagementHandler) RevokeKey(context *gin.Context) { keyname := context.Param("keyname") - if err := handler.Manager.Revoke(keyname); err != nil { - log.Trace("Revoke key failed: ", err.Error()) - SendErrorResponse(context, http.StatusBadRequest, "Failed to revoke key", "DELETE /keymanagement") - return - } + handler.Manager.Revoke(keyname) context.JSON(http.StatusOK, gin.H{ "status": 200, - "message": fmt.Sprintf("Successfuly removed key %s from SOARCA listing. To permanently remove key, delete the revoked key files in key management directory", keyname), + "message": fmt.Sprintf("Removed key %s from SOARCA listing", keyname), }) + log.Trace("Removed key ", keyname) } func SendErrorResponse(context *gin.Context, status int, message string, orginal_call string) { diff --git a/pkg/api/manual/manual_api.go b/pkg/api/manual/manual_api.go index 74ea74d21..ff4bb918a 100644 --- a/pkg/api/manual/manual_api.go +++ b/pkg/api/manual/manual_api.go @@ -58,7 +58,7 @@ func NewManualHandler(interaction interaction.IInteractionStorage) *ManualHandle // @Tags manual // @Accept json // @Produce json -// @Success 200 {object} api.Execution +// @Success 200 {object} []api.InteractionCommandData // @failure 400 {object} []api.InteractionCommandData // @Router /manual/ [GET] func (manualHandler *ManualHandler) GetPendingCommands(g *gin.Context) { diff --git a/pkg/core/capability/ssh/keymanagement.go b/pkg/core/capability/ssh/keymanagement.go deleted file mode 100644 index ffb73e343..000000000 --- a/pkg/core/capability/ssh/keymanagement.go +++ /dev/null @@ -1,252 +0,0 @@ -package ssh - -import ( - "fmt" - "io" - "os" - "path" - "slices" - "soarca/pkg/utils" - "strings" - "time" - - "golang.org/x/crypto/ssh" -) - -type KeyPair struct { - Public ssh.PublicKey - Private ssh.Signer -} -type KeyManagement struct { - underlying_dir string - cached_keys map[string]KeyPair -} - -var globalKeyManagement *KeyManagement - -func InitKeyManagement(underlying_dir string) (*KeyManagement, error) { - globalKeyManagement = &KeyManagement{ - underlying_dir: underlying_dir, - } - err := globalKeyManagement.Refresh() - return globalKeyManagement, err -} - -func (management *KeyManagement) GetKeyPair(name string) (*KeyPair, error) { - keypair, ok := management.cached_keys[name] - if !ok { - return nil, fmt.Errorf("could not find keypair for %s", name) - } - return &keypair, nil -} -func (management *KeyManagement) GetPrivate(name string) (ssh.Signer, error) { - keypair, err := management.GetKeyPair(name) - if err != nil { - return nil, err - } - return keypair.Private, nil -} -func parsePrivateKey(filename string) (ssh.Signer, error) { - log.Tracef("Opening private key from path %s", filename) - file, err := os.Open(filename) - if err != nil { - return nil, err - } - passphrase := utils.GetEnv("SSH_KMS_PASSPHRASE", "") - file_buffer := make([]byte, 2048) - if _, err := file.Read(file_buffer); err != nil { - return nil, err - } - if passphrase == "" { - return ssh.ParsePrivateKey(file_buffer) - } else { - return ssh.ParsePrivateKeyWithPassphrase(file_buffer, []byte(passphrase)) - } -} -func parsePublicKey(filename string) (ssh.PublicKey, error) { - log.Tracef("Opening public key from path %s", filename) - file, err := os.Open(filename) - if err != nil { - return nil, err - } - file_buffer := make([]byte, 2048) - if _, err = file.Read(file_buffer); err != nil { - return nil, err - } - key, _, _, _, err := ssh.ParseAuthorizedKey(file_buffer) - return key, err - -} -func (management *KeyManagement) Refresh() error { - management.cached_keys = make(map[string]KeyPair) - dir, err := os.Open(management.underlying_dir) - if err != nil { - return err - } - filenames, err := dir.Readdirnames(0) - if err != nil { - return err - } - for _, filename := range filenames { - if strings.HasSuffix(filename, ".pub") { - private_filename := strings.TrimSuffix(filename, ".pub") - if !slices.Contains(filenames, private_filename) { - return fmt.Errorf("found public key %s without corresponding private key (%s)", filename, private_filename) - } - log.Trace("Found public key named ", private_filename) - private, err := parsePrivateKey(path.Join(management.underlying_dir, private_filename)) - if err != nil { - return fmt.Errorf("private key (%s) parsing error: %s", private_filename, err) - } - public, err := parsePublicKey(path.Join(management.underlying_dir, filename)) - if err != nil { - return fmt.Errorf("public key (%s) parsing error: %s", filename, err) - } - management.cached_keys[private_filename] = KeyPair{Public: public, Private: private} - } - } - return nil -} - -func (management *KeyManagement) Insert(public string, private string, name string) error { - for key := range management.cached_keys { - if key == name { - return fmt.Errorf("inserting key with name %s: already present", name) - } - } - private_filename := path.Clean(name) - if strings.HasPrefix(private_filename, "..") { - return fmt.Errorf("cannot insert key in parent of Key Management directory") - } - private_filename = path.Join(management.underlying_dir, private_filename) - public_filename := private_filename + ".pub" - public_file, err := os.Open(public_filename) - if err != nil { - if os.IsNotExist(err) { - public_file, err = os.Create(public_filename) - if err != nil { - return err - } - } else { - return err - } - } - private_file, err := os.Open(private_filename) - if err != nil { - if os.IsNotExist(err) { - private_file, err = os.Create(private_filename) - if err != nil { - return err - } - } else { - return err - } - } - n_read, err := public_file.WriteString(public) - if err != nil { - return err - } - if n_read < len(public) { - return fmt.Errorf("write error: did not write whole public key file") - } - if _, err := private_file.WriteString(private); err != nil { - return err - } - if n_read < len(private) { - return fmt.Errorf("write error: did not write whole private key file") - } - if err := public_file.Close(); err != nil { - return err - } - if err := private_file.Close(); err != nil { - return err - } - return management.insertInternal(public, private, name) -} - -func (management *KeyManagement) insertInternal(public string, private string, name string) error { - public_key, _, _, _, err := ssh.ParseAuthorizedKey([]byte(public)) - if err != nil { - return err - } - passphrase := utils.GetEnv("SSH_KMS_PASSPHRASE", "") - var private_key ssh.Signer - if passphrase == "" { - private_key, err = ssh.ParsePrivateKey([]byte(private)) - } else { - private_key, err = ssh.ParsePrivateKeyWithPassphrase([]byte(private), []byte(passphrase)) - } - if err != nil { - return err - } - management.cached_keys[name] = KeyPair{Public: public_key, Private: private_key} - return nil -} -func (management *KeyManagement) ListAllNames() []string { - keys := make([]string, len(management.cached_keys)) - index := 0 - for key := range management.cached_keys { - keys[index] = key - index++ - } - return keys -} - -const revoked_directory string = ".revoked" - -func moveFile(source string, dest string) error { - source_file, err := os.Open(source) - if err != nil { - return err - } - dest_file, err := os.OpenFile(dest, os.O_WRONLY, 0) - if err != nil { - if os.IsNotExist(err) { - dest_file, err = os.Create(dest) - if err != nil { - return err - } - } else { - return err - } - } - if _, err := io.Copy(dest_file, source_file); err != nil { - return err - } - if err := source_file.Close(); err != nil { - return err - } - if err := dest_file.Close(); err != nil { - return err - } - return os.Remove(source) -} -func (management *KeyManagement) Revoke(keyname string) error { - if _, ok := management.cached_keys[keyname]; !ok { - return fmt.Errorf("unknown key %s", keyname) - } - if _, err := os.ReadDir(path.Join(management.underlying_dir, revoked_directory)); err != nil { - if err := os.Mkdir(path.Join(management.underlying_dir, revoked_directory), 0777); err != nil { - return fmt.Errorf("error while creating directory for revoked keys: %s", err) - } - } - now := time.Now() - suffix := fmt.Sprintf(".revoked_%d-%d_%d-%s-%d", now.Second(), now.Hour(), now.Day(), now.Month().String(), now.Year()) - - public_filename := path.Join(management.underlying_dir, keyname+".pub") - public_target := path.Join(management.underlying_dir, revoked_directory, keyname+".pub"+suffix) - log.Infof("Moving %s to %s", public_filename, public_target) - if err := moveFile(public_filename, public_target); err != nil { - return fmt.Errorf("error while moving key: %s", err) - } - - private_filename := path.Join(management.underlying_dir, keyname) - private_target := path.Join(management.underlying_dir, revoked_directory, keyname+suffix) - log.Infof("Moving %s to %s", private_filename, private_target) - if err := moveFile(private_filename, private_target); err != nil { - return fmt.Errorf("error while moving key: %s", err) - } - - delete(management.cached_keys, keyname) - return nil -} diff --git a/pkg/core/capability/ssh/keymanagement_test.go b/pkg/core/capability/ssh/keymanagement_test.go deleted file mode 100644 index 89e867580..000000000 --- a/pkg/core/capability/ssh/keymanagement_test.go +++ /dev/null @@ -1,82 +0,0 @@ -package ssh - -import ( - "os" - "path" - "slices" - "testing" - - "github.com/stretchr/testify/assert" -) - -const testdir string = ".ssh_test" -const testkey string = "test-key" - -func testkey_dir() string { - return path.Join("..", "..", "..", "..", "test", "unittest", "mocks", "mock_utils") -} - -func init() { - err := os.Mkdir(testdir, 0777) - if err != nil { - if err.(*os.PathError).Err.Error() == "file exists" { - if err := os.RemoveAll(testdir); err != nil { - panic(err) - } - if err := os.Mkdir(testdir, 0777); err != nil { - panic(err) - } - } else { - panic(err) - } - } - if _, err = InitKeyManagement(testdir); err != nil { - panic(err) - } -} - -func TestRevoke(t *testing.T) { - addKey(t, testkey) - assert.True(t, slices.Contains(globalKeyManagement.ListAllNames(), testkey)) - assert.Nil(t, globalKeyManagement.Revoke(testkey)) -} -func addKey(t *testing.T, keyname string) { - pubkey_path := path.Join(testkey_dir(), "test-key.pub") - privkey_path := path.Join(testkey_dir(), "test-key") - pubkey_file, err := os.Open(pubkey_path) - assert.Nil(t, err) - - privkey_file, err := os.Open(privkey_path) - assert.Nil(t, err) - pubkey_buf := make([]byte, 2048) - privkey_buf := make([]byte, 2048) - _, err = pubkey_file.Read(pubkey_buf) - assert.Nil(t, err) - _, err = privkey_file.Read(privkey_buf) - assert.Nil(t, err) - assert.Nil(t, globalKeyManagement.Insert(string(pubkey_buf), string(privkey_buf), keyname)) - assert.Nil(t, privkey_file.Close()) - assert.Nil(t, pubkey_file.Close()) -} - -func TestAddKey(t *testing.T) { - addKey(t, testkey) - assert.True(t, slices.Equal(globalKeyManagement.ListAllNames(), []string{testkey})) -} - -func copyFile(src string, dst string) error { - data, err := os.ReadFile(src) - if err != nil { - return err - } - return os.WriteFile(dst, data, 0666) -} -func TestRefresh(t *testing.T) { - assert.False(t, slices.Contains(globalKeyManagement.ListAllNames(), testkey+"1")) - pubkey_path := path.Join(testkey_dir(), "test-key.pub") - privkey_path := path.Join(testkey_dir(), "test-key") - assert.Nil(t, copyFile(pubkey_path, path.Join(testdir, testkey+"1.pub"))) - assert.Nil(t, copyFile(privkey_path, path.Join(testdir, testkey+"1"))) - assert.Nil(t, globalKeyManagement.Refresh()) - assert.True(t, slices.Contains(globalKeyManagement.ListAllNames(), testkey+"1")) -} diff --git a/pkg/core/capability/ssh/ssh.go b/pkg/core/capability/ssh/ssh.go index 9f2068eb0..a64ae2854 100644 --- a/pkg/core/capability/ssh/ssh.go +++ b/pkg/core/capability/ssh/ssh.go @@ -5,6 +5,7 @@ import ( "fmt" "reflect" "soarca/pkg/core/capability" + "soarca/pkg/keymanagement" "soarca/pkg/models/cacao" "soarca/pkg/models/execution" "strings" @@ -21,6 +22,7 @@ const ( ) type SshCapability struct { + Keys *keymanagement.KeyManagement } var component = reflect.TypeOf(SshCapability{}).PkgPath() @@ -38,30 +40,24 @@ func (sshCapability *SshCapability) Execute(metadata execution.Metadata, context capability.Context) (cacao.Variables, error) { log.Trace(metadata.ExecutionId) - return execute(context.Command, context.Authentication, context.Target) -} - -func execute(command cacao.Command, - authentication cacao.AuthenticationInformation, - target cacao.AgentTarget) (cacao.Variables, error) { - err := CheckSshAuthenticationInfo(authentication) + err := CheckSshAuthenticationInfo(context.Authentication) if err != nil { log.Error(err) return cacao.NewVariables(), err } - config, err := getConfig(authentication) + config, err := sshCapability.getConfig(context.Authentication) if err != nil { return cacao.NewVariables(), err } - session, client, err := getSession(config, target) + session, client, err := getSession(config, context.Target) if err != nil { return cacao.NewVariables(), err } defer close(client) - return executeCommand(session, command) + return executeCommand(session, context.Command) } func executeCommand(session *ssh.Session, @@ -70,7 +66,7 @@ func executeCommand(session *ssh.Session, response, err := session.Output(StripSshPrepend(command.Command)) if err != nil { - log.Errorf("Output: %s", err) + log.Error("Output: ", err) return cacao.NewVariables(), err } results := cacao.NewVariables(cacao.Variable{Type: cacao.VariableTypeString, @@ -81,12 +77,12 @@ func executeCommand(session *ssh.Session, if sessionErr != nil && sessionErr.Error() != "EOF" { // The ssh api is subtle, and it can happen that we get EOF as an error. // This is likely not an error, as the session can also be closed by the host. - log.Errorf("Close: %s", sessionErr) + log.Error("Close: ", sessionErr) } return results, err } -func getConfig(authentication cacao.AuthenticationInformation) (ssh.ClientConfig, error) { +func (sshCapability *SshCapability) getConfig(authentication cacao.AuthenticationInformation) (ssh.ClientConfig, error) { config := ssh.ClientConfig{User: authentication.Username, HostKeyCallback: ssh.InsecureIgnoreHostKey(), Timeout: time.Duration(time.Second * 20)} @@ -101,7 +97,7 @@ func getConfig(authentication cacao.AuthenticationInformation) (ssh.ClientConfig if authentication.KmsKeyIdentifier == "" { return config, fmt.Errorf("KMS indicated, but no kms_key_identifier given") } - private_key, err := globalKeyManagement.GetPrivate(authentication.KmsKeyIdentifier) + private_key, err := sshCapability.Keys.GetPrivate(authentication.KmsKeyIdentifier) if err != nil { return config, err } @@ -131,13 +127,13 @@ func getSession(config ssh.ClientConfig, target cacao.AgentTarget) (*ssh.Session host := CombinePortAndAddress(target.Address, target.Port) client, err := ssh.Dial("tcp", host, &config) if err != nil { - log.Errorf("Dialing: %s", err) + log.Error(err) return nil, nil, err } session, err := client.NewSession() if err != nil { - log.Errorf("Session: %s", err) + log.Error(err) close(client) return nil, nil, err } @@ -192,7 +188,7 @@ func close(client *ssh.Client) { if client != nil { err := client.Close() if err != nil { - log.Errorf("Closing: %s", err) + log.Error(err) } } } diff --git a/pkg/keymanagement/keymanagement.go b/pkg/keymanagement/keymanagement.go new file mode 100644 index 000000000..d92b580f8 --- /dev/null +++ b/pkg/keymanagement/keymanagement.go @@ -0,0 +1,122 @@ +package keymanagement + +import ( + "fmt" + "os" + "reflect" + keymanagementrepository "soarca/internal/database/keymanagement" + "soarca/internal/logger" + keys "soarca/pkg/models/keymanagement" + + "golang.org/x/crypto/ssh" +) + +var component = reflect.TypeOf(KeyManagement{}).PkgPath() +var log *logger.Log + +func init() { + log = logger.Logger(component, logger.Info, "", logger.Json) +} + +type KeyManagement struct { + database keymanagementrepository.IKeyManagementRepository + cached_keys map[string]keys.KeyPair +} + +func InitKeyManagement(database keymanagementrepository.IKeyManagementRepository) *KeyManagement { + return &KeyManagement{database: database, cached_keys: make(map[string]keys.KeyPair)} +} + +func (management *KeyManagement) GetKeyPair(name string) (*keys.KeyPair, error) { + keypair, ok := management.cached_keys[name] + if !ok { + keypair, err := management.database.Read(name) + if err != nil { + return nil, err + } + return &keypair, nil + } + return &keypair, nil +} +func (management *KeyManagement) GetPrivate(name string) (ssh.Signer, error) { + keypair, err := management.GetKeyPair(name) + if err != nil { + return nil, err + } + return keypair.Private, nil +} +func parsePrivateKey(filename string, passphrase string) (ssh.Signer, error) { + log.Tracef("Opening private key from path %s", filename) + file_buffer, err := os.ReadFile(filename) + if err != nil { + return nil, err + } + if passphrase == "" { + return ssh.ParsePrivateKey(file_buffer) + } else { + return ssh.ParsePrivateKeyWithPassphrase(file_buffer, []byte(passphrase)) + } +} +func parsePublicKey(filename string) (ssh.PublicKey, error) { + log.Tracef("Opening public key from path %s", filename) + file, err := os.Open(filename) + if err != nil { + return nil, err + } + file_buffer := make([]byte, 2048) + if _, err = file.Read(file_buffer); err != nil { + return nil, err + } + key, _, _, _, err := ssh.ParseAuthorizedKey(file_buffer) + return key, err + +} + +func (management *KeyManagement) Insert(public string, private string, passphrase string, name string) error { + if _, err := management.GetKeyPair(name); err == nil { + return fmt.Errorf("Key with name already exists: %s (error: %s)", name, err) + } + return management.insertInternal(public, private, passphrase, name) +} + +func (management *KeyManagement) Update(public string, private string, passphrase string, name string) error { + if _, err := management.GetKeyPair(name); err != nil { + return fmt.Errorf("No such key exists: %s (error: %s)", name, err) + } + return management.insertInternal(public, private, passphrase, name) +} + +func (management *KeyManagement) insertInternal(public string, private string, passphrase string, name string) error { + public_key, _, _, _, err := ssh.ParseAuthorizedKey([]byte(public)) + if err != nil { + return fmt.Errorf("parsing public key: %s", err) + } + var private_key ssh.Signer + if passphrase == "" { + private_key, err = ssh.ParsePrivateKey([]byte(private)) + } else { + private_key, err = ssh.ParsePrivateKeyWithPassphrase([]byte(private), []byte(passphrase)) + } + if err != nil { + return fmt.Errorf("parsing private key: %s", err) + } + keypair := keys.KeyPair{Public: public_key, Private: private_key} + management.cached_keys[name] = keypair + management.database.Create(name, keypair) + return nil +} + +func (management *KeyManagement) ListAllNames() []string { + keys := make([]string, len(management.cached_keys)) + index := 0 + for key := range management.cached_keys { + keys[index] = key + index++ + } + return keys +} + +func (management *KeyManagement) Revoke(keyname string) { + management.database.Delete(keyname) + delete(management.cached_keys, keyname) +} diff --git a/pkg/keymanagement/keymanagement_test.go b/pkg/keymanagement/keymanagement_test.go new file mode 100644 index 000000000..7e4506d42 --- /dev/null +++ b/pkg/keymanagement/keymanagement_test.go @@ -0,0 +1,53 @@ +package keymanagement + +import ( + "os" + "path" + "slices" + "soarca/internal/database/memory" + "testing" + + "github.com/stretchr/testify/assert" +) + +const testkey string = "test-key" + +func testkey_dir() string { + return path.Join("..", "..", "test", "unittest", "mocks", "mock_utils") +} + +var globalKeyManagement *KeyManagement + +func init() { + globalKeyManagement = InitKeyManagement(memory.NewKeyManagementDatabase()) +} + +func TestRevoke(t *testing.T) { + addKey(t, testkey) + assert.True(t, slices.Contains(globalKeyManagement.ListAllNames(), testkey)) + globalKeyManagement.Revoke(testkey) + assert.False(t, slices.Contains(globalKeyManagement.ListAllNames(), testkey)) +} +func addKey(t *testing.T, keyname string) { + pubkey_path := path.Join(testkey_dir(), "test-key.pub") + privkey_path := path.Join(testkey_dir(), "test-key") + pubkey_file, err := os.Open(pubkey_path) + assert.Nil(t, err) + + privkey_file, err := os.Open(privkey_path) + assert.Nil(t, err) + pubkey_buf := make([]byte, 2048) + privkey_buf := make([]byte, 2048) + _, err = pubkey_file.Read(pubkey_buf) + assert.Nil(t, err) + _, err = privkey_file.Read(privkey_buf) + assert.Nil(t, err) + assert.Nil(t, globalKeyManagement.Insert(string(pubkey_buf), string(privkey_buf), "", keyname)) + assert.Nil(t, privkey_file.Close()) + assert.Nil(t, pubkey_file.Close()) +} + +func TestAddKey(t *testing.T) { + addKey(t, testkey) + assert.True(t, slices.Equal(globalKeyManagement.ListAllNames(), []string{testkey})) +} diff --git a/pkg/models/api/keymanagement.go b/pkg/models/api/keymanagement.go index 6ce88ccca..41808ed52 100644 --- a/pkg/models/api/keymanagement.go +++ b/pkg/models/api/keymanagement.go @@ -4,6 +4,7 @@ type KeyManagementKeyList struct { Keys []string `json:"keys"` } type KeyManagementKey struct { - Private string `json:"private"` - Public string `json:"public"` + Private string `json:"private"` + Public string `json:"public"` + Passphrase string `json:"passphrase"` } diff --git a/pkg/models/keymanagement/keymanagent.go b/pkg/models/keymanagement/keymanagent.go new file mode 100644 index 000000000..a96199619 --- /dev/null +++ b/pkg/models/keymanagement/keymanagent.go @@ -0,0 +1,8 @@ +package keymanagement + +import "golang.org/x/crypto/ssh" + +type KeyPair struct { + Public ssh.PublicKey + Private ssh.Signer +} From b74164282e56982ee97ca969478e996172766387 Mon Sep 17 00:00:00 2001 From: Thijs Heijligenberg Date: Mon, 18 Aug 2025 17:36:56 +0200 Subject: [PATCH 12/18] Updated ssh-kms deployment and added ci --- .github/workflows/ci.yml | 7 +- .../docker/testing/ssh-kms-test/Dockerfile | 19 ++++ .../testing/ssh-kms-test/docker-compose.yml | 22 ++--- .../testing/ssh-kms-test/prepare_keys.sh | 6 -- examples/ssh-kms-playbook.json | 2 +- test/integration/kms/kms_test.go | 89 +++++++++++++++++++ 6 files changed, 120 insertions(+), 25 deletions(-) create mode 100644 deployments/docker/testing/ssh-kms-test/Dockerfile delete mode 100755 deployments/docker/testing/ssh-kms-test/prepare_keys.sh create mode 100644 test/integration/kms/kms_test.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0d29f5e63..a467d693a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -59,10 +59,13 @@ jobs: - name: Install swaggo run: go install github.com/swaggo/swag/cmd/swag@latest timeout-minutes: 12 - - name: Start docker containers for test + - name: Start docker container for http test run: docker compose -f "deployments/docker/testing/httpbin-test/docker-compose.yml" up -d --build + - name: Start docker container for ssh test + run: docker compose -f "deployments/docker/testing/ssh-test/docker-compose.yml" up -d --build + - name: Start docker container for kms test + run: docker compose -f "deployments/docker/testing/ssh-kms-test/docker-compose.yml" up -d --build - name: Run tests run: | - docker compose -f "deployments/docker/testing/ssh-test/docker-compose.yml" up -d --build make ci-test diff --git a/deployments/docker/testing/ssh-kms-test/Dockerfile b/deployments/docker/testing/ssh-kms-test/Dockerfile new file mode 100644 index 000000000..59d20de0d --- /dev/null +++ b/deployments/docker/testing/ssh-kms-test/Dockerfile @@ -0,0 +1,19 @@ +FROM ubuntu:latest + +RUN apt update && apt install openssh-server sudo -y + +RUN sudo useradd sshtest + + +RUN mkdir -p /home/sshtest/ +RUN ssh-keygen -q -N "" -f /home/sshtest/test +RUN mkdir -p /home/sshtest/.ssh/ +RUN cat /home/sshtest/test.pub > /home/sshtest/.ssh/authorized_keys +RUN echo "PasswordAuthentication no" >> /etc/ssh/sshd_config +RUN echo "PubkeyAuthentication yes" >> /etc/ssh/sshd_config + +RUN service ssh start + +EXPOSE 22 + +CMD ["/usr/sbin/sshd","-D"] diff --git a/deployments/docker/testing/ssh-kms-test/docker-compose.yml b/deployments/docker/testing/ssh-kms-test/docker-compose.yml index ee32b830b..be883465a 100644 --- a/deployments/docker/testing/ssh-kms-test/docker-compose.yml +++ b/deployments/docker/testing/ssh-kms-test/docker-compose.yml @@ -1,17 +1,7 @@ -services: - openssh-server: - image: lscr.io/linuxserver/openssh-server:latest - container_name: openssh-server - hostname: openssh-server - environment: - - PUID=1000 - - PGID=1000 - - TZ=Etc/UTC - - USER_NAME=linuxserver.io - - LOG_STDOUT= ssh.log - volumes: - - ./config:/config + services: + ssh_server: + container_name: ssh_server + build: + dockerfile: Dockerfile ports: - - 2222:2222 - restart: unless-stopped - + - 2223:22 diff --git a/deployments/docker/testing/ssh-kms-test/prepare_keys.sh b/deployments/docker/testing/ssh-kms-test/prepare_keys.sh deleted file mode 100755 index 21edf4c95..000000000 --- a/deployments/docker/testing/ssh-kms-test/prepare_keys.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/sh -set -u -mkdir -p ssh-keystore/ -ssh-keygen -q -N "" -f ssh-keystore/test -mkdir -p config/.ssh/ -cat ssh-keystore/test.pub > config/.ssh/authorized_keys diff --git a/examples/ssh-kms-playbook.json b/examples/ssh-kms-playbook.json index b4b7af4d2..8a2a5b4b3 100644 --- a/examples/ssh-kms-playbook.json +++ b/examples/ssh-kms-playbook.json @@ -43,7 +43,7 @@ "127.0.0.1" ] }, - "port": "2222", + "port": "2223", "authentication_info": "user-auth--b7ddc2ea-9f6a-4e82-8eaa-be202e942090" } }, diff --git a/test/integration/kms/kms_test.go b/test/integration/kms/kms_test.go new file mode 100644 index 000000000..32a80323e --- /dev/null +++ b/test/integration/kms/kms_test.go @@ -0,0 +1,89 @@ +package kms + +import ( + "fmt" + "os" + "path" + "soarca/internal/database/memory" + "soarca/pkg/core/capability" + "soarca/pkg/core/capability/ssh" + "soarca/pkg/keymanagement" + "soarca/pkg/models/cacao" + "soarca/pkg/models/execution" + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" +) + +var globalKeyManagement *keymanagement.KeyManagement + +func init() { + globalKeyManagement = keymanagement.InitKeyManagement(memory.NewKeyManagementDatabase()) +} + +const testkey string = "test" + +func testkey_dir() string { + return path.Join("..", "..", "..", "deployments", "docker", "testing", "ssh-kms-test") +} +func addTestKey(t *testing.T) { + pubkey_path := path.Join(testkey_dir(), testkey+".pub") + privkey_path := path.Join(testkey_dir(), testkey) + pubkey, err := os.ReadFile(pubkey_path) + assert.Nil(t, err) + privkey, err := os.ReadFile(privkey_path) + assert.Nil(t, err) + assert.Nil(t, globalKeyManagement.Insert(string(pubkey), string(privkey), "", testkey)) +} + +func TestSshConnection(t *testing.T) { + sshCapability := ssh.SshCapability{Keys: globalKeyManagement} + addTestKey(t) + + expectedCommand := cacao.Command{ + Type: "ssh", + Command: "ls -la", + } + + expectedAuthenticationInformation := cacao.AuthenticationInformation{ + ID: "some-authid-1", + Type: "user-auth", + Username: "sshtest", + Kms: true, + KmsKeyIdentifier: testkey, + } + + expectedTarget := cacao.AgentTarget{ + Type: "ssh", + Address: map[cacao.NetAddressType][]string{"ipv4": {"localhost"}}, + Port: "2223", + AuthInfoIdentifier: "some-authid-1", + } + + expectedVariables := cacao.Variable{ + Type: "string", + Name: "var1", + Value: "testing", + } + + var executionId, _ = uuid.Parse("6ba7b810-9dad-11d1-80b4-00c04fd430c8") + var playbookId = "playbook--d09351a2-a075-40c8-8054-0b7c423db83f" + var stepId = "step--81eff59f-d084-4324-9e0a-59e353dbd28f" + var metadata = execution.Metadata{ExecutionId: executionId, PlaybookId: playbookId, StepId: stepId} + data := capability.Context{ + Command: expectedCommand, + Target: expectedTarget, + Authentication: expectedAuthenticationInformation, + Variables: cacao.NewVariables(expectedVariables), + } + results, err := sshCapability.Execute(metadata, + data) + if err != nil { + fmt.Println(err) + t.Fail() + } + + fmt.Println(results) + +} From 77eb7dd97680fc89ad0e55920f1535be0d30ef4c Mon Sep 17 00:00:00 2001 From: Thijs Heijligenberg Date: Tue, 19 Aug 2025 10:29:56 +0200 Subject: [PATCH 13/18] Updated ci build to properly fetch keys --- .github/workflows/ci.yml | 5 ++++- .../docker/testing/ssh-kms-test/Dockerfile | 16 +++++++++------- .../testing/ssh-kms-test/docker-compose.yml | 1 + 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a467d693a..31736f11c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -64,7 +64,10 @@ jobs: - name: Start docker container for ssh test run: docker compose -f "deployments/docker/testing/ssh-test/docker-compose.yml" up -d --build - name: Start docker container for kms test - run: docker compose -f "deployments/docker/testing/ssh-kms-test/docker-compose.yml" up -d --build + run: | + docker compose -f "deployments/docker/testing/ssh-kms-test/docker-compose.yml" up -d --build + docker compose -f "deployments/docker/testing/ssh-kms-test/docker-compose.yml" cp ssh_server:/test deployments/docker/testing/ssh-kms-test + docker compose -f "deployments/docker/testing/ssh-kms-test/docker-compose.yml" cp ssh_server:/test.pub deployments/docker/testing/ssh-kms-test - name: Run tests run: | make ci-test diff --git a/deployments/docker/testing/ssh-kms-test/Dockerfile b/deployments/docker/testing/ssh-kms-test/Dockerfile index 59d20de0d..ea3e6b6c2 100644 --- a/deployments/docker/testing/ssh-kms-test/Dockerfile +++ b/deployments/docker/testing/ssh-kms-test/Dockerfile @@ -1,19 +1,21 @@ -FROM ubuntu:latest +FROM ubuntu:latest RUN apt update && apt install openssh-server sudo -y -RUN sudo useradd sshtest +RUN sudo useradd -m sshtest +RUN ssh-keygen -q -N "" -f /test -RUN mkdir -p /home/sshtest/ -RUN ssh-keygen -q -N "" -f /home/sshtest/test RUN mkdir -p /home/sshtest/.ssh/ -RUN cat /home/sshtest/test.pub > /home/sshtest/.ssh/authorized_keys + +RUN cat /test.pub > /home/sshtest/.ssh/authorized_keys + RUN echo "PasswordAuthentication no" >> /etc/ssh/sshd_config + RUN echo "PubkeyAuthentication yes" >> /etc/ssh/sshd_config -RUN service ssh start +RUN sudo service ssh start EXPOSE 22 -CMD ["/usr/sbin/sshd","-D"] +CMD ["sudo","/usr/sbin/sshd","-D"] diff --git a/deployments/docker/testing/ssh-kms-test/docker-compose.yml b/deployments/docker/testing/ssh-kms-test/docker-compose.yml index be883465a..d84e78441 100644 --- a/deployments/docker/testing/ssh-kms-test/docker-compose.yml +++ b/deployments/docker/testing/ssh-kms-test/docker-compose.yml @@ -5,3 +5,4 @@ dockerfile: Dockerfile ports: - 2223:22 + From 16ebe57220e9772fe3bdf060a3ee80c5ed8f7dc1 Mon Sep 17 00:00:00 2001 From: Thijs Heijligenberg Date: Tue, 19 Aug 2025 10:43:05 +0200 Subject: [PATCH 14/18] Smaller fixes from review --- pkg/api/keymanagement/keymanagement_api.go | 41 +++++++-------------- pkg/core/capability/ssh/ssh.go | 21 +++++++---- test/unittest/mocks/mock_utils/test-key.pub | 2 +- 3 files changed, 28 insertions(+), 36 deletions(-) diff --git a/pkg/api/keymanagement/keymanagement_api.go b/pkg/api/keymanagement/keymanagement_api.go index 16341bb9e..5f4402c84 100644 --- a/pkg/api/keymanagement/keymanagement_api.go +++ b/pkg/api/keymanagement/keymanagement_api.go @@ -2,14 +2,13 @@ package keymanagement_api import ( "encoding/json" - "fmt" "io" "net/http" "reflect" "soarca/internal/logger" + apiError "soarca/pkg/api/error" "soarca/pkg/keymanagement" "soarca/pkg/models/api" - "strconv" "github.com/gin-gonic/gin" ) @@ -63,19 +62,19 @@ func (handler *KeyManagementHandler) AddKey(context *gin.Context) { keyname := context.Param("keyname") jsonData, err := io.ReadAll(context.Request.Body) if err != nil { - log.Trace("Submit key has failed: ", err.Error()) - SendErrorResponse(context, http.StatusBadRequest, "Failed to read json on server side", "PUT /keymanagement/:keyname") + log.Error("Submit key has failed: ", err.Error()) + apiError.SendErrorResponse(context, http.StatusBadRequest, "Failed to read json on server side", "PUT /keymanagement/:keyname", "") return } var key api.KeyManagementKey if err := json.Unmarshal(jsonData, &key); err != nil { - log.Trace("Submit key failed to unmarshal: ", err.Error()) - SendErrorResponse(context, http.StatusBadRequest, "Failed to marshall json on server side", "PUT /keymanagement/:keyname") + log.Error("Submit key failed to unmarshal: ", err.Error()) + apiError.SendErrorResponse(context, http.StatusBadRequest, "Failed to marshall json on server side", "PUT /keymanagement/:keyname", "") return } if err := handler.Manager.Insert(key.Public, key.Private, key.Passphrase, keyname); err != nil { - log.Trace("Submit key failed to insert: ", err.Error()) - SendErrorResponse(context, http.StatusBadRequest, "Failed to insert key on server side", "PUT /keymanagement/:keyname") + log.Error("Submit key failed to insert: ", err.Error()) + apiError.SendErrorResponse(context, http.StatusBadRequest, "Failed to insert key on server side", "PUT /keymanagement/:keyname", "") return } @@ -98,19 +97,19 @@ func (handler *KeyManagementHandler) UpdateKey(context *gin.Context) { keyname := context.Param("keyname") jsonData, err := io.ReadAll(context.Request.Body) if err != nil { - log.Trace("Update key has failed: ", err.Error()) - SendErrorResponse(context, http.StatusBadRequest, "Failed to read json on server side", "PATCH /keymanagement/:keyname") + log.Error("Update key has failed: ", err.Error()) + apiError.SendErrorResponse(context, http.StatusBadRequest, "Failed to read json on server side", "PATCH /keymanagement/:keyname", "") return } var key api.KeyManagementKey if err := json.Unmarshal(jsonData, &key); err != nil { - log.Trace("Update key failed to unmarshal: ", err.Error()) - SendErrorResponse(context, http.StatusBadRequest, "Failed to marshall json on server side", "PATCH /keymanagement/:keyname") + log.Error("Update key failed to unmarshal: ", err.Error()) + apiError.SendErrorResponse(context, http.StatusBadRequest, "Failed to marshall json on server side", "PATCH /keymanagement/:keyname", "") return } if err := handler.Manager.Update(key.Public, key.Private, key.Passphrase, keyname); err != nil { - log.Trace("Update key failed to insert: ", err.Error()) - SendErrorResponse(context, http.StatusBadRequest, "Failed to update key on server side", "PATCH /keymanagement/:keyname") + log.Error("Update key failed to insert: ", err.Error()) + apiError.SendErrorResponse(context, http.StatusBadRequest, "Failed to update key on server side", "PATCH /keymanagement/:keyname", "") return } @@ -131,18 +130,6 @@ func (handler *KeyManagementHandler) UpdateKey(context *gin.Context) { func (handler *KeyManagementHandler) RevokeKey(context *gin.Context) { keyname := context.Param("keyname") handler.Manager.Revoke(keyname) - context.JSON(http.StatusOK, gin.H{ - "status": 200, - "message": fmt.Sprintf("Removed key %s from SOARCA listing", keyname), - }) + context.JSON(http.StatusOK, Empty{}) log.Trace("Removed key ", keyname) } - -func SendErrorResponse(context *gin.Context, status int, message string, orginal_call string) { - msg := gin.H{ - "status": strconv.Itoa(status), - "message": message, - "original-call": orginal_call, - } - context.JSON(status, msg) -} diff --git a/pkg/core/capability/ssh/ssh.go b/pkg/core/capability/ssh/ssh.go index a64ae2854..d5f627e72 100644 --- a/pkg/core/capability/ssh/ssh.go +++ b/pkg/core/capability/ssh/ssh.go @@ -74,10 +74,14 @@ func executeCommand(session *ssh.Session, Value: string(response)}) log.Trace("Finished ssh execution will return the variables: ", results) sessionErr := session.Close() - if sessionErr != nil && sessionErr.Error() != "EOF" { - // The ssh api is subtle, and it can happen that we get EOF as an error. - // This is likely not an error, as the session can also be closed by the host. - log.Error("Close: ", sessionErr) + if sessionErr != nil { + if sessionErr.Error() != "EOF" { + // The ssh api is subtle, and it can happen that we get EOF as an error. + // This is likely not an error, as the session can also be closed by the host. + log.Debug("SSH session close got EOF") + } else { + log.Error("Close: ", sessionErr) + } } return results, err } @@ -89,10 +93,6 @@ func (sshCapability *SshCapability) getConfig(authentication cacao.Authenticatio switch authentication.Type { case "user-auth": - if authentication.Password != "" { - config.Auth = []ssh.AuthMethod{ - ssh.Password(authentication.Password)} - } if authentication.Kms { if authentication.KmsKeyIdentifier == "" { return config, fmt.Errorf("KMS indicated, but no kms_key_identifier given") @@ -104,6 +104,11 @@ func (sshCapability *SshCapability) getConfig(authentication cacao.Authenticatio config.Auth = []ssh.AuthMethod{ ssh.PublicKeys(private_key), } + } else if authentication.Password != "" { + config.Auth = []ssh.AuthMethod{ + ssh.Password(authentication.Password)} + } else { + return config, fmt.Errorf("No authentication method given in user-auth") } return config, nil diff --git a/test/unittest/mocks/mock_utils/test-key.pub b/test/unittest/mocks/mock_utils/test-key.pub index d6e7b2c9e..e860bf5dc 100644 --- a/test/unittest/mocks/mock_utils/test-key.pub +++ b/test/unittest/mocks/mock_utils/test-key.pub @@ -1 +1 @@ -ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGzijTr3feGJz8CuxKjR+z1xROTQAdza843VmpLrxV/u thijs@PC-44318 +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGzijTr3feGJz8CuxKjR+z1xROTQAdza843VmpLrxV/u user@PC From efed787cf0eddb064043e03d5f8c3d41374a2892 Mon Sep 17 00:00:00 2001 From: Thijs Heijligenberg Date: Tue, 19 Aug 2025 10:45:50 +0200 Subject: [PATCH 15/18] Fixed name clash --- .github/workflows/ci.yml | 4 ++-- deployments/docker/testing/ssh-kms-test/docker-compose.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 31736f11c..7169bf41f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -66,8 +66,8 @@ jobs: - name: Start docker container for kms test run: | docker compose -f "deployments/docker/testing/ssh-kms-test/docker-compose.yml" up -d --build - docker compose -f "deployments/docker/testing/ssh-kms-test/docker-compose.yml" cp ssh_server:/test deployments/docker/testing/ssh-kms-test - docker compose -f "deployments/docker/testing/ssh-kms-test/docker-compose.yml" cp ssh_server:/test.pub deployments/docker/testing/ssh-kms-test + docker compose -f "deployments/docker/testing/ssh-kms-test/docker-compose.yml" cp ssh_kms_server:/test deployments/docker/testing/ssh-kms-test + docker compose -f "deployments/docker/testing/ssh-kms-test/docker-compose.yml" cp ssh_kms_server:/test.pub deployments/docker/testing/ssh-kms-test - name: Run tests run: | make ci-test diff --git a/deployments/docker/testing/ssh-kms-test/docker-compose.yml b/deployments/docker/testing/ssh-kms-test/docker-compose.yml index d84e78441..43ff56745 100644 --- a/deployments/docker/testing/ssh-kms-test/docker-compose.yml +++ b/deployments/docker/testing/ssh-kms-test/docker-compose.yml @@ -1,6 +1,6 @@ services: - ssh_server: - container_name: ssh_server + ssh_kms_server: + container_name: ssh_kms_server build: dockerfile: Dockerfile ports: From 2ea5e7834aef968a37597fd9cb785dfe58883259 Mon Sep 17 00:00:00 2001 From: Thijs Heijligenberg Date: Tue, 19 Aug 2025 10:49:47 +0200 Subject: [PATCH 16/18] More capitalization errors --- internal/database/memory/keymanagement.go | 2 +- pkg/core/capability/ssh/ssh.go | 2 +- pkg/keymanagement/keymanagement.go | 30 ++--------------------- 3 files changed, 4 insertions(+), 30 deletions(-) diff --git a/internal/database/memory/keymanagement.go b/internal/database/memory/keymanagement.go index fb89cc458..bdfeb0989 100644 --- a/internal/database/memory/keymanagement.go +++ b/internal/database/memory/keymanagement.go @@ -27,7 +27,7 @@ func (database *InMemoryKeyManagementDatabase) Create(name string, keypair keyma func (database *InMemoryKeyManagementDatabase) Read(id string) (keymanagement.KeyPair, error) { keypair, ok := database.keys[id] if !ok { - return keymanagement.KeyPair{}, fmt.Errorf("Could not find key named %s", id) + return keymanagement.KeyPair{}, fmt.Errorf("could not find key named %s", id) } return keypair, nil } diff --git a/pkg/core/capability/ssh/ssh.go b/pkg/core/capability/ssh/ssh.go index d5f627e72..525a235b6 100644 --- a/pkg/core/capability/ssh/ssh.go +++ b/pkg/core/capability/ssh/ssh.go @@ -108,7 +108,7 @@ func (sshCapability *SshCapability) getConfig(authentication cacao.Authenticatio config.Auth = []ssh.AuthMethod{ ssh.Password(authentication.Password)} } else { - return config, fmt.Errorf("No authentication method given in user-auth") + return config, fmt.Errorf("no authentication method given in user-auth") } return config, nil diff --git a/pkg/keymanagement/keymanagement.go b/pkg/keymanagement/keymanagement.go index d92b580f8..fb66bbe6e 100644 --- a/pkg/keymanagement/keymanagement.go +++ b/pkg/keymanagement/keymanagement.go @@ -45,43 +45,17 @@ func (management *KeyManagement) GetPrivate(name string) (ssh.Signer, error) { } return keypair.Private, nil } -func parsePrivateKey(filename string, passphrase string) (ssh.Signer, error) { - log.Tracef("Opening private key from path %s", filename) - file_buffer, err := os.ReadFile(filename) - if err != nil { - return nil, err - } - if passphrase == "" { - return ssh.ParsePrivateKey(file_buffer) - } else { - return ssh.ParsePrivateKeyWithPassphrase(file_buffer, []byte(passphrase)) - } -} -func parsePublicKey(filename string) (ssh.PublicKey, error) { - log.Tracef("Opening public key from path %s", filename) - file, err := os.Open(filename) - if err != nil { - return nil, err - } - file_buffer := make([]byte, 2048) - if _, err = file.Read(file_buffer); err != nil { - return nil, err - } - key, _, _, _, err := ssh.ParseAuthorizedKey(file_buffer) - return key, err - -} func (management *KeyManagement) Insert(public string, private string, passphrase string, name string) error { if _, err := management.GetKeyPair(name); err == nil { - return fmt.Errorf("Key with name already exists: %s (error: %s)", name, err) + return fmt.Errorf("key with name already exists: %s (error: %s)", name, err) } return management.insertInternal(public, private, passphrase, name) } func (management *KeyManagement) Update(public string, private string, passphrase string, name string) error { if _, err := management.GetKeyPair(name); err != nil { - return fmt.Errorf("No such key exists: %s (error: %s)", name, err) + return fmt.Errorf("no such key exists: %s (error: %s)", name, err) } return management.insertInternal(public, private, passphrase, name) } From 5b19a3523ee3d0fb917a63e907d2a78abf7971f2 Mon Sep 17 00:00:00 2001 From: Thijs Heijligenberg Date: Tue, 19 Aug 2025 10:57:48 +0200 Subject: [PATCH 17/18] Fixing lint errors --- internal/database/mongodb/mongo.go | 10 +++++++++- pkg/api/keymanagement/keymanagement_api.go | 7 +++++-- pkg/keymanagement/keymanagement.go | 13 ++++++++----- pkg/keymanagement/keymanagement_test.go | 2 +- 4 files changed, 23 insertions(+), 9 deletions(-) diff --git a/internal/database/mongodb/mongo.go b/internal/database/mongodb/mongo.go index 5a307f432..06764243c 100644 --- a/internal/database/mongodb/mongo.go +++ b/internal/database/mongodb/mongo.go @@ -82,8 +82,16 @@ func SetupMongodb(uri string, username string, password string) error { } cacaoPlayBookRepo, err = NewMongoCollection[cacao.Playbook](mongoclient, "soarca", "cacoa_playbook_collection") + if err != nil { + log.Error("failed to setup playbook MongoCollection, error msg: ", err.Error()) + return err + } keyManagementRepo, err = NewMongoCollection[keymanagementrepository.KeyPairEntry](mongoclient, "keymanagement", "keymanagement_collection") - return err + if err != nil { + log.Error("failed to setup kms MongoCollection, error msg: ", err.Error()) + return err + } + return nil } // helper function to poperly obtain whether object is already in the database store diff --git a/pkg/api/keymanagement/keymanagement_api.go b/pkg/api/keymanagement/keymanagement_api.go index 5f4402c84..e88c29e60 100644 --- a/pkg/api/keymanagement/keymanagement_api.go +++ b/pkg/api/keymanagement/keymanagement_api.go @@ -111,7 +111,6 @@ func (handler *KeyManagementHandler) UpdateKey(context *gin.Context) { log.Error("Update key failed to insert: ", err.Error()) apiError.SendErrorResponse(context, http.StatusBadRequest, "Failed to update key on server side", "PATCH /keymanagement/:keyname", "") return - } log.Trace("Updated key ", keyname) context.JSON(http.StatusOK, Empty{}) @@ -129,7 +128,11 @@ func (handler *KeyManagementHandler) UpdateKey(context *gin.Context) { // @Router /keymanagement/:keyname/ [DELETE] func (handler *KeyManagementHandler) RevokeKey(context *gin.Context) { keyname := context.Param("keyname") - handler.Manager.Revoke(keyname) + if err := handler.Manager.Revoke(keyname); err != nil { + log.Error("Failed to remove key:", err.Error()) + apiError.SendErrorResponse(context, http.StatusBadRequest, "Failed to delete key on server side", "DELETE /keymanagement/:keyname", "") + return + } context.JSON(http.StatusOK, Empty{}) log.Trace("Removed key ", keyname) } diff --git a/pkg/keymanagement/keymanagement.go b/pkg/keymanagement/keymanagement.go index fb66bbe6e..688903aec 100644 --- a/pkg/keymanagement/keymanagement.go +++ b/pkg/keymanagement/keymanagement.go @@ -2,7 +2,6 @@ package keymanagement import ( "fmt" - "os" "reflect" keymanagementrepository "soarca/internal/database/keymanagement" "soarca/internal/logger" @@ -28,6 +27,7 @@ func InitKeyManagement(database keymanagementrepository.IKeyManagementRepository } func (management *KeyManagement) GetKeyPair(name string) (*keys.KeyPair, error) { + log.Trace("Getting keypair named", name) keypair, ok := management.cached_keys[name] if !ok { keypair, err := management.database.Read(name) @@ -61,6 +61,7 @@ func (management *KeyManagement) Update(public string, private string, passphras } func (management *KeyManagement) insertInternal(public string, private string, passphrase string, name string) error { + log.Trace("Inserting keypair named", name) public_key, _, _, _, err := ssh.ParseAuthorizedKey([]byte(public)) if err != nil { return fmt.Errorf("parsing public key: %s", err) @@ -76,11 +77,11 @@ func (management *KeyManagement) insertInternal(public string, private string, p } keypair := keys.KeyPair{Public: public_key, Private: private_key} management.cached_keys[name] = keypair - management.database.Create(name, keypair) - return nil + return management.database.Create(name, keypair) } func (management *KeyManagement) ListAllNames() []string { + log.Trace("Listing all keys") keys := make([]string, len(management.cached_keys)) index := 0 for key := range management.cached_keys { @@ -90,7 +91,9 @@ func (management *KeyManagement) ListAllNames() []string { return keys } -func (management *KeyManagement) Revoke(keyname string) { - management.database.Delete(keyname) +func (management *KeyManagement) Revoke(keyname string) error { + log.Trace("Deleting keypair named", keyname) + err := management.database.Delete(keyname) delete(management.cached_keys, keyname) + return err } diff --git a/pkg/keymanagement/keymanagement_test.go b/pkg/keymanagement/keymanagement_test.go index 7e4506d42..eb0b4e814 100644 --- a/pkg/keymanagement/keymanagement_test.go +++ b/pkg/keymanagement/keymanagement_test.go @@ -25,7 +25,7 @@ func init() { func TestRevoke(t *testing.T) { addKey(t, testkey) assert.True(t, slices.Contains(globalKeyManagement.ListAllNames(), testkey)) - globalKeyManagement.Revoke(testkey) + assert.Nil(t, globalKeyManagement.Revoke(testkey)) assert.False(t, slices.Contains(globalKeyManagement.ListAllNames(), testkey)) } func addKey(t *testing.T, keyname string) { From 589c193e5c3ff12b19e5634e91131e2aed1a50ef Mon Sep 17 00:00:00 2001 From: Thijs Heijligenberg Date: Tue, 19 Aug 2025 11:10:35 +0200 Subject: [PATCH 18/18] Removed cached keys --- pkg/api/keymanagement/keymanagement_api.go | 7 ++++- pkg/keymanagement/keymanagement.go | 30 ++++++---------------- pkg/keymanagement/keymanagement_test.go | 12 ++++++--- 3 files changed, 23 insertions(+), 26 deletions(-) diff --git a/pkg/api/keymanagement/keymanagement_api.go b/pkg/api/keymanagement/keymanagement_api.go index e88c29e60..cdcd133e6 100644 --- a/pkg/api/keymanagement/keymanagement_api.go +++ b/pkg/api/keymanagement/keymanagement_api.go @@ -42,7 +42,12 @@ func NewKeyManagementHandler(manager *keymanagement.KeyManagement) *KeyManagemen // @failure 400 {object} api.Error // @Router /keymanagement/ [GET] func (handler *KeyManagementHandler) GetKeys(context *gin.Context) { - keyInfo := handler.Manager.ListAllNames() + keyInfo, err := handler.Manager.ListAllNames() + if err != nil { + log.Error("Listing keys has failed: ", err.Error()) + apiError.SendErrorResponse(context, http.StatusBadRequest, "Failed to read keys on server side", "GET /keymanagement/", "") + return + } log.Trace("Listing all key names") context.JSON(http.StatusOK, keyInfo) } diff --git a/pkg/keymanagement/keymanagement.go b/pkg/keymanagement/keymanagement.go index 688903aec..3af0caf11 100644 --- a/pkg/keymanagement/keymanagement.go +++ b/pkg/keymanagement/keymanagement.go @@ -18,23 +18,18 @@ func init() { } type KeyManagement struct { - database keymanagementrepository.IKeyManagementRepository - cached_keys map[string]keys.KeyPair + database keymanagementrepository.IKeyManagementRepository } func InitKeyManagement(database keymanagementrepository.IKeyManagementRepository) *KeyManagement { - return &KeyManagement{database: database, cached_keys: make(map[string]keys.KeyPair)} + return &KeyManagement{database: database} } func (management *KeyManagement) GetKeyPair(name string) (*keys.KeyPair, error) { log.Trace("Getting keypair named", name) - keypair, ok := management.cached_keys[name] - if !ok { - keypair, err := management.database.Read(name) - if err != nil { - return nil, err - } - return &keypair, nil + keypair, err := management.database.Read(name) + if err != nil { + return nil, err } return &keypair, nil } @@ -76,24 +71,15 @@ func (management *KeyManagement) insertInternal(public string, private string, p return fmt.Errorf("parsing private key: %s", err) } keypair := keys.KeyPair{Public: public_key, Private: private_key} - management.cached_keys[name] = keypair return management.database.Create(name, keypair) } -func (management *KeyManagement) ListAllNames() []string { +func (management *KeyManagement) ListAllNames() ([]string, error) { log.Trace("Listing all keys") - keys := make([]string, len(management.cached_keys)) - index := 0 - for key := range management.cached_keys { - keys[index] = key - index++ - } - return keys + return management.database.GetKeyNames() } func (management *KeyManagement) Revoke(keyname string) error { log.Trace("Deleting keypair named", keyname) - err := management.database.Delete(keyname) - delete(management.cached_keys, keyname) - return err + return management.database.Delete(keyname) } diff --git a/pkg/keymanagement/keymanagement_test.go b/pkg/keymanagement/keymanagement_test.go index eb0b4e814..f1f07ffd6 100644 --- a/pkg/keymanagement/keymanagement_test.go +++ b/pkg/keymanagement/keymanagement_test.go @@ -24,9 +24,9 @@ func init() { func TestRevoke(t *testing.T) { addKey(t, testkey) - assert.True(t, slices.Contains(globalKeyManagement.ListAllNames(), testkey)) + assert.True(t, slices.Contains(allNames(t), testkey)) assert.Nil(t, globalKeyManagement.Revoke(testkey)) - assert.False(t, slices.Contains(globalKeyManagement.ListAllNames(), testkey)) + assert.False(t, slices.Contains(allNames(t), testkey)) } func addKey(t *testing.T, keyname string) { pubkey_path := path.Join(testkey_dir(), "test-key.pub") @@ -47,7 +47,13 @@ func addKey(t *testing.T, keyname string) { assert.Nil(t, pubkey_file.Close()) } +func allNames(t *testing.T) []string { + allnames, err := globalKeyManagement.ListAllNames() + assert.Nil(t, err) + return allnames +} + func TestAddKey(t *testing.T) { addKey(t, testkey) - assert.True(t, slices.Equal(globalKeyManagement.ListAllNames(), []string{testkey})) + assert.True(t, slices.Equal(allNames(t), []string{testkey})) }