新增数据库驱动的 SecurityEventConnectionManager,复用 RFC 7662 机器 Client,自动完成 Discovery、Push Bearer 生成、Stream 创建、Verification、首次启用、零停机轮换和安全退役。 增加文件与 Kubernetes SecretStore、最小权限 RBAC、动态 Receiver/撤销水位/内省降级,以及系统设置管理页面。已通过全量 Go 测试、go vet、关键包竞态测试、27 个 Web 测试、类型检查、生产构建、Compose 和 Kubernetes dry-run。
113 lines
3.0 KiB
Go
113 lines
3.0 KiB
Go
package securityevents
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
)
|
|
|
|
var secretReferencePattern = regexp.MustCompile(`^[a-z0-9][a-z0-9-]{0,127}$`)
|
|
|
|
var ErrSecretNotFound = errors.New("security event secret not found")
|
|
|
|
type SecretStore interface {
|
|
Put(context.Context, string, []byte) error
|
|
Get(context.Context, string) ([]byte, error)
|
|
Delete(context.Context, string) error
|
|
}
|
|
|
|
type FileSecretStore struct{ directory string }
|
|
|
|
func NewFileSecretStore(directory string) (*FileSecretStore, error) {
|
|
if directory == "" {
|
|
return nil, errors.New("security event secret directory is required")
|
|
}
|
|
if err := os.MkdirAll(directory, 0o700); err != nil {
|
|
return nil, fmt.Errorf("create security event secret directory: %w", err)
|
|
}
|
|
if err := os.Chmod(directory, 0o700); err != nil {
|
|
return nil, fmt.Errorf("protect security event secret directory: %w", err)
|
|
}
|
|
return &FileSecretStore{directory: directory}, nil
|
|
}
|
|
|
|
func (s *FileSecretStore) Put(_ context.Context, reference string, value []byte) error {
|
|
path, err := s.path(reference)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(value) < 32 || len(value) > 4096 {
|
|
return errors.New("security event secret length is invalid")
|
|
}
|
|
temporary, err := os.CreateTemp(s.directory, ".ssf-secret-*")
|
|
if err != nil {
|
|
return fmt.Errorf("create temporary security event secret: %w", err)
|
|
}
|
|
temporaryName := temporary.Name()
|
|
defer func() { _ = os.Remove(temporaryName) }()
|
|
if err := temporary.Chmod(0o600); err != nil {
|
|
_ = temporary.Close()
|
|
return err
|
|
}
|
|
if _, err := temporary.Write(value); err != nil {
|
|
_ = temporary.Close()
|
|
return fmt.Errorf("write security event secret: %w", err)
|
|
}
|
|
if err := temporary.Sync(); err != nil {
|
|
_ = temporary.Close()
|
|
return err
|
|
}
|
|
if err := temporary.Close(); err != nil {
|
|
return err
|
|
}
|
|
if err := os.Rename(temporaryName, path); err != nil {
|
|
return fmt.Errorf("publish security event secret: %w", err)
|
|
}
|
|
return os.Chmod(path, 0o600)
|
|
}
|
|
|
|
func (s *FileSecretStore) Get(_ context.Context, reference string) ([]byte, error) {
|
|
path, err := s.path(reference)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
info, err := os.Stat(path)
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
return nil, ErrSecretNotFound
|
|
}
|
|
if err != nil || !info.Mode().IsRegular() || info.Mode().Perm()&0o077 != 0 {
|
|
return nil, errors.New("security event secret file is not protected")
|
|
}
|
|
value, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read security event secret: %w", err)
|
|
}
|
|
if len(value) < 32 || len(value) > 4096 {
|
|
clear(value)
|
|
return nil, errors.New("security event secret length is invalid")
|
|
}
|
|
return value, nil
|
|
}
|
|
|
|
func (s *FileSecretStore) Delete(_ context.Context, reference string) error {
|
|
path, err := s.path(reference)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = os.Remove(path)
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (s *FileSecretStore) path(reference string) (string, error) {
|
|
if !secretReferencePattern.MatchString(reference) {
|
|
return "", errors.New("security event secret reference is invalid")
|
|
}
|
|
return filepath.Join(s.directory, reference), nil
|
|
}
|