- Remove telemetry: disabled all phone-home (installation notify, false positives, behavior tracking, upgrade tips) - Version branding: removed 'ce-' prefix, product name changed to '长亭雷池 WAF' - Detection engine enhancement: strict preset enables all 17 modules with aggressive configs - Multi-user RBAC: admin/operator/viewer roles with password + TOTP 2FA - Rate limiting: nginx limit_req per website (RPS, burst, action) - JS Challenge: browser fingerprint verification page with cookie-based bypass - Security fixes: removed InsecureSkipVerify, hardcoded credentials, NO_AUTH backdoor
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
|
||||
"chaitin.cn/patronus/safeline-2/management/tcontrollerd/pkg/config"
|
||||
"chaitin.cn/patronus/safeline-2/management/tcontrollerd/pkg/log"
|
||||
pb "chaitin.cn/patronus/safeline-2/management/tcontrollerd/proto/website"
|
||||
)
|
||||
|
||||
var (
|
||||
logger = log.GetLogger("controller")
|
||||
)
|
||||
|
||||
func Handle() error {
|
||||
logger.Infof("Connect mgt-webserver at %s", config.GlobalConfig.MgtWebserver)
|
||||
gRPCConn, err := grpc.Dial(config.GlobalConfig.MgtWebserver, []grpc.DialOption{
|
||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||
}...)
|
||||
if err != nil {
|
||||
logger.Errorf("Fail to dial: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
wsClient := pb.NewWebsiteClient(gRPCConn)
|
||||
|
||||
defer func(conn *grpc.ClientConn) {
|
||||
err := conn.Close()
|
||||
if err != nil {
|
||||
logger.Errorf("Fail to close: %v", err)
|
||||
return
|
||||
}
|
||||
}(gRPCConn)
|
||||
|
||||
if err = websiteHandler(wsClient); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package controller
|
||||
|
||||
// nginxConfigTpl generates nginx config for each website.
|
||||
// Placeholders:
|
||||
// %s = upstream name
|
||||
// %s = upstream addr(s)
|
||||
// %s = server listen directive
|
||||
// %s = server_name directive
|
||||
// %s = ssl_certificate directive (or empty)
|
||||
// %s = ssl_certificate_key directive (or empty)
|
||||
// %s = upstream scheme (http/https)
|
||||
// %s = upstream name (again for proxy_pass)
|
||||
// %d = site UUID (SL-CE-SUID header value)
|
||||
var nginxConfigTpl = `
|
||||
upstream %s {
|
||||
%s
|
||||
keepalive 128;
|
||||
keepalive_timeout 75;
|
||||
}
|
||||
server {
|
||||
%s
|
||||
%s
|
||||
%s
|
||||
%s
|
||||
location = /forbidden_page {
|
||||
internal;
|
||||
root /etc/nginx/forbidden_pages;
|
||||
try_files /default_forbidden_page.html =403;
|
||||
}
|
||||
location ^~ / {
|
||||
proxy_pass %s://%s;
|
||||
include proxy_params;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header Accept-Encoding "";
|
||||
t1k_append_header SL-CE-SUID %d;
|
||||
t1k_body_size 1024k;
|
||||
tx_body_size 4k;
|
||||
t1k_error_page 403 /forbidden_page;
|
||||
tx_error_page 403 /forbidden_page;
|
||||
%s
|
||||
%s
|
||||
}
|
||||
}`
|
||||
|
||||
// rateLimitZoneTpl defines a shared memory zone for rate limiting.
|
||||
// %s = zone name (site_N)
|
||||
// %s = rate (e.g. "100r/s")
|
||||
var rateLimitZoneTpl = `limit_req_zone $binary_remote_addr zone=%s:%dm rate=%s;`
|
||||
|
||||
// rateLimitReqTpl applies rate limiting to a location.
|
||||
// %s = zone name
|
||||
// %d = burst value
|
||||
var rateLimitReqTpl = `limit_req zone=%s burst=%d nodelay;`
|
||||
|
||||
// rateLimitRejectTpl returns 429 when rate limit is exceeded.
|
||||
var rateLimitRejectTpl = `limit_req_status 429;`
|
||||
|
||||
// challengeLocationTpl adds a JS challenge verification endpoint.
|
||||
var challengeLocationTpl = `
|
||||
location = /__waf_challenge {
|
||||
internal;
|
||||
root /etc/nginx/challenge_pages;
|
||||
try_files /challenge.html =200;
|
||||
}
|
||||
location = /__waf_verify {
|
||||
internal;
|
||||
proxy_pass http://127.0.0.1:3335/verify;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}`
|
||||
|
||||
var upstreamAddrTpl = `server %s;`
|
||||
var serverListenTpl = `listen 0.0.0.0:%s%s%s;`
|
||||
var addrAnyPropertiesTpl = " default_server backlog=65536 reuseport"
|
||||
var serverNameTpl = `server_name %s;`
|
||||
var certTpl = `ssl_certificate /etc/nginx/certs/%s;`
|
||||
var certKeyTpl = `ssl_certificate_key /etc/nginx/certs/%s;`
|
||||
var backendTpl = `backend_%d`
|
||||
@@ -0,0 +1,328 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"io/ioutil"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"chaitin.cn/patronus/safeline-2/management/tcontrollerd/model"
|
||||
"chaitin.cn/patronus/safeline-2/management/tcontrollerd/pkg/ngcmd"
|
||||
pb "chaitin.cn/patronus/safeline-2/management/tcontrollerd/proto/website"
|
||||
"chaitin.cn/patronus/safeline-2/management/tcontrollerd/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
Ping = "ping"
|
||||
Pong = "pong"
|
||||
EventTypeWebsite = "website"
|
||||
EventTypeDeleteWebsite = "deleteWebsite"
|
||||
EventTypeFullWebsite = "fullWebsite"
|
||||
|
||||
nginxConfigPath = "/etc/nginx/sites-enabled/" // 修改的为 host /resources/nginx/ 中的文件
|
||||
nginxFilePrefix = "IF_backend_"
|
||||
nginxBackupFilePrefix = "BAK_IF_backend_"
|
||||
nginxFileMode = 0644
|
||||
|
||||
HttpsScheme = "https"
|
||||
DefaultHttpsPort = "443"
|
||||
)
|
||||
|
||||
var pong = pb.Response{Type: Pong, Msg: nil, Err: false}
|
||||
|
||||
func generateNginxConfig(website *model.WebsiteConfig) (string, error) {
|
||||
// only ONE upstream supported in v1.0
|
||||
var upstreamAddr string
|
||||
scheme := "http"
|
||||
for _, upstream := range website.Upstreams {
|
||||
urlInfo, err := url.Parse(upstream)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if urlInfo.Scheme == HttpsScheme && urlInfo.Port() == "" {
|
||||
urlInfo.Host = urlInfo.Host + ":" + DefaultHttpsPort
|
||||
}
|
||||
upstreamAddr = fmt.Sprintf(upstreamAddrTpl, urlInfo.Host)
|
||||
if len(urlInfo.Scheme) > 0 {
|
||||
scheme = urlInfo.Scheme
|
||||
}
|
||||
}
|
||||
|
||||
sslFlag := ""
|
||||
sslCertFilename := ""
|
||||
sslCertKeyFilename := ""
|
||||
if website.KeyFilename != "" && website.CertFilename != "" {
|
||||
sslFlag = " ssl" // with a blank ahead
|
||||
sslCertFilename = fmt.Sprintf(certTpl, website.CertFilename)
|
||||
sslCertKeyFilename = fmt.Sprintf(certKeyTpl, website.KeyFilename)
|
||||
}
|
||||
|
||||
// only ONE server name supported in v1.0
|
||||
var serverName string
|
||||
var addrAnyProperties string
|
||||
for _, sn := range website.ServerNames {
|
||||
if sn == "*" || sn == "" {
|
||||
sn = "_"
|
||||
addrAnyProperties = addrAnyPropertiesTpl
|
||||
}
|
||||
serverName = fmt.Sprintf(serverNameTpl, sn)
|
||||
}
|
||||
|
||||
// only ONE port supported in v1.0
|
||||
var serverListen string
|
||||
for _, port := range website.Ports {
|
||||
serverListen = fmt.Sprintf(serverListenTpl, port, sslFlag, addrAnyProperties)
|
||||
}
|
||||
|
||||
// Rate limiting directives
|
||||
rateLimitDirective := ""
|
||||
if website.RateLimitEnabled && website.RateLimitRPS > 0 {
|
||||
zoneName := fmt.Sprintf("site_%d", website.Id)
|
||||
rps := website.RateLimitRPS
|
||||
burst := website.RateLimitBurst
|
||||
if burst <= 0 {
|
||||
burst = rps * 2
|
||||
}
|
||||
rateLimitDirective = fmt.Sprintf(rateLimitReqTpl, zoneName, burst)
|
||||
}
|
||||
|
||||
// Challenge directive
|
||||
challengeDirective := ""
|
||||
if website.ChallengeEnabled {
|
||||
challengeDirective = `error_page 403 = /__waf_challenge;`
|
||||
}
|
||||
|
||||
upstreamName := fmt.Sprintf(backendTpl, website.Id)
|
||||
nginxConfig := strings.TrimSpace(fmt.Sprintf(nginxConfigTpl,
|
||||
upstreamName, upstreamAddr,
|
||||
serverListen, serverName, sslCertFilename, sslCertKeyFilename,
|
||||
scheme, upstreamName, website.Id,
|
||||
rateLimitDirective, challengeDirective,
|
||||
))
|
||||
|
||||
// Prepend rate limit zone definition if enabled
|
||||
if website.RateLimitEnabled && website.RateLimitRPS > 0 {
|
||||
zoneName := fmt.Sprintf("site_%d", website.Id)
|
||||
rps := website.RateLimitRPS
|
||||
zoneDef := fmt.Sprintf(rateLimitZoneTpl, zoneName, 10, fmt.Sprintf("%dr/s", rps))
|
||||
nginxConfig = zoneDef + "\n" + nginxConfig
|
||||
}
|
||||
|
||||
return nginxConfig, nil
|
||||
}
|
||||
|
||||
func nginxTestAndReload() error {
|
||||
err := ngcmd.NginxConfTest()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = ngcmd.NginxConfReload()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func generateFullConfigAndReload(msg []byte) error {
|
||||
var websites []model.WebsiteConfig
|
||||
if err := json.Unmarshal(msg, &websites); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
configFilename := make(map[string]struct{})
|
||||
for _, website := range websites {
|
||||
configFilename[fmt.Sprintf("%s%d", nginxFilePrefix, website.Id)] = struct{}{}
|
||||
}
|
||||
filepath.Walk(nginxConfigPath, func(path string, info fs.FileInfo, err error) error {
|
||||
_, ok := configFilename[info.Name()]
|
||||
if !ok && strings.HasPrefix(info.Name(), nginxFilePrefix) {
|
||||
if err := os.Remove(path); err != nil {
|
||||
// not return error only logged error in order to delete not exist website nginx conf
|
||||
logger.Warn(err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
for _, website := range websites {
|
||||
if err := generateConfigAndReload(&website); err != nil {
|
||||
// trigger a full site push when tcd starts, ignore some site errors, and push as much as possible
|
||||
logger.Warn(err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func generateConfigAndReload(website *model.WebsiteConfig) error {
|
||||
nginxConfig, err := generateNginxConfig(website)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
configPath := filepath.Join(nginxConfigPath, fmt.Sprintf("%s%d", nginxFilePrefix, website.Id))
|
||||
backupPath := filepath.Join(nginxConfigPath, fmt.Sprintf("%s%d", nginxBackupFilePrefix, website.Id))
|
||||
|
||||
oldConfigExists, err := utils.FileExist(configPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if oldConfigExists {
|
||||
oldConfig, err := ioutil.ReadFile(configPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if string(oldConfig) == nginxConfig {
|
||||
logger.Info("No changes in the new website config, skip nginx -s reload")
|
||||
return nil
|
||||
}
|
||||
|
||||
// tmp save old config to the backup path
|
||||
if err = utils.CopyFile(configPath, backupPath); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err = utils.EnsureWriteFile(configPath, []byte(nginxConfig), nginxFileMode); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = nginxTestAndReload(); err != nil {
|
||||
nginxError := err
|
||||
if err = os.Remove(configPath); err != nil {
|
||||
return err
|
||||
}
|
||||
if oldConfigExists {
|
||||
// new config err, restore old config
|
||||
if err = utils.CopyFile(backupPath, configPath); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = os.Remove(backupPath); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nginxError
|
||||
}
|
||||
|
||||
if oldConfigExists {
|
||||
if err = os.Remove(backupPath); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func deleteConfigAndReload(config []byte) error {
|
||||
var websiteIds []uint
|
||||
if err := json.Unmarshal(config, &websiteIds); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, id := range websiteIds {
|
||||
configPath := filepath.Join(nginxConfigPath, fmt.Sprintf("%s%d", nginxFilePrefix, id))
|
||||
exists, err := utils.FileExist(configPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if exists {
|
||||
if err := os.Remove(configPath); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := nginxTestAndReload(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func sendResponse(stream pb.Website_SubscribeClient, eventType string, errMsg []byte) error {
|
||||
return stream.Send(&pb.Response{
|
||||
Type: eventType,
|
||||
Msg: errMsg,
|
||||
Err: len(errMsg) != 0,
|
||||
})
|
||||
}
|
||||
|
||||
func websiteHandler(wc pb.WebsiteClient) error {
|
||||
stream, err := wc.Subscribe(context.Background())
|
||||
if err != nil {
|
||||
logger.Errorf("Subscribe failed: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
for {
|
||||
event, err := stream.Recv()
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
// read done.
|
||||
logger.Infof("Recv EOF from webserver")
|
||||
return nil
|
||||
} else {
|
||||
logger.Errorf("Handle failed: %v", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
logger.Debugf("Got message Type %s, Msg: %s", event.GetType(), event.GetMsg())
|
||||
if event.Type == Ping {
|
||||
if err = stream.Send(&pong); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if event.Type == EventTypeWebsite {
|
||||
logger.Infof("Update website with config: %s", event.GetMsg())
|
||||
var website model.WebsiteConfig
|
||||
if err := json.Unmarshal(event.GetMsg(), &website); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = generateConfigAndReload(&website); err != nil {
|
||||
logger.Error(err)
|
||||
if err := sendResponse(stream, EventTypeDeleteWebsite, []byte(err.Error())); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err = sendResponse(stream, EventTypeDeleteWebsite, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else if event.Type == EventTypeFullWebsite {
|
||||
if err = generateFullConfigAndReload(event.Msg); err != nil {
|
||||
logger.Error(err)
|
||||
sendErr := sendResponse(stream, EventTypeFullWebsite, []byte(err.Error()))
|
||||
if sendErr != nil {
|
||||
return sendErr
|
||||
}
|
||||
} else {
|
||||
if err = sendResponse(stream, EventTypeFullWebsite, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else if event.Type == EventTypeDeleteWebsite {
|
||||
if err = deleteConfigAndReload(event.Msg); err != nil {
|
||||
logger.Error(err)
|
||||
sendErr := sendResponse(stream, EventTypeDeleteWebsite, []byte(err.Error()))
|
||||
if sendErr != nil {
|
||||
return sendErr
|
||||
}
|
||||
} else {
|
||||
if err = sendResponse(stream, EventTypeDeleteWebsite, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user