SHA256
Добавить readme.md
This commit is contained in:
commit
d6660a9d78
1 file changed
+303
@@ -0,0 +1,303 @@
|
||||
|
||||
---
|
||||
|
||||
# Часть 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 успешно завершена! ==="
|
||||
|
||||
```
|
||||
Reference in new issue
Block a user