Implement a two-level key hierarchy: the MEK now wraps per-engine DEKs stored in a new barrier_keys table, rather than encrypting all barrier entries directly. A v2 ciphertext format (0x02) embeds the key ID so the barrier can resolve which DEK to use on decryption. v1 ciphertext remains supported for backward compatibility. Key changes: - crypto: EncryptV2/DecryptV2/ExtractKeyID for v2 ciphertext with key IDs - barrier: key registry (CreateKey, RotateKey, ListKeys, MigrateToV2, ReWrapKeys) - seal: RotateMEK re-wraps DEKs without re-encrypting data - engine: Mount auto-creates per-engine DEK - REST + gRPC: barrier/keys, barrier/rotate-mek, barrier/rotate-key, barrier/migrate - proto: BarrierService (v1 + v2) with ListKeys, RotateMEK, RotateKey, Migrate - db: migration v2 adds barrier_keys table Also includes: security audit report, CSRF protection, engine design specs (sshca, transit, user), path-bound AAD migration tool, policy engine enhancements, and ARCHITECTURE.md updates. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -22,6 +22,38 @@ const (
|
||||
EffectDeny Effect = "deny"
|
||||
)
|
||||
|
||||
// Action constants for policy evaluation.
|
||||
const (
|
||||
ActionAny = "any" // matches all non-admin actions
|
||||
ActionRead = "read" // retrieve/list operations
|
||||
ActionWrite = "write" // create/update/delete operations
|
||||
ActionEncrypt = "encrypt" // encrypt data
|
||||
ActionDecrypt = "decrypt" // decrypt data
|
||||
ActionSign = "sign" // sign data
|
||||
ActionVerify = "verify" // verify signatures
|
||||
ActionHMAC = "hmac" // compute HMAC
|
||||
ActionAdmin = "admin" // administrative operations (never matched by "any")
|
||||
)
|
||||
|
||||
// validEffects is the set of recognized effects.
|
||||
var validEffects = map[Effect]bool{
|
||||
EffectAllow: true,
|
||||
EffectDeny: true,
|
||||
}
|
||||
|
||||
// validActions is the set of recognized actions.
|
||||
var validActions = map[string]bool{
|
||||
ActionAny: true,
|
||||
ActionRead: true,
|
||||
ActionWrite: true,
|
||||
ActionEncrypt: true,
|
||||
ActionDecrypt: true,
|
||||
ActionSign: true,
|
||||
ActionVerify: true,
|
||||
ActionHMAC: true,
|
||||
ActionAdmin: true,
|
||||
}
|
||||
|
||||
// Rule is a policy rule stored in the barrier.
|
||||
type Rule struct {
|
||||
ID string `json:"id"`
|
||||
@@ -88,8 +120,34 @@ func (e *Engine) Match(ctx context.Context, req *Request) (Effect, bool, error)
|
||||
return EffectDeny, false, nil // default deny, no matching rule
|
||||
}
|
||||
|
||||
// CreateRule stores a new policy rule.
|
||||
// LintRule validates a rule's effect and actions. It returns a list of problems
|
||||
// (empty if the rule is valid). This does not check resource patterns or other
|
||||
// fields — only the enumerated values that must come from a known set.
|
||||
func LintRule(rule *Rule) []string {
|
||||
var problems []string
|
||||
|
||||
if rule.ID == "" {
|
||||
problems = append(problems, "rule ID is required")
|
||||
}
|
||||
|
||||
if !validEffects[rule.Effect] {
|
||||
problems = append(problems, fmt.Sprintf("invalid effect %q (must be %q or %q)", rule.Effect, EffectAllow, EffectDeny))
|
||||
}
|
||||
|
||||
for _, a := range rule.Actions {
|
||||
if !validActions[strings.ToLower(a)] {
|
||||
problems = append(problems, fmt.Sprintf("invalid action %q", a))
|
||||
}
|
||||
}
|
||||
|
||||
return problems
|
||||
}
|
||||
|
||||
// CreateRule validates and stores a new policy rule.
|
||||
func (e *Engine) CreateRule(ctx context.Context, rule *Rule) error {
|
||||
if problems := LintRule(rule); len(problems) > 0 {
|
||||
return fmt.Errorf("policy: invalid rule: %s", strings.Join(problems, "; "))
|
||||
}
|
||||
data, err := json.Marshal(rule)
|
||||
if err != nil {
|
||||
return fmt.Errorf("policy: marshal rule: %w", err)
|
||||
@@ -157,14 +215,28 @@ func matchesRule(rule *Rule, req *Request) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check action match.
|
||||
if len(rule.Actions) > 0 && !containsString(rule.Actions, req.Action) {
|
||||
// Check action match. The "any" action matches all non-admin actions.
|
||||
if len(rule.Actions) > 0 && !matchesAction(rule.Actions, req.Action) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// matchesAction checks whether any of the rule's actions match the requested action.
|
||||
// The special "any" action matches all actions except "admin".
|
||||
func matchesAction(ruleActions []string, reqAction string) bool {
|
||||
for _, a := range ruleActions {
|
||||
if strings.EqualFold(a, reqAction) {
|
||||
return true
|
||||
}
|
||||
if strings.EqualFold(a, ActionAny) && !strings.EqualFold(reqAction, ActionAdmin) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func containsString(haystack []string, needle string) bool {
|
||||
for _, s := range haystack {
|
||||
if strings.EqualFold(s, needle) {
|
||||
|
||||
Reference in New Issue
Block a user