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

303 lines
9.0 KiB
Markdown

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