From 897bb3de010bbaf1cb0700682ee5c02fd9666dcb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9A=D0=B0=D0=BB=D0=B8=D0=BD=D0=B8=D0=BD=20=D0=A1=D0=B5?= =?UTF-8?q?=D1=80=D0=B3=D0=B5=D0=B9=20=D0=92=D0=B0=D0=BB=D0=B5=D1=80=D1=8C?= =?UTF-8?q?=D0=B5=D0=B2=D0=B8=D1=87?= Date: Thu, 17 Oct 2024 15:04:35 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B0=20=D0=BF=D0=BE=D0=B4=D0=B4=D0=B5=D1=80=D0=B6=D0=BA?= =?UTF-8?q?=D0=B0=20wrap?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 9 ++-- docker-compose.yml | 49 ++++++++++++++--- entrypoint.sh | 2 +- vault.go | 127 ++++++++++++++++++++++++++++++++++++++------- 4 files changed, 159 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 91e2f95..d9ba7e1 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Vault Wrap/Unwrap -ВЭБ-интерфейс к сервису vault/unwrap для безопасной передачи секретов. Также генератор паролей. +ВЭБ-интерфейс к сервису Hashicorp Vault wrap/unwrap для безопасной передачи секретов. Также генератор паролей. ## Запуск @@ -19,18 +19,21 @@ vault-wrap -action-address "http://saecret.example.ru:8080" -vault-url "https:// Если сервис запущен за обратным прокси, то в качестве адреса сервиса -action-address требуется указать адрес прокси, т.е. адрес (FQDN) на который будут приходить запросы. +Для работы шифрования (wrap) нужен vault токен с доступом к vault wrap/unwrap. Токен задается через переменную окружения VAULT_TOKEN. + ## Ключи командной строки - action-address string - Адрес данного сервиса (https://secret.example.ru). Адрес который будет подставляться в форму в html-шаблон - debug - Вывод отладочных сообщений - listen-port string - Номер порта сервиса (default "8080") - log-file string - Путь до лог-файла (default "vault-unwrap.log") + - max-text-length - Максимальная длина текста для шифрования и длина пароля для генератора (default 100) - template-dir string -Каталог с шаблонами (default "html-template") - template-file string Файл-шаблон для ВЭБ-странцы (default "index.html") - tls - Использовать SSL/TLS - tls-cert string - TLS сертификат (полный путь к файлу) - tls-key string - TLS ключ (полный путь к файлу) - - vault-url string - Адрес сервера Hashicorp Vault (https://host.name:8200) + - token-ttl - Время жизни wrap-токена в секундах (default "3600") + - vault-url string - Адрес сервера Hashicorp Vault (https://host.name:8200) - help - Вывод справочной информации - diff --git a/docker-compose.yml b/docker-compose.yml index 5bf3c11..a4a2312 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,10 +8,13 @@ services: environment: - ACTION_ADDRESS=${ACTION_ADDRESS:-https://secret.example.ru} - VAULT_ADDRESS=${VAULT_ADDRESS} + - VAULT_TOKEN=${WRAP_TOKEN} - LISTEN_PORT=8080 - TLS_KEY_FILE=${TLS_KEY_FILE} - TLS_CERT_FILE=${TLS_CERT_FILE} - TZ=Europe/Moscow + - MAX_TEXT_LENGTH=${MAX_TEXT_LENGTH:-100} + - TOKEN_TTL=${TOKEN_TTL:-3600} restart: always # ports: # - 1234:8080 @@ -26,16 +29,48 @@ services: max-size: "10m" max-file: "5" labels: - - "tra.enable=true" - - "tra.http.routers.secret.rule=Host(`secret.example.ru`)" - - "tra.http.services.secret.loadbalancer.server.port=8080" - - "tra.docker.network=reverse-proxy" - - "tra.http.routers.secret.tls=true" - - "tra.http.services.secret.loadbalancer.server.scheme=http" + - "traefik.enable=true" + - "traefik.http.routers.secret.rule=Host(`secret.example.ru`)" + - "traefik.http.services.secret.loadbalancer.server.port=8080" + - "traefik.docker.network=reverse-proxy" + - "traefik.http.routers.secret.tls=true" + - "traefik.http.services.secret.loadbalancer.server.scheme=http" networks: - default - vault-wrap + traefik: + image: traefik:v3.0 + container_name: traefik + command: +# - --entrypoints.web.address=:80 +# - --entrypoints.web-secure.address=:443 +# - --providers.docker=true + - --providers.file.directory=/configuration/ + - --providers.file.watch=true + volumes: + - traefik-dynamic-conf:/configuration/ + - /home/gitlab-runner/traefik/traefik.yml:/traefik.yml:ro + - /var/run/docker.sock:/var/run/docker.sock:ro + - traefik-ssl:/ssl/:ro + ports: + - 80:80 + # - 8080:8080 + - 888:888 + - 443:443 + restart: always + networks: + - default + labels: + - "traefik.enable=true" + - "traefik.http.routers.traefik.entrypoints=https" + - "traefik.http.routers.traefik.rule=Host(`somehost.example.ru`)" + - "traefik.http.routers.traefik.tls=true" +# - "traefik.http.routers.traefik.tls.certresolver=letsEncrypt" + - "traefik.http.routers.traefik.service=api@internal" + - "traefik.http.services.traefik.loadbalancer.server.port=888" + - "traefik.http.services.traefik.loadbalancer.server.scheme=https" + networks: default: name: reverse-proxy @@ -46,3 +81,5 @@ networks: volumes: vault-wrap-log: vault-wrap-conf: + traefik-dynamic-conf: + traefik-ssl: diff --git a/entrypoint.sh b/entrypoint.sh index 7d35d85..d0e989b 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -3,6 +3,6 @@ set -u while true ;do # /go/bin/vault-wrap -action-address "${ACTION_ADDRESS}" -vault-url "${VAULT_ADDRESS}" -tls-cert "/usr/local/share/vault-wrap/${TLS_CERT_FILE}" -tls-key "/usr/local/share/vault-wrap/${TLS_KEY_FILE}" -template-dir /usr/local/share/vault-wrap -log-file /var/log/vault-wrap/vault-wrap.log -listen-port "${LISTEN_PORT}" -tls - /go/bin/vault-wrap -action-address "${ACTION_ADDRESS}" -template-dir /usr/local/share/vault-wrap -log-file /var/log/vault-wrap/vault-wrap.log + /go/bin/vault-wrap -action-address "${ACTION_ADDRESS}" -template-dir /usr/local/share/vault-wrap -log-file /var/log/vault-wrap/vault-wrap.log -token-ttl ${TOKEN_TTL} sleep 120 done diff --git a/vault.go b/vault.go index 1bc0c17..8de6719 100644 --- a/vault.go +++ b/vault.go @@ -1,3 +1,10 @@ +// ------------------------------------------- +// Hashicorp Vault wrap/unwrap web service +// Distributed under GNU Public License +// Author: Sergey Kalinin svk@nuk-svk.ru +// Home page: https://nuk-svk.ru https://git.nuk-svk.ru +// ------------------------------------------- + package main import ( @@ -10,6 +17,8 @@ import ( "regexp" "bytes" "strconv" + "strings" + "time" "encoding/json" "crypto/tls" "net/http" @@ -41,15 +50,19 @@ var ( TemplateFile string ActionAddress string VaultAddress string + VaultToken string Data string ListenPort string TlsEnable bool TlsCertFile string TlsKeyFile string + MaxTextLength int + TokenTTL string ) type TemplateData struct { URL string TEXT string + MAXTEXTLENGTH int } type UnwrappedData struct { Rerquest_id string `json: "request_id"` @@ -57,24 +70,41 @@ type UnwrappedData struct { Renewable bool `json: "renewable"` Lease_daration int `json:"lease_duration"` Data map[string]string `json: "data"` - Wrap_info string `json: "wrap_info"` + Wrap_info *WrapInfo `json: "wrap_info"` Warnings string `json: "warnings"` Auth string `json: "auth"` Mount_type string `json: "mount_type"` - Error string `json: "errors"` + Errors string `json: "errors"` } -func vaultDataWrap(vaultAddr string, vaultToken string, vaultSecretName string) string { +type WrapInfo struct { + Token string `json:"token"` + Ttl int `json:"ttl"` + CreationTime time.Time `json:"creation_time"` + CreationPath string `json:"creation_path"` +} +// const MaxTextLength = 100 + +func vaultDataWrap(vaultAddr string, text string) string { + secret := make(map[string]string) + secret["wrapped_data"] = text + + postBody, err := json.Marshal(secret) + if err != nil { + log.Println("Errored marshaling the text:", text) + } + customTransport := &(*http.DefaultTransport.(*http.Transport)) customTransport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} client := &http.Client{Transport: customTransport} // client := &http.Client{} - req, _ := http.NewRequest("POST", vaultAddr, nil) + req, _ := http.NewRequest("POST", vaultAddr, bytes.NewBuffer(postBody)) req.Header.Add("Accept", "application/json") - req.Header.Add("X-Vault-Token", vaultToken) + req.Header.Add("X-Vault-Token", VaultToken) + req.Header.Add("X-Vault-Wrap-TTL", TokenTTL) resp, err := client.Do(req) @@ -82,11 +112,16 @@ func vaultDataWrap(vaultAddr string, vaultToken string, vaultSecretName string) log.Println("Errored when sending request to the Vault server") } - var result map[string]interface{} + var result UnwrappedData json.NewDecoder(resp.Body).Decode(&result) - secret := result["data"].(map[string]interface{})["data"].(map[string]interface{})[vaultSecretName] - log.Println(result) - return fmt.Sprint(secret) + // wrappedToken := result.Data + if Debug { + log.Println(resp) + log.Println("result", result) + log.Println("token", result.Wrap_info.Token) + } + + return result.Wrap_info.Token } func vaultDataUnWrap(vaultAddr string, vaultWrapToken string) map[string]string { @@ -115,9 +150,13 @@ func vaultDataUnWrap(vaultAddr string, vaultWrapToken string) map[string]string json.NewDecoder(resp.Body).Decode(&result) secret := result.Data if Debug { - log.Println(result) - log.Println(secret) + log.Println("result", result) + log.Println("secret", secret) } + // log.Println("Length=", len(secret)) + // if len(secret) == 0 { + // log.Println("Error:", result.Errors) + // } // fmt.Sprint(secret) // for v, k := range secret { // log.Println(k, v) @@ -151,8 +190,8 @@ func getStaticPage(w http.ResponseWriter, r *http.Request) { // templateData.UUID = uuid templateData.URL = ActionAddress - templateData.TEXT = Data + templateData.MAXTEXTLENGTH = MaxTextLength // templateData.URL = FishingUrl + "/" + arrUsers[i].messageUUID if body, err := ParseTemplate(template, templateData); err == nil { @@ -163,7 +202,7 @@ func getStaticPage(w http.ResponseWriter, r *http.Request) { // hvs.CAES - 95 // s.Dj7kZS - 26 -func getDataFromHtmlForm(w http.ResponseWriter, r *http.Request) { +func unwrapDataFromHtmlForm(w http.ResponseWriter, r *http.Request) { r.ParseForm() token := r.FormValue("input_token") @@ -173,7 +212,7 @@ func getDataFromHtmlForm(w http.ResponseWriter, r *http.Request) { // fmt.Fprintln(w, r.URL.RawQuery) // log.Println(w, r.URL.RawQuery) if Debug { - log.Printf("Текст для расшифровки: %s ", token) + log.Printf("Текст для шифровки: %s ", token) log.Printf("Адрес сервера Hashicorp Vault: %s ", vaultPath) } // Проверка текста на соответствие шаблону @@ -181,15 +220,26 @@ func getDataFromHtmlForm(w http.ResponseWriter, r *http.Request) { if Debug { fmt.Println(re.Match([]byte(token))) } + + token = strings.ReplaceAll(token, "\r", "") + token = strings.ReplaceAll(token, "\n", "") + token = strings.ReplaceAll(token, " ", "") + token = strings.TrimSpace(token) + if token != "" && re.Match([]byte(token)) { b := new(bytes.Buffer) for key, value := range vaultDataUnWrap(vaultPath, token) { fmt.Fprintf(b, "%s: %s\n", key, value) } Data = b.String() + if Data == "" { + Data = "Ошибка! Токен не найден." + } if Debug { log.Println(Data) } + } else if token != "" { + Data = "Введенные данные не соответствуют формату. Введите корректный токен." } getStaticPage(w, r) // http.Redirect(w, r, "http://"+r.Host, http.StatusMovedPermanently) @@ -209,7 +259,7 @@ func genPassword(w http.ResponseWriter, r *http.Request) { } // w.Write([]byte("Длина пароля " + passLength + "/n")) passwordLength, err := strconv.Atoi(passLength) - if passwordLength > 1024 { + if passwordLength > MaxTextLength { log.Printf("Oversized password length") Data = "Превышена длина пароля" getStaticPage(w, r) @@ -242,6 +292,33 @@ func genPasswordDefault(w http.ResponseWriter, r *http.Request) { getStaticPage(w, r) } +func wrapDataFromHtmlForm(w http.ResponseWriter, r *http.Request) { + r.ParseForm() + secret := r.FormValue("input_token") + if len([]rune(secret)) > MaxTextLength { + log.Println("Длина текста превышает заданные ограничения:", MaxTextLength) + Data = fmt.Sprintf("Длина текста превышает заданные ограничения: %s > %s", strconv.Itoa(len([]rune(secret))), strconv.Itoa(MaxTextLength)) + } else { + vaultPath := VaultAddress + "/v1/sys/wrapping/wrap" + Data = "" + if Debug { + fmt.Println("Введен текст:", secret) + } + if secret != "" { + Data = vaultDataWrap(vaultPath, secret) + if Data == "" { + Data = "Ошибка! Токен не найден." + } + if Debug { + log.Println(Data) + } + } else if secret != "" { + Data = "Введите текст для шифровки." + } + } + getStaticPage(w, r) +} + func main() { var ( logFile string @@ -256,6 +333,8 @@ func main() { flag.StringVar(&TlsCertFile, "tls-cert", "", "TLS сертификат (файл)") flag.StringVar(&TlsKeyFile, "tls-key", "", "TLS ключ (файл)") flag.BoolVar(&TlsEnable, "tls", false, "Использовать SSL/TLS") + flag.IntVar(&MaxTextLength, "max-text-length", 100 , "Максимальная длина текста для шифрования и длина пароля для генератора") + flag.StringVar(&TokenTTL, "token-ttl", "3600", "Время жизни wrap-токена в секундах") flag.Parse() @@ -281,17 +360,30 @@ func main() { VaultAddress = os.Getenv("VAULT_ADDRESS") } + if os.Getenv("VAULT_TOKEN") == "" && VaultToken == "" { + log.Println("Send error: make sure environment variables `VAULT_TOKEN` was set") + } else if os.Getenv("VAULT_TOKEN") != "" && VaultToken == "" { + VaultToken = os.Getenv("VAULT_TOKEN") + } + + if os.Getenv("MAX_TEXT_LENGTH") != "" && MaxTextLength == 100 { + MaxTextLength, err = strconv.Atoi(os.Getenv("MAX_TEXT_LENGTH")) + if err != nil { + log.Printf("Ошибка преобразования значения ", os.Getenv("MAX_TEXT_LENGTH")) + } + } if Debug { log.Printf("Адрес сервера Hashicorp Vault: %s ", VaultAddress) } rtr := mux.NewRouter() - rtr.HandleFunc("/unwrap", getDataFromHtmlForm) + rtr.HandleFunc("/unwrap", unwrapDataFromHtmlForm) + rtr.HandleFunc("/wrap", wrapDataFromHtmlForm) rtr.HandleFunc("/genpassword/{passLength:[0-9]+}", genPassword) rtr.HandleFunc("/genpassword", genPassword) - rtr.HandleFunc("/", getDataFromHtmlForm) + rtr.HandleFunc("/", unwrapDataFromHtmlForm) rtr.PathPrefix("/").Handler(http.FileServer(http.Dir("./static"))) http.Handle("/", rtr) @@ -316,4 +408,3 @@ func main() { http.ListenAndServe(listenAddr, nil) } } -