From be06e3b87cc4a7b81bffca59a7640104d7109328 Mon Sep 17 00:00:00 2001 From: rony5394 <143897221+rony5394@users.noreply.github.com> Date: Sat, 14 Mar 2026 14:39:19 +0100 Subject: [PATCH] Added ephemeral ssh keys. --- docker/docker.go | 9 ++-- docker/keys.go | 54 ++++++++++++++++++++++ docker/prepare.go | 35 +++++++++++++-- host/host.go | 95 +++++++++++++++++++++++++++++++++------ {docker => shared}/ssh.go | 23 ++++------ 5 files changed, 180 insertions(+), 36 deletions(-) create mode 100644 docker/keys.go rename {docker => shared}/ssh.go (57%) diff --git a/docker/docker.go b/docker/docker.go index d4d6f5e..2929551 100644 --- a/docker/docker.go +++ b/docker/docker.go @@ -50,12 +50,6 @@ func Run(Config cfg.Config){ panic("Node is not a swarm manager."); } - sshKeypair := generateKeypair(); - ApiClient.ConfigCreate(context.Background(), swarm.ConfigSpec{ - Data: sshKeypair.public, - Annotations: swarm.Annotations{Name: "blazenaSSHPublicKey"}, - }); - server := &http.Server{ Addr: ":1234", } @@ -65,6 +59,7 @@ func Run(Config cfg.Config){ http.HandleFunc("/scale/down", scaleDown); http.HandleFunc("/prepare", prepare); http.HandleFunc("/cleanup", cleanup); + http.HandleFunc("/keys", exchangeKeys); // I'll make it better someday. http.HandleFunc("/shutdown", func(w http.ResponseWriter, r *http.Request) { if !bearerAuth(w, r){return} @@ -103,6 +98,8 @@ func Run(Config cfg.Config){ ApiClient.NetworkRemove(context.Background(), "blazenaPohar"); ApiClient.ConfigRemove(context.Background(), "blazenaSSHPublicKey") + ApiClient.SecretRemove(context.Background(), "blazenaSSHHostPrivateKey"); + fmt.Println("Exiting!"); } diff --git a/docker/keys.go b/docker/keys.go new file mode 100644 index 0000000..d436666 --- /dev/null +++ b/docker/keys.go @@ -0,0 +1,54 @@ +package docker + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "context" + "github.com/docker/docker/api/types/swarm" + + "github.com/rony5394/blazena/shared" +) + +func exchangeKeys(w http.ResponseWriter, r *http.Request){ + if r.Method != http.MethodPost{ + w.WriteHeader(http.StatusMethodNotAllowed); + fmt.Fprint(w, "Method Not Allowed"); + return; + } + if !bearerAuth(w, r) {return;} + + rawBody, err := io.ReadAll(r.Body); + if err != nil { + panic("Failed to read body!"); + } + + var bodyDecoded struct{ + SshPkPem string `json:"sshPkPem"` + }; + + err = json.Unmarshal(rawBody, &bodyDecoded); + if err != nil { + panic("Failed to unmarshal json."+ err.Error()); + } + sshPkPem := bodyDecoded.SshPkPem; + hostKeypair := shared.GenerateSSHKeypair(); + encoded, err := json.Marshal(struct{HostPkPem string `json:"hostPkPem"`}{HostPkPem: hostKeypair.Public}); + if err != nil { + panic("I wonder how. I wonder why?"+err.Error()); + } + + ApiClient.ConfigCreate(context.Background(), swarm.ConfigSpec{ + Data: []byte(sshPkPem), + Annotations: swarm.Annotations{Name: "blazenaSSHPublicKey"}, + }); + + ApiClient.SecretCreate(context.Background(), swarm.SecretSpec{ + Data: []byte(hostKeypair.Private), + Annotations: swarm.Annotations{Name: "blazenaSSHHostPrivateKey"}, + }); + + + fmt.Fprint(w, string(encoded)); +} diff --git a/docker/prepare.go b/docker/prepare.go index 1447b3c..9bdb6d6 100644 --- a/docker/prepare.go +++ b/docker/prepare.go @@ -65,7 +65,6 @@ func contains(slice []string, str string) bool { return false } -//By gpt (I'm lazy) func getConfigIDByName(cli *client.Client, name string) (string, error) { ctx := context.Background() @@ -83,6 +82,23 @@ func getConfigIDByName(cli *client.Client, name string) (string, error) { return "", fmt.Errorf("config not found: %s", name) } +func getSecretIDByName(cli *client.Client, name string) (string, error) { + ctx := context.Background() + + secrets, err := cli.SecretList(ctx, swarm.SecretListOptions{}) + if err != nil { + return "", err + } + + for _, sec := range secrets { + if sec.Spec.Name == name { + return sec.ID, nil + } + } + + return "", fmt.Errorf("config not found: %s", name) +} + func pullBlazenaImage(){ authConfig := registry.AuthConfig{ Username: theConfig.RegistryAuth.Username, @@ -109,14 +125,15 @@ func createHelper(targetNode string, targetVolume string){ maxConcurrent := uint64(1); totalCompletions := uint64(1); stopGracePeriod := time.Second * 5; - helperCommand := `ssh-keygen -t ed25519 -f /host_key && \ - /usr/sbin/sshd -h /host_key -p 2222 -D`; + helperCommand := `/usr/sbin/sshd -h /host-key -p 2222 -D`; sshKeyConfigId, err := getConfigIDByName(ApiClient, "blazenaSSHPublicKey"); if err != nil { panic("Docker needs both id and name to mount config for some reason and getting id of it failed!"+err.Error()); } + + sshHostKeySecretId, err := getSecretIDByName(ApiClient, "blazenaSSHHostPrivateKey") _, err = ApiClient.ServiceCreate(context.Background(), swarm.ServiceSpec{ Annotations: swarm.Annotations{ Name: "BlazenaHelper", @@ -153,6 +170,18 @@ func createHelper(targetNode string, targetVolume string){ }, }, }, + Secrets: []*swarm.SecretReference{ + &swarm.SecretReference{ + SecretID: sshHostKeySecretId, + SecretName: "blazenaSSHHostPrivateKey", + File: &swarm.SecretReferenceFileTarget{ + Name: "/host-key", + Mode: 0600, + UID: "0", + GID: "0", + }, + }, + }, }, Placement: &swarm.Placement{ Constraints: []string{"node.hostname=="+targetNode}, diff --git a/host/host.go b/host/host.go index 69f4a33..525cbb0 100644 --- a/host/host.go +++ b/host/host.go @@ -1,13 +1,14 @@ package host import ( + "archive/tar" "bytes" "context" + "encoding/base64" "encoding/json" "fmt" "io" "net/http" - "encoding/base64" "os" "time" @@ -15,11 +16,12 @@ import ( "github.com/docker/docker/api/types/image" "github.com/docker/docker/api/types/mount" "github.com/docker/docker/api/types/network" - "github.com/docker/docker/api/types/strslice" "github.com/docker/docker/api/types/registry" + "github.com/docker/docker/api/types/strslice" "github.com/docker/docker/client" v1 "github.com/opencontainers/image-spec/specs-go/v1" cfg "github.com/rony5394/blazena/config" + "github.com/rony5394/blazena/shared" ) var token string = "12345"; @@ -41,7 +43,9 @@ func Run(Config cfg.Config) { panic("Failed to ping DockerClient."); } - createStorageContainer(Config, DockerClient); + sshKeyPair := shared.GenerateSSHKeypair(); + sshHostPkPem := exchangeKeys(Config, string(sshKeyPair.Public)); + createStorageContainer(Config, DockerClient, sshKeyPair.Private, sshHostPkPem); services := getServices(Config); @@ -55,10 +59,9 @@ func Run(Config cfg.Config) { fmt.Println("Done!"); // Skiping Host Key Check is temporary. - command := `rsync -avz --delete -e "ssh -i /ssh-key -p 2222 -o StrictHostKeyChecking=no" \ + command := `rsync -avz --delete -e "ssh -i /ssh-key -p 2222 -o StrictHostKeyChecking=yes -o UserKnownHostsFile=/expected-host-key" \ root@tasks.BlazenaHelper:/volume/ /tmp/` + volume; - exec, err := DockerClient.ContainerExecCreate(context.Background(), "BlazenaStorage", container.ExecOptions{ Cmd: []string{"sh", "-c", command}, AttachStdout: true, @@ -233,7 +236,7 @@ func scale(Config cfg.Config, serviceId string, up bool)bool{ return true; } -func createStorageContainer(Config cfg.Config, DockerClient *client.Client){ +func createStorageContainer(Config cfg.Config, DockerClient *client.Client, sshSkPem string, sshHostPkPem string){ authConfig := registry.AuthConfig{ Username: Config.RegistryAuth.Username, Password: Config.RegistryAuth.Password, @@ -285,15 +288,17 @@ func createStorageContainer(Config cfg.Config, DockerClient *client.Client){ } - reader, err := os.Open("./ssh-key.tar"); + var buf bytes.Buffer; + tw := tar.NewWriter(&buf); - if err != nil { - panic("Failed To open ssh key file for reading!"+err.Error()); - } + addToTar(tw, "ssh-key", sshSkPem); + addToTar(tw, "expected-host-key", "[tasks.BlazenaHelper]:2222 "+ sshHostPkPem); + tw.Close(); + if err != nil {panic("The british are comming!")} - defer reader.Close(); - - err = DockerClient.CopyToContainer(context.Background(), "BlazenaStorage", "/", reader, container.CopyToContainerOptions{ + os.WriteFile("/tmp/test", buf.Bytes(), os.ModeAppend); + + err = DockerClient.CopyToContainer(context.Background(), "BlazenaStorage", "/", &buf, container.CopyToContainerOptions{ AllowOverwriteDirWithFile: true, }); if err != nil { @@ -321,3 +326,67 @@ func shutdown(Config cfg.Config)bool{ return true; } + +func exchangeKeys(Config cfg.Config, sshKeyPem string)string{ + var body struct{ + SshPkPem string `json:"sshPkPem"` + } = struct{SshPkPem string `json:"sshPkPem"`}{ + SshPkPem: sshKeyPem, + }; + + bodyEncoded, err := json.Marshal(body); + + if err != nil { + panic("Failed to marshal body."+ err.Error()); + } + + rq, err := http.NewRequest("POST", Config.DockerManagerBaseUrl + "/keys", bytes.NewBuffer(bodyEncoded)); + + if err != nil{ + panic("Failed to create http request"+ err.Error()); + } + + rq.Header.Set("Authorization", "Bearer "+ token); + rq.Close = true; + rs, err := http.DefaultClient.Do(rq); + + if err != nil{ + panic("Failed to send http request"+ err.Error()); + } + + defer rs.Body.Close(); + + rsBodyRaw, err := io.ReadAll(rs.Body); + + if err != nil{ + panic("Failed to read response's body!"+err.Error()); + } + + var rsBody struct{ + HostPkPem string `json:"hostPkPem"` + }; + + err = json.Unmarshal(rsBodyRaw, &rsBody); + if err != nil{ + panic("Failed to unmarshal rsBodyRaw!"+ err.Error()); + } + + + return rsBody.HostPkPem; +} + +func addToTar(tw *tar.Writer, filename string, content string) error{ + hdr := &tar.Header{ + Name: filename, + Mode: 0600, + Size: int64(len([]byte(content))), + }; + + if err := tw.WriteHeader(hdr); err != nil{ + return err; + } + + _, err := tw.Write([]byte(content)) + + return err; +} diff --git a/docker/ssh.go b/shared/ssh.go similarity index 57% rename from docker/ssh.go rename to shared/ssh.go index 4336fb6..568f186 100644 --- a/docker/ssh.go +++ b/shared/ssh.go @@ -1,34 +1,30 @@ -package docker +package shared import ( "crypto/ed25519" "crypto/rand" - "crypto/x509" "encoding/pem" "golang.org/x/crypto/ssh" ) type Keypair struct { - public []byte - private []byte + Public string + Private string } -func generateKeypair() Keypair { +func GenerateSSHKeypair() Keypair { publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader) if err != nil { panic(err) } - privBytes, err := x509.MarshalPKCS8PrivateKey(privateKey) + privBlock, err := ssh.MarshalPrivateKey(privateKey, "") if err != nil { panic(err) } - privPem := pem.EncodeToMemory(&pem.Block{ - Type: "PRIVATE KEY", - Bytes: privBytes, - }) + privPem := pem.EncodeToMemory(privBlock) sshPubKey, err := ssh.NewPublicKey(publicKey) if err != nil { @@ -38,8 +34,7 @@ func generateKeypair() Keypair { pubBytes := ssh.MarshalAuthorizedKey(sshPubKey) return Keypair{ - private: privPem, - public: pubBytes, - }; - + Private: string(privPem), + Public: string(pubBytes), + } }