2026-10-03 16:20:30 +00:00
2026-10-03 16:20:30 +00:00


Часть 1. Среда разработки (АРМ / Сборочный сервер)

На сервере разработки производятся написание, компиляция микросервиса на Go и подготовка артефактов для деплоя на продакшн-сервера РЕД ОС. На продакшн-сервера средства разработки (компиляторы, заголовки, git) не переносятся.

Подготовка сборочного стенда (РЕД ОС / Linux)

# Установка 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:

mkdir -p /opt/bastion-auth && cd /opt/bastion-auth
go mod init bastion-auth

Содержимое файла main.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 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.

#!/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 успешно завершена! ==="

S
Description
No description provided
Readme
29 KiB
0 Stars 1 Watchers 0 Forks