Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7cb5d8d8c4 | |||
| 19b52f6c9b | |||
| 9c65833954 |
@@ -0,0 +1,34 @@
|
|||||||
|
# P23 Dual-Write Closure — Decision (v0.12)
|
||||||
|
|
||||||
|
**Status**: DEFERRED to v1.x. The full deletion of the legacy CA
|
||||||
|
(`internal/security/ca.go`), mTLS transport (`internal/transport/mtls.go`),
|
||||||
|
and daemon plaintext mode is too large a refactor for v0.12 without
|
||||||
|
risking build stability. The legacy code is already marked Deprecated;
|
||||||
|
the step-ca + OIDC path (P04/P05/P07) is the primary identity layer.
|
||||||
|
|
||||||
|
## What v0.12 did close
|
||||||
|
|
||||||
|
- P07 removed all password paths (step-ca `--password-file`, Proxmox
|
||||||
|
`--password`, KindToken always-denies).
|
||||||
|
- P09 removed daemon plaintext mode (Start() requires mTLS).
|
||||||
|
- P11 added SVID chain validation (VerifySVIDWithChain).
|
||||||
|
- P06 rewrote ACL to OIDC (KindToken deprecated).
|
||||||
|
|
||||||
|
## What remains for v1.x
|
||||||
|
|
||||||
|
- Delete `internal/security/ca.go` legacy CA (requires migrating
|
||||||
|
`orca init` + `orca cert *` to step-ca exclusively).
|
||||||
|
- Delete `internal/transport/mtls.go` deprecated path.
|
||||||
|
- Delete `internal/certpaths/` (v0.8 flat layout); `internal/paths/`
|
||||||
|
is the only layout.
|
||||||
|
- Migrate `rotate-lead`, `drain`, `cutover`, `recovery` from
|
||||||
|
`certpaths` to `paths`.
|
||||||
|
|
||||||
|
## Why not in v0.12
|
||||||
|
|
||||||
|
The legacy CA is load-bearing for `orca init` and 6+ CLI commands. A
|
||||||
|
big-bang deletion would require migrating all of them to step-ca in a
|
||||||
|
single phase, with high risk of breaking the build. v0.12 is a
|
||||||
|
security-hardening milestone; the dual-write window is a code-hygiene
|
||||||
|
issue, not a security vulnerability (the legacy CA is deprecated and
|
||||||
|
the new path is primary). v1.x will close it as a focused refactor.
|
||||||
@@ -20,7 +20,9 @@ package drift
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/hmac"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
|
"encoding/base64"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
@@ -31,6 +33,8 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/hkdf"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Status string
|
type Status string
|
||||||
@@ -571,3 +575,28 @@ func MarshalEvent(e Event) ([]byte, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var _ Detector = (*DefaultDetector)(nil)
|
var _ Detector = (*DefaultDetector)(nil)
|
||||||
|
|
||||||
|
// VerifyEventSignature verifies the HMAC-SHA256 signature of a drift
|
||||||
|
// event using the per-peer key derived from the master key (REQ-140,
|
||||||
|
// F18). The per-peer key = HKDF-SHA256(masterKey, salt=peerID,
|
||||||
|
// info="orca-drift-event-hmac"). The event payload is the JSON-encoded
|
||||||
|
// event (without the signature field). The signature is base64-encoded.
|
||||||
|
//
|
||||||
|
// This function is called by the aggregator when it receives events
|
||||||
|
// from peers. Unsigned or forged events are rejected. The per-peer key
|
||||||
|
// is deployed to peers at /etc/orca/keys/drift-hmac.key (0600, owned by
|
||||||
|
// the orca user) during peer setup.
|
||||||
|
func VerifyEventSignature(eventJSON []byte, signature string, masterKey []byte, peerID string) bool {
|
||||||
|
if len(masterKey) == 0 || peerID == "" || signature == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// Derive the per-peer key.
|
||||||
|
hk := hkdf.New(sha256.New, masterKey, []byte(peerID), []byte("orca-drift-event-hmac"))
|
||||||
|
key := make([]byte, 32)
|
||||||
|
hk.Read(key)
|
||||||
|
// Compute the expected HMAC.
|
||||||
|
mac := hmac.New(sha256.New, key)
|
||||||
|
mac.Write(eventJSON)
|
||||||
|
expected := base64.StdEncoding.EncodeToString(mac.Sum(nil))
|
||||||
|
return hmac.Equal([]byte(expected), []byte(signature))
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,12 +2,17 @@ package drift
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/hkdf"
|
||||||
)
|
)
|
||||||
|
|
||||||
type mockTransport struct {
|
type mockTransport struct {
|
||||||
@@ -463,3 +468,44 @@ func TestNsForPath(t *testing.T) {
|
|||||||
t.Errorf("nsForPath = %q, want empty", got)
|
t.Errorf("nsForPath = %q, want empty", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- REQ-140 / F18 drift event authentication test ---
|
||||||
|
|
||||||
|
// TestVerifyEventSignature verifies HMAC verification works.
|
||||||
|
func TestVerifyEventSignature(t *testing.T) {
|
||||||
|
masterKey := make([]byte, 32)
|
||||||
|
for i := range masterKey {
|
||||||
|
masterKey[i] = byte(i)
|
||||||
|
}
|
||||||
|
peerID := "peer-1"
|
||||||
|
eventJSON := []byte(`{"event_id":"EVT-123","path":"/etc/traefik/orca.yaml","status":"changed"}`)
|
||||||
|
// Compute a valid signature.
|
||||||
|
hk := hkdf.New(sha256.New, masterKey, []byte(peerID), []byte("orca-drift-event-hmac"))
|
||||||
|
key := make([]byte, 32)
|
||||||
|
hk.Read(key)
|
||||||
|
mac := hmac.New(sha256.New, key)
|
||||||
|
mac.Write(eventJSON)
|
||||||
|
sig := base64.StdEncoding.EncodeToString(mac.Sum(nil))
|
||||||
|
if !VerifyEventSignature(eventJSON, sig, masterKey, peerID) {
|
||||||
|
t.Error("valid signature should verify")
|
||||||
|
}
|
||||||
|
// Wrong key.
|
||||||
|
wrongKey := make([]byte, 32)
|
||||||
|
if VerifyEventSignature(eventJSON, sig, wrongKey, peerID) {
|
||||||
|
t.Error("wrong key should fail")
|
||||||
|
}
|
||||||
|
// Wrong peer.
|
||||||
|
if VerifyEventSignature(eventJSON, sig, masterKey, "wrong-peer") {
|
||||||
|
t.Error("wrong peer should fail")
|
||||||
|
}
|
||||||
|
// Tampered event.
|
||||||
|
tampered := append([]byte{}, eventJSON...)
|
||||||
|
tampered[0] ^= 0xFF
|
||||||
|
if VerifyEventSignature(tampered, sig, masterKey, peerID) {
|
||||||
|
t.Error("tampered event should fail")
|
||||||
|
}
|
||||||
|
// Empty signature.
|
||||||
|
if VerifyEventSignature(eventJSON, "", masterKey, peerID) {
|
||||||
|
t.Error("empty signature should fail")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -16,6 +16,11 @@ func Flock(path string) (release func(), err error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
// REQ-139 / F15: tighten pre-existing looser perms to 0600.
|
||||||
|
// OpenFile with O_CREATE only sets the mode on creation; if the
|
||||||
|
// file already exists with looser perms, they persist. Chmod
|
||||||
|
// ensures 0600 regardless.
|
||||||
|
_ = os.Chmod(path, 0o600)
|
||||||
if err := syscall.Flock(int(f.Fd()), syscall.LOCK_EX); err != nil {
|
if err := syscall.Flock(int(f.Fd()), syscall.LOCK_EX); err != nil {
|
||||||
f.Close()
|
f.Close()
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
Reference in New Issue
Block a user