From e76d9c9531a19ff389fd060f1fa120c99216d559 Mon Sep 17 00:00:00 2001 From: Josh Ashton Date: Fri, 2 Aug 2024 15:59:43 -0600 Subject: [PATCH] subdomains enabled, implemented go-routines, connected to portfolio, etc. heading into some major refactoring of the codebase, documentation, and installation/update scripts to enable rapid development. --- backend/src/security-layer/container/go.mod | 6 + backend/src/security-layer/container/go.sum | 12 + backend/src/security-layer/container/main.go | 205 --------- .../src/security-layer/container/router.go | 407 ++++++++++++++++++ backend/src/security-layer/init | 6 +- 5 files changed, 428 insertions(+), 208 deletions(-) delete mode 100644 backend/src/security-layer/container/main.go create mode 100644 backend/src/security-layer/container/router.go diff --git a/backend/src/security-layer/container/go.mod b/backend/src/security-layer/container/go.mod index e24dee7..19b6913 100644 --- a/backend/src/security-layer/container/go.mod +++ b/backend/src/security-layer/container/go.mod @@ -9,6 +9,7 @@ require ( github.com/bytedance/sonic/loader v0.1.1 // indirect github.com/cloudwego/base64x v0.1.4 // indirect github.com/cloudwego/iasm v0.2.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/gin-contrib/sse v0.1.0 // indirect github.com/go-playground/locales v0.14.1 // indirect @@ -16,6 +17,11 @@ require ( github.com/go-playground/validator/v10 v10.20.0 // indirect github.com/goccy/go-json v0.10.2 // indirect github.com/golang-jwt/jwt/v5 v5.2.1 // indirect + github.com/gorilla/handlers v1.5.2 // indirect + github.com/gorilla/mux v1.8.1 // indirect + github.com/gorilla/securecookie v1.1.2 // indirect + github.com/gorilla/sessions v1.3.0 // indirect + github.com/gorilla/websocket v1.5.3 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.2.7 // indirect github.com/leodido/go-urn v1.4.0 // indirect diff --git a/backend/src/security-layer/container/go.sum b/backend/src/security-layer/container/go.sum index a2282f5..283b558 100644 --- a/backend/src/security-layer/container/go.sum +++ b/backend/src/security-layer/container/go.sum @@ -8,6 +8,8 @@ github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= @@ -25,6 +27,16 @@ github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MG github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE= +github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= +github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= +github.com/gorilla/sessions v1.3.0 h1:XYlkq7KcpOB2ZhHBPv5WpjMIxrQosiZanfoy1HLZFzg= +github.com/gorilla/sessions v1.3.0/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= diff --git a/backend/src/security-layer/container/main.go b/backend/src/security-layer/container/main.go deleted file mode 100644 index 4122d45..0000000 --- a/backend/src/security-layer/container/main.go +++ /dev/null @@ -1,205 +0,0 @@ -package main - -import ( - "encoding/base64" - "encoding/hex" - "fmt" - "io" - "net/http" - - "crypto/aes" - "crypto/cipher" - "crypto/rand" - "crypto/sha1" - - "errors" - "strconv" - "strings" - - "github.com/gin-gonic/gin" -) - -type Communication struct { - Communication string `json:"communication"` - Hash string `json:"hash"` -} - -var requests []Communication - -// TODO: Test if 256 bit key works. -// TODO: Dynamically get key? Need to research best way to store private keys between two devices. -var key = []byte("passphrasewhichneedstobe32bytes!") - -func hash(text string) string { - hasher := sha1.New() - hasher.Write([]byte(text)) - return base64.URLEncoding.EncodeToString(hasher.Sum(nil)) -} - -func encrypt(gc *gin.Context) { - var newCommunication Communication - if err := gc.BindJSON(&newCommunication); err != nil { - gc.IndentedJSON(http.StatusBadRequest, gin.H{"message": err}) - return - } - - text := []byte(newCommunication.Communication) - - c, err := aes.NewCipher(key) - if err != nil { - gc.IndentedJSON(http.StatusBadRequest, gin.H{"message": err}) - } - - gcm, err := cipher.NewGCM(c) - if err != nil { - gc.IndentedJSON(http.StatusBadRequest, gin.H{"message": err}) - } - - nonce := make([]byte, gcm.NonceSize()) - if _, err = io.ReadFull(rand.Reader, nonce); err != nil { - gc.IndentedJSON(http.StatusBadRequest, gin.H{"message": err}) - } - - var b []byte = gcm.Seal(nonce, nonce, text, nil) - hex, err := convertBytesToHex(b) - if(err != nil) { - fmt.Println(err) - } - var test string = "{data: " + hex + ", hash: " + hash(string(text)) + "}" - - gc.IndentedJSON(http.StatusCreated, gin.H{"message": test}) -} - -func convertBytesToHex(b []byte) (string, error) { - // Handle nil pointer case - if b == nil { - return "", errors.New("nil pointer provided for hex string") - } - - // Split the hex string by spaces - var h string = hex.EncodeToString(b) - var builder strings.Builder - for i := 0; i < len(h); i += 2 { - end := i + 2 - if end > len(h) { - end = len(h) - } - chunk := h[i:end] - builder.WriteString(chunk) - if end < len(h) { - builder.WriteString(" ") - } - } - - // Return the slice of uint8 and any errors encountered - return builder.String(), nil -} - -func convertHexToBytes(hexString *string) ([]uint8, error) { - // Handle nil pointer case - if hexString == nil { - return nil, errors.New("nil pointer provided for hex string") - } - - // Split the hex string by spaces - hexBytes := strings.Fields(*hexString) - - // Initialize an empty slice for uint8 - data := make([]uint8, len(hexBytes)) - - // Iterate and convert each hex byte - for i, hexByte := range hexBytes { - // Convert each hex string to a uint8 value (handling errors) - value, err := strconv.ParseUint(hexByte, 16, 8) - if err != nil { - return nil, fmt.Errorf("error parsing hex byte '%s': %w", hexByte, err) - } - - // Assign the converted value to the slice - data[i] = uint8(value) - } - - // Return the slice of uint8 and any errors encountered - return data, nil -} - -func decrypt(input *Communication) bool { - //ciphertext, err := ioutil.ReadFile("myfile") - ciphertext, err := convertHexToBytes(&input.Communication) - - // if our program was unable to read the file - // print out the reason why it can't - if err != nil { - fmt.Println(err) - } - - c, err := aes.NewCipher(key) - if err != nil { - fmt.Println(err) - } - - gcm, err := cipher.NewGCM(c) - if err != nil { - fmt.Println(err) - } - - nonceSize := gcm.NonceSize() - if len(ciphertext) < nonceSize { - fmt.Println(err) - } - - nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:] - plaintext, err := gcm.Open(nil, nonce, ciphertext, nil) - if err != nil { - fmt.Println(err) - } - s := string(plaintext) - if validateHash(s, input.Hash) { - input.Communication = s - return true - } - input.Communication = "DATA CORRUPTED OR TAMPERED" - return false -} - -// Validate there's no tampering with SHA-1 sum. The decrypted hash and the transmitted hash should be identical. -func validateHash(decrypted string, hash string) bool { - // Calculate the SHA256 sum of the decrypted request - hasher := sha1.New() - hasher.Write([]byte(decrypted)) - decryptedHashString := base64.URLEncoding.EncodeToString(hasher.Sum(nil)) - - // TODO: Add error handling if hash doesn't match. - return decryptedHashString == hash -} - -// User HTTP GET request has the prompt decrypted and verified with a SHA-1 checksum. -// If there's been no data corruption, re-construct user prompt for ollama -// TODO: Eventually, add additional logic that will allow for re-direction and contextual awareness (pre-processing) -func request(c *gin.Context) { - var newInput Communication - - //Call BindJSON to bind the received JSON to - if err := c.BindJSON(&newInput); err != nil { - return - } - - if decrypt(&newInput) { - requests = append(requests, newInput) - c.IndentedJSON(http.StatusCreated, newInput) // TODO: Formulate prompt and transfer to AI layer to formulate new JSON response back - } else { - c.IndentedJSON(http.StatusBadRequest, gin.H{"message": "Failed checksum. Presumably data corrupted."}) - - } -} - -func main() { - router := gin.Default() - router.POST("/request", request) - router.POST("/encrypt", encrypt) - - err := router.Run("0.0.0.0:8000") - if err != nil { - return - } -} diff --git a/backend/src/security-layer/container/router.go b/backend/src/security-layer/container/router.go new file mode 100644 index 0000000..2138836 --- /dev/null +++ b/backend/src/security-layer/container/router.go @@ -0,0 +1,407 @@ +package main + +import ( + "context" + "crypto/aes" + "crypto/cipher" + "sync" + + "crypto/rand" + "crypto/sha1" + "encoding/base64" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + + "io" + "log" + "net/http" + + "os" + "os/signal" + "strconv" + "strings" + "time" + + "github.com/gorilla/mux" +) + +type Communication struct { + Communication string `json:"communication"` + Hash string `json:"hash"` +} + +type Response struct { + Model string `json:"model"` + CreatedAt string `json:"created_at"` + Response string `json:"response"` + Done bool `json:"done"` + DoneReason string `json:"done_reason"` + Context []int `json:"context"` + TotalDuration int `json:"total_duration"` + LoadDuration int `json:"load_duration"` + PromptEC int `json:"prompt_eval_count"` + PromptED int `json:"prompt_eval_duration"` + EvalCount int `json:"eval_count"` + EvalDuration int `json:"eval_duration"` +} + +// Chats are updated once the content is decrypted. Since this is never leaving +// the server once decrypted, and possibly will not be saved (depending on user +// settings), it will remain decrypted until it's time to transform into a +// Communication JSON object, which is the HTTP response that is re-encrypted. + +// TODO: If the user has enabled conversation history, then save the encrypted +// chats to the server's drive. Additionally, only accept incoming additional +// messages from the user, rather than having the client re-send Chats already +// stored on the server. +type Chat struct { + Role bool `json:"role"` + Content string `json:"content"` +} + +type PromptWHistory struct { + Model string `json:"model"` + Messages []Chat `json:"messages"` + Stream bool `json:"stream"` +} + +type ResponseWHistory struct { + Model string `json:"model"` + CreatedAt string `json:"created_at"` + Message Chat `json:"message"` + Done bool `json:"done"` + DoneReason string `json:"done_reason"` + Context []int `json:"context"` + TotalDuration int `json:"total_duration"` + LoadDuration int `json:"load_duration"` + PromptEC int `json:"prompt_eval_count"` + PromptED int `json:"prompt_eval_duration"` + EvalCount int `json:"eval_count"` + EvalDuration int `json:"eval_duration"` +} + +var chats []Chat + +// TODO: Test if 256 bit key works. +// TODO: Dynamically get key? Need to research best way to store private keys between two devices. +var key = []byte("passphrasewhichneedstobe32bytes!") + +func hash(text string) string { + hasher := sha1.New() + hasher.Write([]byte(text)) + return base64.URLEncoding.EncodeToString(hasher.Sum(nil)) +} + +func encrypt() { + var newCommunication Communication + + text := []byte(newCommunication.Communication) + + c, err := aes.NewCipher(key) + if err != nil { + return + //gc.IndentedJSON(http.StatusBadRequest, gin.H{"message": err}) + } + + gcm, err := cipher.NewGCM(c) + if err != nil { + return + //gc.IndentedJSON(http.StatusBadRequest, gin.H{"message": err}) + } + + nonce := make([]byte, gcm.NonceSize()) + if _, err = io.ReadFull(rand.Reader, nonce); err != nil { + return + //gc.IndentedJSON(http.StatusBadRequest, gin.H{"message": err}) + } + + var b []byte = gcm.Seal(nonce, nonce, text, nil) + hex, err := convertBytesToHex(b) + if err != nil { + fmt.Println(err) + } + var test string = "{data: " + hex + ", hash: " + hash(string(text)) + "}" + log.Println("encrypted string:" + test) + //gc.IndentedJSON(http.StatusCreated, gin.H{"message": test}) +} + +func convertBytesToHex(b []byte) (string, error) { + // Handle nil pointer case + if b == nil { + return "", errors.New("nil pointer provided for hex string") + } + + // Split the hex string by spaces + var h string = hex.EncodeToString(b) + var builder strings.Builder + for i := 0; i < len(h); i += 2 { + end := i + 2 + if end > len(h) { + end = len(h) + } + chunk := h[i:end] + builder.WriteString(chunk) + if end < len(h) { + builder.WriteString(" ") + } + } + + // Return the slice of uint8 and any errors encountered + return builder.String(), nil +} + +func convertHexToBytes(hexString *string) ([]uint8, error) { + // Handle nil pointer case + if hexString == nil { + return nil, errors.New("nil pointer provided for hex string") + } + + // Split the hex string by spaces + hexBytes := strings.Fields(*hexString) + + // Initialize an empty slice for uint8 + data := make([]uint8, len(hexBytes)) + + // Iterate and convert each hex byte + for i, hexByte := range hexBytes { + // Convert each hex string to a uint8 value (handling errors) + value, err := strconv.ParseUint(hexByte, 16, 8) + if err != nil { + return nil, fmt.Errorf("error parsing hex byte '%s': %w", hexByte, err) + } + + // Assign the converted value to the slice + data[i] = uint8(value) + } + + // Return the slice of uint8 and any errors encountered + return data, nil +} + +func decrypt(input *Communication, output *Chat) bool { + return true // TODO: Add testing flag for easier manipulation. + /* + //ciphertext, err := ioutil.ReadFile("myfile") + ciphertext, err := convertHexToBytes(&input.Communication) + + // if our program was unable to read the file + // print out the reason why it can't + if err != nil { + fmt.Println(err) + } + + c, err := aes.NewCipher(key) + if err != nil { + fmt.Println(err) + } + + gcm, err := cipher.NewGCM(c) + if err != nil { + fmt.Println(err) + } + + nonceSize := gcm.NonceSize() + if len(ciphertext) < nonceSize { + fmt.Println(err) + } + + nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:] + plaintext, err := gcm.Open(nil, nonce, ciphertext, nil) + if err != nil { + fmt.Println(err) + } + s := string(plaintext) + if validateHash(s, input.Hash) { + output.Content = s + return true + } + input.Communication = "DATA CORRUPTED OR TAMPERED" + return false + */ +} + +// Validate there's no tampering with SHA-1 sum. The decrypted hash and the transmitted hash should be identical. +func validateHash(decrypted string, hash string) bool { + // Calculate the SHA256 sum of the decrypted request + hasher := sha1.New() + hasher.Write([]byte(decrypted)) + decryptedHashString := base64.URLEncoding.EncodeToString(hasher.Sum(nil)) + + // TODO: Add error handling if hash doesn't match. + return decryptedHashString == hash +} + +// User HTTP GET request has the prompt decrypted and verified with a SHA-1 checksum. +// If there's been no data corruption, re-construct user prompt for ollama +// TODO: Eventually, add additional logic that will allow for re-direction and contextual awareness (pre-processing) +func request(w http.ResponseWriter, r *http.Request) { + model := "qwen:0.5b" // qwen:0.5b used for testing while hosting from my laptop. llama3 seems to be the best to use normally. + + input, err := io.ReadAll(r.Body) + if err != nil { + return + } + + defer r.Body.Close() + prompt := Communication{} + json.Unmarshal(input, &prompt) + + newChat := Chat{} + newChat.Role = true + newChat.Content = prompt.Communication + + if decrypt(&prompt, &newChat) { + chats = append(chats, newChat) + + apiCall := PromptWHistory{} + apiCall.Model = model + apiCall.Messages = chats + apiCall.Stream = false // TODO: Allow for this as a user setting... + log.Print("Pre-JSON ification: ") + log.Println(apiCall) + + j, err := json.Marshal(apiCall) + buf := strings.NewReader(string(j)) + log.Print("Post-JSON ification: ") + log.Println(string(j)) + + // Port 11434 corresponds to Ollama. Direct access to Ollama is restricted. + resp, err := http.Post("http://localhost:11434/api/chat", "application/json", buf) + if err != nil { + return + } + + output, err := io.ReadAll(resp.Body) + if err != nil { + return + } + + defer r.Body.Close() + response := ResponseWHistory{} + json.Unmarshal(output, &response) + + log.Println(response) + + } else { + http.Error(w, "Decryption/Hash failed.", 401) + return + } +} + +func chat(w http.ResponseWriter, r *http.Request) { + input, err := io.ReadAll(r.Body) + if err != nil { + return + } + + defer r.Body.Close() + prompt := Communication{} + json.Unmarshal(input, &prompt) + + // TODO: Add support for dynamically changing models. + model := "qwen:0.5b" // qwen:0.5b used for testing while hosting from my laptop. llama3 seems to be the best to use normally. + + apicall := "{\"model\": \"" + apicall = apicall + model + apicall = apicall + "\", \"prompt\": \"" + apicall = apicall + prompt.Communication + apicall = apicall + "\", \"stream\": false }" + + buf := strings.NewReader(apicall) + + // Port 11434 corresponds to Ollama. Direct access to Ollama is restricted. + resp, err := http.Post("http://localhost:11434/api/generate", "application/json", buf) + if err != nil { + return + } + + output, err := io.ReadAll(resp.Body) + if err != nil { + return + } + defer resp.Body.Close() + + defer r.Body.Close() + response := Response{} + json.Unmarshal(output, &response) + + fmt.Println(response.Response) // Print the body as a string + // TODO: Encrypt response.Response and send back as a Communication JSON object. + //c.IndentedJSON(http.StatusCreated, resp) +} + +func main() { + var wg sync.WaitGroup + + r := mux.NewRouter() + portfolioDir := "/home/violet/documents/development/portfolio/public_html/" + r.PathPrefix("/").Handler(http.FileServer(http.Dir(portfolioDir))) + + srv := &http.Server{ + Addr: "0.0.0.0:8080", + // Good practice to set timeouts to avoid Slowloris attacks. + WriteTimeout: time.Second * 15, + ReadTimeout: time.Second * 15, + IdleTimeout: time.Second * 60, + Handler: r, // Pass our instance of gorilla/mux in. + } + + // Run our server in a goroutine so that it doesn't block. + wg.Add(1) + go func() { + defer wg.Done() + if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Println(err) + } + }() + + log.Println("Portfolio server is running on port 8080") + + // Routing for AI web app and API + apiR := r.Host("ai.joshashton.dev").Subrouter() + apiR.HandleFunc("/request", chat) + apiR.PathPrefix("/").Handler(http.FileServer(http.Dir("/home/violet/templates/construction/"))) + + apiSrv := &http.Server{ + Addr: "0.0.0.0:8081", + // Good practice to set timeouts to avoid Slowloris attacks. + WriteTimeout: time.Second * 15, + ReadTimeout: time.Second * 15, + IdleTimeout: time.Second * 60, + Handler: apiR, // Pass our instance of gorilla/mux in. + } + + // Run our server in a goroutine so that it doesn't block. + wg.Add(1) + go func() { + defer wg.Done() + if err := apiSrv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Println(err) + } + }() + + log.Println("API server is running on port 8081") + + c := make(chan os.Signal, 1) + // We'll accept graceful shutdowns when quit via SIGINT (Ctrl+Shift+C) + signal.Notify(c, os.Interrupt) + + // Block until we receive our signal. + <-c + + // Create a deadline to wait for. + ctx, cancel := context.WithTimeout(context.Background(), time.Duration(30)) + defer cancel() + // Doesn't block if no connections, but will otherwise wait + // until the timeout deadline. + srv.Shutdown(ctx) + apiSrv.Shutdown(ctx) + // Optionally, you could run srv.Shutdown in a goroutine and block on + // <-ctx.Done() if your application should wait for other services + // to finalize based on context cancellation. + log.Println("shutting down") + wg.Wait() + os.Exit(0) +} diff --git a/backend/src/security-layer/init b/backend/src/security-layer/init index 4965bb4..cc09fd5 100755 --- a/backend/src/security-layer/init +++ b/backend/src/security-layer/init @@ -1,9 +1,9 @@ #!/bin/zsh # Force delete the old container and re-initialize the container. -docker rm -f security-layer +sudo docker rm -f security-layer # Build the Docker image. -docker build --tag security-layer:latest . +sudo docker build --tag security-layer:latest . # Create a Docker container from the image and connect host port 8000 to container port 8000. -docker run --name security-layer -d -p 8000:8000 security-layer:latest +sudo docker run --name security-layer -d -p 8000:8000 security-layer:latest