Добавлена поддержка wrap

This commit is contained in:
Калинин Сергей Валерьевич 2024-10-17 15:04:35 +03:00
parent ee921f6ae3
commit 897bb3de01
4 changed files with 159 additions and 28 deletions

View File

@ -1,6 +1,6 @@
# Vault Wrap/Unwrap # 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) на который будут приходить запросы. Если сервис запущен за обратным прокси, то в качестве адреса сервиса -action-address требуется указать адрес прокси, т.е. адрес (FQDN) на который будут приходить запросы.
Для работы шифрования (wrap) нужен vault токен с доступом к vault wrap/unwrap. Токен задается через переменную окружения VAULT_TOKEN.
## Ключи командной строки ## Ключи командной строки
- action-address string - Адрес данного сервиса (https://secret.example.ru). Адрес который будет подставляться в форму в html-шаблон - action-address string - Адрес данного сервиса (https://secret.example.ru). Адрес который будет подставляться в форму в html-шаблон
- debug - Вывод отладочных сообщений - debug - Вывод отладочных сообщений
- listen-port string - Номер порта сервиса (default "8080") - listen-port string - Номер порта сервиса (default "8080")
- log-file string - Путь до лог-файла (default "vault-unwrap.log") - log-file string - Путь до лог-файла (default "vault-unwrap.log")
- max-text-length - Максимальная длина текста для шифрования и длина пароля для генератора (default 100)
- template-dir string -Каталог с шаблонами (default "html-template") - template-dir string -Каталог с шаблонами (default "html-template")
- template-file string Файл-шаблон для ВЭБ-странцы (default "index.html") - template-file string Файл-шаблон для ВЭБ-странцы (default "index.html")
- tls - Использовать SSL/TLS - tls - Использовать SSL/TLS
- tls-cert string - TLS сертификат (полный путь к файлу) - tls-cert string - TLS сертификат (полный путь к файлу)
- tls-key string - TLS ключ (полный путь к файлу) - tls-key string - TLS ключ (полный путь к файлу)
- token-ttl - Время жизни wrap-токена в секундах (default "3600")
- vault-url string - Адрес сервера Hashicorp Vault (https://host.name:8200) - vault-url string - Адрес сервера Hashicorp Vault (https://host.name:8200)
- help - Вывод справочной информации - help - Вывод справочной информации

View File

@ -8,10 +8,13 @@ services:
environment: environment:
- ACTION_ADDRESS=${ACTION_ADDRESS:-https://secret.example.ru} - ACTION_ADDRESS=${ACTION_ADDRESS:-https://secret.example.ru}
- VAULT_ADDRESS=${VAULT_ADDRESS} - VAULT_ADDRESS=${VAULT_ADDRESS}
- VAULT_TOKEN=${WRAP_TOKEN}
- LISTEN_PORT=8080 - LISTEN_PORT=8080
- TLS_KEY_FILE=${TLS_KEY_FILE} - TLS_KEY_FILE=${TLS_KEY_FILE}
- TLS_CERT_FILE=${TLS_CERT_FILE} - TLS_CERT_FILE=${TLS_CERT_FILE}
- TZ=Europe/Moscow - TZ=Europe/Moscow
- MAX_TEXT_LENGTH=${MAX_TEXT_LENGTH:-100}
- TOKEN_TTL=${TOKEN_TTL:-3600}
restart: always restart: always
# ports: # ports:
# - 1234:8080 # - 1234:8080
@ -26,16 +29,48 @@ services:
max-size: "10m" max-size: "10m"
max-file: "5" max-file: "5"
labels: labels:
- "tra.enable=true" - "traefik.enable=true"
- "tra.http.routers.secret.rule=Host(`secret.example.ru`)" - "traefik.http.routers.secret.rule=Host(`secret.example.ru`)"
- "tra.http.services.secret.loadbalancer.server.port=8080" - "traefik.http.services.secret.loadbalancer.server.port=8080"
- "tra.docker.network=reverse-proxy" - "traefik.docker.network=reverse-proxy"
- "tra.http.routers.secret.tls=true" - "traefik.http.routers.secret.tls=true"
- "tra.http.services.secret.loadbalancer.server.scheme=http" - "traefik.http.services.secret.loadbalancer.server.scheme=http"
networks: networks:
- default - default
- vault-wrap - 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: networks:
default: default:
name: reverse-proxy name: reverse-proxy
@ -46,3 +81,5 @@ networks:
volumes: volumes:
vault-wrap-log: vault-wrap-log:
vault-wrap-conf: vault-wrap-conf:
traefik-dynamic-conf:
traefik-ssl:

View File

@ -3,6 +3,6 @@ set -u
while true ;do 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}" -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 sleep 120
done done

127
vault.go
View File

@ -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 package main
import ( import (
@ -10,6 +17,8 @@ import (
"regexp" "regexp"
"bytes" "bytes"
"strconv" "strconv"
"strings"
"time"
"encoding/json" "encoding/json"
"crypto/tls" "crypto/tls"
"net/http" "net/http"
@ -41,15 +50,19 @@ var (
TemplateFile string TemplateFile string
ActionAddress string ActionAddress string
VaultAddress string VaultAddress string
VaultToken string
Data string Data string
ListenPort string ListenPort string
TlsEnable bool TlsEnable bool
TlsCertFile string TlsCertFile string
TlsKeyFile string TlsKeyFile string
MaxTextLength int
TokenTTL string
) )
type TemplateData struct { type TemplateData struct {
URL string URL string
TEXT string TEXT string
MAXTEXTLENGTH int
} }
type UnwrappedData struct { type UnwrappedData struct {
Rerquest_id string `json: "request_id"` Rerquest_id string `json: "request_id"`
@ -57,14 +70,30 @@ type UnwrappedData struct {
Renewable bool `json: "renewable"` Renewable bool `json: "renewable"`
Lease_daration int `json:"lease_duration"` Lease_daration int `json:"lease_duration"`
Data map[string]string `json: "data"` Data map[string]string `json: "data"`
Wrap_info string `json: "wrap_info"` Wrap_info *WrapInfo `json: "wrap_info"`
Warnings string `json: "warnings"` Warnings string `json: "warnings"`
Auth string `json: "auth"` Auth string `json: "auth"`
Mount_type string `json: "mount_type"` 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 := &(*http.DefaultTransport.(*http.Transport))
customTransport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} customTransport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
@ -72,9 +101,10 @@ func vaultDataWrap(vaultAddr string, vaultToken string, vaultSecretName string)
client := &http.Client{Transport: customTransport} client := &http.Client{Transport: customTransport}
// client := &http.Client{} // 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("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) 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") log.Println("Errored when sending request to the Vault server")
} }
var result map[string]interface{} var result UnwrappedData
json.NewDecoder(resp.Body).Decode(&result) json.NewDecoder(resp.Body).Decode(&result)
secret := result["data"].(map[string]interface{})["data"].(map[string]interface{})[vaultSecretName] // wrappedToken := result.Data
log.Println(result) if Debug {
return fmt.Sprint(secret) 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 { 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) json.NewDecoder(resp.Body).Decode(&result)
secret := result.Data secret := result.Data
if Debug { if Debug {
log.Println(result) log.Println("result", result)
log.Println(secret) log.Println("secret", secret)
} }
// log.Println("Length=", len(secret))
// if len(secret) == 0 {
// log.Println("Error:", result.Errors)
// }
// fmt.Sprint(secret) // fmt.Sprint(secret)
// for v, k := range secret { // for v, k := range secret {
// log.Println(k, v) // log.Println(k, v)
@ -151,8 +190,8 @@ func getStaticPage(w http.ResponseWriter, r *http.Request) {
// templateData.UUID = uuid // templateData.UUID = uuid
templateData.URL = ActionAddress templateData.URL = ActionAddress
templateData.TEXT = Data templateData.TEXT = Data
templateData.MAXTEXTLENGTH = MaxTextLength
// templateData.URL = FishingUrl + "/" + arrUsers[i].messageUUID // templateData.URL = FishingUrl + "/" + arrUsers[i].messageUUID
if body, err := ParseTemplate(template, templateData); err == nil { if body, err := ParseTemplate(template, templateData); err == nil {
@ -163,7 +202,7 @@ func getStaticPage(w http.ResponseWriter, r *http.Request) {
// hvs.CAES - 95 // hvs.CAES - 95
// s.Dj7kZS - 26 // s.Dj7kZS - 26
func getDataFromHtmlForm(w http.ResponseWriter, r *http.Request) { func unwrapDataFromHtmlForm(w http.ResponseWriter, r *http.Request) {
r.ParseForm() r.ParseForm()
token := r.FormValue("input_token") token := r.FormValue("input_token")
@ -173,7 +212,7 @@ func getDataFromHtmlForm(w http.ResponseWriter, r *http.Request) {
// fmt.Fprintln(w, r.URL.RawQuery) // fmt.Fprintln(w, r.URL.RawQuery)
// log.Println(w, r.URL.RawQuery) // log.Println(w, r.URL.RawQuery)
if Debug { if Debug {
log.Printf("Текст для расшифровки: %s ", token) log.Printf("Текст для шифровки: %s ", token)
log.Printf("Адрес сервера Hashicorp Vault: %s ", vaultPath) log.Printf("Адрес сервера Hashicorp Vault: %s ", vaultPath)
} }
// Проверка текста на соответствие шаблону // Проверка текста на соответствие шаблону
@ -181,15 +220,26 @@ func getDataFromHtmlForm(w http.ResponseWriter, r *http.Request) {
if Debug { if Debug {
fmt.Println(re.Match([]byte(token))) 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)) { if token != "" && re.Match([]byte(token)) {
b := new(bytes.Buffer) b := new(bytes.Buffer)
for key, value := range vaultDataUnWrap(vaultPath, token) { for key, value := range vaultDataUnWrap(vaultPath, token) {
fmt.Fprintf(b, "%s: %s\n", key, value) fmt.Fprintf(b, "%s: %s\n", key, value)
} }
Data = b.String() Data = b.String()
if Data == "" {
Data = "Ошибка! Токен не найден."
}
if Debug { if Debug {
log.Println(Data) log.Println(Data)
} }
} else if token != "" {
Data = "Введенные данные не соответствуют формату. Введите корректный токен."
} }
getStaticPage(w, r) getStaticPage(w, r)
// http.Redirect(w, r, "http://"+r.Host, http.StatusMovedPermanently) // 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")) // w.Write([]byte("Длина пароля " + passLength + "/n"))
passwordLength, err := strconv.Atoi(passLength) passwordLength, err := strconv.Atoi(passLength)
if passwordLength > 1024 { if passwordLength > MaxTextLength {
log.Printf("Oversized password length") log.Printf("Oversized password length")
Data = "Превышена длина пароля" Data = "Превышена длина пароля"
getStaticPage(w, r) getStaticPage(w, r)
@ -242,6 +292,33 @@ func genPasswordDefault(w http.ResponseWriter, r *http.Request) {
getStaticPage(w, r) 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() { func main() {
var ( var (
logFile string logFile string
@ -256,6 +333,8 @@ func main() {
flag.StringVar(&TlsCertFile, "tls-cert", "", "TLS сертификат (файл)") flag.StringVar(&TlsCertFile, "tls-cert", "", "TLS сертификат (файл)")
flag.StringVar(&TlsKeyFile, "tls-key", "", "TLS ключ (файл)") flag.StringVar(&TlsKeyFile, "tls-key", "", "TLS ключ (файл)")
flag.BoolVar(&TlsEnable, "tls", false, "Использовать SSL/TLS") flag.BoolVar(&TlsEnable, "tls", false, "Использовать SSL/TLS")
flag.IntVar(&MaxTextLength, "max-text-length", 100 , "Максимальная длина текста для шифрования и длина пароля для генератора")
flag.StringVar(&TokenTTL, "token-ttl", "3600", "Время жизни wrap-токена в секундах")
flag.Parse() flag.Parse()
@ -281,17 +360,30 @@ func main() {
VaultAddress = os.Getenv("VAULT_ADDRESS") 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 { if Debug {
log.Printf("Адрес сервера Hashicorp Vault: %s ", VaultAddress) log.Printf("Адрес сервера Hashicorp Vault: %s ", VaultAddress)
} }
rtr := mux.NewRouter() 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/{passLength:[0-9]+}", genPassword)
rtr.HandleFunc("/genpassword", genPassword) rtr.HandleFunc("/genpassword", genPassword)
rtr.HandleFunc("/", getDataFromHtmlForm) rtr.HandleFunc("/", unwrapDataFromHtmlForm)
rtr.PathPrefix("/").Handler(http.FileServer(http.Dir("./static"))) rtr.PathPrefix("/").Handler(http.FileServer(http.Dir("./static")))
http.Handle("/", rtr) http.Handle("/", rtr)
@ -316,4 +408,3 @@ func main() {
http.ListenAndServe(listenAddr, nil) http.ListenAndServe(listenAddr, nil)
} }
} }