--- # Часть 1. Среда разработки (АРМ / Сборочный сервер) На сервере разработки производятся написание, компиляция микросервиса на Go и подготовка артефактов для деплоя на продакшн-сервера РЕД ОС. На продакшн-сервера средства разработки (компиляторы, заголовки, git) не переносятся. ### Подготовка сборочного стенда (РЕД ОС / Linux) ```bash # Установка Go и git на сервере разработки wget https://go.dev/dl/go1.22.5.linux-amd64.tar.gz sudo rm -rf /usr/local/go && sudo tar -C /usr/local -xzf go1.22.5.linux-amd64.tar.gz export PATH=$PATH:/usr/local/go/bin ``` ### Исходный код микросервиса (`main.go`) Создайте проект и файл `main.go`: ```bash mkdir -p /opt/bastion-auth && cd /opt/bastion-auth go mod init bastion-auth ``` Содержимое файла `main.go`: ```go package main import ( "context" "crypto/hmac" "crypto/sha256" "encoding/base64" "encoding/json" "fmt" "log" "net/http" "strings" "time" "github.com/redis/go-redis/v9" ) const BastionSecret = "super-secret-key-known-only-by-django-and-proxy" var ctx = context.Background() var rdb *redis.Client type TokenPayload struct { User string `json:"user"` Groups []string `json:"groups"` Exp int64 `json:"exp"` JTI string `json:"jti"` } func authHandler(w http.ResponseWriter, r *http.Request) { cookie, err := r.Cookie("X-Bastion-Auth") if err != nil { http.Error(w, "No auth cookie", http.StatusUnauthorized) return } targetGroup := r.Header.Get("X-Target-Group") if targetGroup == "" { http.Error(w, "Target group missing", http.StatusBadRequest) return } parts := strings.Split(cookie.Value, ".") if len(parts) != 2 { http.Error(w, "Invalid token format", http.StatusUnauthorized) return } payloadB64, signature := parts[0], parts[1] mac := hmac.New(sha256.New, []byte(BastionSecret)) mac.Write([]byte(payloadB64)) expectedSig := fmt.Sprintf("%x", mac.Sum(nil)) if !hmac.Equal([]byte(expectedSig), []byte(signature)) { http.Error(w, "Invalid signature", http.StatusUnauthorized) return } payloadBytes, err := base64.RawURLEncoding.DecodeString(payloadB64) if err != nil { payloadBytes, err = base64.URLEncoding.DecodeString(payloadB64) if err != nil { http.Error(w, "Invalid base64", http.StatusUnauthorized) return } } var payload TokenPayload if err := json.Unmarshal(payloadBytes, &payload); err != nil { http.Error(w, "Invalid JSON", http.StatusUnauthorized) return } if payload.Exp < time.Now().Unix() { http.Error(w, "Token expired", http.StatusUnauthorized) return } val, err := rdb.Exists(ctx, "revoked:"+payload.JTI).Result() if err == nil && val > 0 { http.Error(w, "Token revoked", http.StatusUnauthorized) return } allowed := false for _, g := range payload.Groups { if strings.EqualFold(g, targetGroup) { allowed = true break } } if !allowed { http.Error(w, "Forbidden: group mismatch", http.StatusForbidden) return } w.Header().Set("X-Remote-User", payload.User) w.Header().Set("X-Remote-Group", targetGroup) w.WriteHeader(http.StatusOK) } func main() { rdb = redis.NewClient(&redis.Options{ Addr: "127.0.0.1:6379", }) http.HandleFunc("/internal/auth", authHandler) log.Println("Go Auth Service started on 127.0.0.1:8080...") log.Fatal(http.ListenAndServe("127.0.0.1:8080", nil)) } ``` ### Сборка исполняемого файла ```go go get github.com/redis/go-redis/v9 CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o bastion-auth main.go ``` Полученный бинарник `bastion-auth` (вместе с конфигурациями) переносится на продакшн-серверы. --- # Часть 2. Продакшн-среда (Node 1 и Node 2 на РЕД ОС) На продакшн-серверах устанавливаются **только** исполняемые компоненты без средств разработки. ## Автоматизированный установочный скрипт для Продакшн-ноды (РЕД ОС) Скопируйте этот скрипт на каждый из двух серверов (предварительно изменив переменные `NODE_ROLE` на `MASTER` или `BACKUP`), а также разместите рядом скомпилированный на этапе разработки файл `bastion-auth`. ```bash #!/bin/bash set -e # --- ПЕРЕМЕННЫЕ ОКРУЖЕНИЯ ПРОДАКШЕНА --- NODE_ROLE="MASTER" # Укажите MASTER для первой ноды и BACKUP для второй VIP_IP="192.168.1.100/24" # Плавающий VIP-адрес кластера INTERFACE_NAME="eth0" # Сетевой интерфейс в РЕД ОС echo "=== [1/5] Базовая настройка ОС (Отключение SELinux и настройка sysctl) ===" # Полное отключение SELinux sudo sed -i 's/^SELINUX=.*/SELINUX=disabled/' /etc/selinux/config || true sudo setenforce 0 || true # Настройка net.ipv4.ip_nonlocal_bind для работы VIP на backup-ноде echo "net.ipv4.ip_nonlocal_bind = 1" | sudo tee /etc/sysctl.d/99-ip-nonlocal-bind.conf sudo sysctl --system echo "=== [2/5] Установка только рантантим-пакетов (без средств разработки) ===" sudo dnf update -y sudo dnf install -y haproxy redis keepalived curl echo "=== [3/5] Развертывание Go-микросервиса авторизации ===" sudo mkdir -p /opt/bastion-auth # Предполагается, что скомпилированный на этапе разработки файл bastion-auth скопирован в текущую директорию sudo cp bastion-auth /opt/bastion-auth/bastion-auth sudo chmod +x /opt/bastion-auth/bastion-auth # Создание systemd юнита для микросервиса cat << 'EOF' > /etc/systemd/system/bastion-auth.service [Unit] Description=AdminBastion Auth Microservice After=network.target redis.service [Service] ExecStart=/opt/bastion-auth/bastion-auth Restart=always User=root [Install] WantedBy=multi-user.target EOF sudo systemctl daemon-reload sudo systemctl enable --now bastion-auth echo "=== [4/5] Настройка конфигурации HAProxy ===" cat << 'EOF' > /etc/haproxy/haproxy.cfg global log /dev/log local0 user haproxy group haproxy daemon defaults log global mode http option httplog option dontlognull timeout connect 5s timeout client 50s timeout server 50s frontend https_in bind *:443 ssl crt /etc/ssl/bastion/bastion.pem acl is_django path_beg /login /logout /dashboard /api /static use_backend django_sso if is_django acl uri_format path_reg ^/([^/]+)/([^/]+)(/.*)?$ http-request deny deny_status 400 unless uri_format http-request set-var(tx.url_group) path,regsub(^/([^/]+)/([^/]+).*,\1) http-request set-var(tx.url_resource) path,regsub(^/([^/]+)/([^/]+).*,\2) http-request set-header X-Target-Group %[var(tx.url_group)] auth-url /internal/auth auth_backend go_auth_service http-request redirect location /login if { status 401 } http-request deny deny_status 403 if { status 403 } http-request set-path %[path,regsub(^/[^/]+/[^/]+,,g)] http-request set-path / if { path -m str "" } use_backend grafana_corp if { var(tx.url_resource) -m str grafana } use_backend gitlab_corp if { var(tx.url_resource) -m str gitlab } default_backend 404_backend backend go_auth_service server auth1 127.0.0.1:8080 check backend django_sso server django1 127.0.0.1:8000 check backend grafana_corp http-request set-header Host grafana.corp.local server grafana 10.0.1.10:443 ssl verify none backend gitlab_corp http-request set-header Host gitlab.corp.local server gitlab 10.0.1.20:443 ssl verify none backend 404_backend errorfile 404 /etc/haproxy/errors/404.http EOF sudo mkdir -p /etc/ssl/bastion # (Не забудьте положить действующий файл сертификата /etc/ssl/bastion/bastion.pem) echo "=== [5/5] Настройка Keepalived для высокой доступности (HA) ===" PRIORITY=101 if [ "$NODE_ROLE" == "BACKUP" ]; then PRIORITY=100 fi cat << EOF > /etc/keepalived/keepalived.conf vrrp_instance VI_1 { state $NODE_ROLE interface $INTERFACE_NAME virtual_router_id 51 priority $PRIORITY advert_int 1 authentication { auth_type PASS auth_pass bastion_cluster_secret } virtual_ipaddress { $VIP_IP } } EOF sudo systemctl enable --now redis sudo systemctl enable --now keepalived sudo systemctl restart haproxy echo "=== Настройка продакшн ноды AdminBastion успешно завершена! ===" ```