Scert/backend/internal/nginxactivation/policy_linux.go

135 lines
4.4 KiB
Go

package nginxactivation
import (
"bytes"
"encoding/json"
"errors"
"io"
"os"
"path/filepath"
"syscall"
"golang.org/x/sys/unix"
)
// Policy 必须由 root 独立维护,不能复用 Scert 可写的应用配置。
type Policy struct {
Version int `json:"version"`
AllowedUID *uint32 `json:"allowed_uid"`
SocketPath string `json:"socket_path"`
Directory string `json:"directory"`
Hostname string `json:"hostname"`
ProxyAddress string `json:"proxy_address"`
UpstreamAddress string `json:"upstream_address"`
NginxBinary string `json:"nginx_binary"`
NginxConfig string `json:"nginx_config"`
NginxPrefix string `json:"nginx_prefix"`
}
func LoadPolicy(path string) (Policy, error) {
return loadPolicy(path, 0)
}
func loadPolicy(path string, owner uint32) (Policy, error) {
var policy Policy
data, err := readTrustedPolicy(path, owner)
if err != nil {
return policy, err
}
decoder := json.NewDecoder(bytes.NewReader(data))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&policy); err != nil {
return policy, err
}
var extra any
if err := decoder.Decode(&extra); err != io.EOF {
return policy, errors.New("激活策略包含多余内容")
}
if policy.Version != 1 || policy.AllowedUID == nil || *policy.AllowedUID == 0 {
return policy, errors.New("激活策略版本或非特权调用者 UID 无效")
}
if !filepath.IsAbs(policy.SocketPath) || filepath.Clean(policy.SocketPath) != policy.SocketPath || policy.SocketPath == "/" || len(policy.SocketPath) > 107 {
return policy, errors.New("激活 socket 路径无效")
}
if err := validateTrustedParents(filepath.Dir(policy.SocketPath), owner); err != nil {
return policy, err
}
if err := validateDirectory(policy.Directory); err != nil {
return policy, err
}
rotation := policy.Rotation()
if err := errors.Join(rotation.Proxy.validate(), rotation.Upstream.validate()); err != nil {
return policy, err
}
if policy.ProxyAddress == policy.UpstreamAddress {
return policy, errors.New("代理和上游不能使用同一地址")
}
if err := validateOwnedFile(policy.NginxBinary, owner, true); err != nil {
return policy, err
}
if err := validateOwnedFile(policy.NginxConfig, owner, false); err != nil {
return policy, err
}
if !filepath.IsAbs(policy.NginxPrefix) || filepath.Clean(policy.NginxPrefix) != policy.NginxPrefix {
return policy, errors.New("Nginx 前缀必须是规范绝对目录")
}
if err := validateTrustedParents(policy.NginxPrefix, owner); err != nil {
return policy, err
}
return policy, nil
}
func (p Policy) Rotation() Rotation {
return Rotation{
Directory: p.Directory,
Proxy: Endpoint{Address: p.ProxyAddress, Hostname: p.Hostname},
Upstream: Endpoint{Address: p.UpstreamAddress, Hostname: p.Hostname},
Reloader: CommandReloader{Binary: p.NginxBinary, Config: p.NginxConfig, Prefix: p.NginxPrefix},
}
}
func validateOwnedFile(path string, owner uint32, executable bool) error {
if !filepath.IsAbs(path) || filepath.Clean(path) != path {
return errors.New("激活策略文件必须使用规范绝对路径")
}
if err := validateTrustedParents(filepath.Dir(path), owner); err != nil {
return err
}
info, err := os.Lstat(path)
if err != nil {
return err
}
stat, ok := info.Sys().(*syscall.Stat_t)
if !ok || !info.Mode().IsRegular() || stat.Uid != owner || stat.Nlink != 1 || info.Mode().Perm()&0o022 != 0 {
return errors.New("激活策略文件类型、所有者或写权限不可信")
}
if executable && (info.Mode().Perm()&0o100 == 0 || info.Mode()&(os.ModeSetuid|os.ModeSetgid) != 0) {
return errors.New("Nginx 可执行文件权限无效")
}
return nil
}
func readTrustedPolicy(path string, owner uint32) ([]byte, error) {
if err := validateOwnedFile(path, owner, false); err != nil {
return nil, err
}
file, err := os.OpenFile(path, os.O_RDONLY|unix.O_NOFOLLOW|unix.O_NONBLOCK, 0)
if err != nil {
return nil, err
}
defer file.Close()
info, err := file.Stat()
if err != nil {
return nil, err
}
stat, ok := info.Sys().(*syscall.Stat_t)
if !ok || !info.Mode().IsRegular() || stat.Uid != owner || stat.Nlink != 1 || info.Mode().Perm()&0o022 != 0 || info.Size() > 16<<10 {
return nil, errors.New("激活策略文件不可信或超过限制")
}
data, err := io.ReadAll(io.LimitReader(file, (16<<10)+1))
if err != nil || len(data) > 16<<10 {
return nil, errors.New("读取激活策略失败或超过限制")
}
return data, nil
}