Skip to content

Commit 4ab4ade

Browse files
committed
Harden untrusted mode after duplication and reuse review
Consolidate the private-IP classifier: the egress broker's dial allowlist now uses networking.IsPrivateIP instead of a second local table, gaining NAT64 embedded-IPv4 decoding (64:ff9b::/96, 64:ff9b:1::/48) so a crafted DNS answer can no longer smuggle the cloud metadata endpoint past D7 on NAT64 networks. The shared classifier absorbs the two benchmarking prefixes the egress table had, making the merged set a strict superset. Render the Envoy bootstrap as data: all config-derived values (vhosts, routes, ext_authz address, scan posture) go through yaml.Marshal instead of string substitution, matching the operator side and removing the reliance on cross-package validation alone. TokenMap rebuilt on hashicorp/golang-lru/v2 (already a dependency), keeping consume-on-read semantics via Peek+Remove under the map mutex; the hand-rolled container/list machinery is gone. Converge tokenenc on pkg/secrets/aes: new EncryptWithAAD/ DecryptWithAAD helpers carry the nonce|ct|tag convention and size bound; tokenenc deletes its duplicated GCM boilerplate and calls them, keeping the redis-key AAD cut-and-paste defense. Also: slices.Contains/Sort/ContainsFunc in the policy hot path, and cross-referenced drift-guard comments between the operator and vMCP secret-material gate test suites.
1 parent 0cc06ff commit 4ab4ade

10 files changed

Lines changed: 388 additions & 179 deletions

File tree

cmd/thv-operator/pkg/controllerutil/untrusted_env_validation_test.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,15 @@ func configMapEnvVar(name, configMapName, key string) corev1.EnvVar {
3939
}
4040
}
4141

42+
// TestValidateNoSecretEnvForUntrusted pins the operator admission-side
43+
// untrusted pod gate. CHANNEL-COVERAGE MIRROR: the vmcp runtime-side gate's
44+
// test — pkg/vmcp/session/untrusted/template_clone_test.go
45+
// (TestClonePodFromTemplate_FailClosed / TestReverifyAllowsFieldRefEnv) —
46+
// covers the same secret-injection channels (secretKeyRef / configMapKeyRef
47+
// env, envFrom secret/configmap refs, secret / configmap / projected
48+
// volumes). When a new injection channel is added or allowed on one side,
49+
// update the other side's suite too — drift between the two is a silent gap
50+
// in the untrusted isolation boundary.
4251
func TestValidateNoSecretEnvForUntrusted(t *testing.T) {
4352
t.Parallel()
4453

pkg/authserver/tokenenc/tokenenc.go

Lines changed: 6 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,14 @@
2929
package tokenenc
3030

3131
import (
32-
"crypto/aes"
33-
"crypto/cipher"
3432
"crypto/rand"
3533
"encoding/base64"
3634
"encoding/json"
3735
"errors"
3836
"fmt"
3937
"io"
38+
39+
"github.com/stacklok/toolhive/pkg/secrets/aes"
4040
)
4141

4242
const (
@@ -144,12 +144,12 @@ func Seal(kr Keyring, redisKey string, plaintext []byte) ([]byte, error) {
144144

145145
activeID, kek := kr.Active()
146146

147-
edek, err := gcmSeal(kek, dek, nil)
147+
edek, err := aes.Encrypt(dek, kek)
148148
if err != nil {
149149
return nil, fmt.Errorf("token encryption: failed to wrap data key: %w", err)
150150
}
151151

152-
ct, err := gcmSeal(dek, plaintext, []byte(redisKey))
152+
ct, err := aes.EncryptWithAAD(plaintext, dek, []byte(redisKey))
153153
if err != nil {
154154
return nil, fmt.Errorf("token encryption: failed to encrypt record: %w", err)
155155
}
@@ -201,7 +201,7 @@ func Open(kr Keyring, redisKey string, value []byte) (plaintext []byte, legacy b
201201
if err != nil {
202202
return nil, false, fmt.Errorf("token encryption: malformed wrapped data key: %w", err)
203203
}
204-
dek, err := gcmOpen(kek, edek, nil)
204+
dek, err := aes.Decrypt(edek, kek)
205205
if err != nil {
206206
return nil, false, fmt.Errorf("token encryption: failed to unwrap data key: %w", err)
207207
}
@@ -210,7 +210,7 @@ func Open(kr Keyring, redisKey string, value []byte) (plaintext []byte, legacy b
210210
if err != nil {
211211
return nil, false, fmt.Errorf("token encryption: malformed ciphertext: %w", err)
212212
}
213-
plaintext, err = gcmOpen(dek, ct, []byte(redisKey))
213+
plaintext, err = aes.DecryptWithAAD(ct, dek, []byte(redisKey))
214214
if err != nil {
215215
return nil, false, fmt.Errorf("token encryption: failed to decrypt record: %w", err)
216216
}
@@ -246,37 +246,3 @@ func NeedsRotation(kr Keyring, value []byte) bool {
246246
activeID, _ := kr.Active()
247247
return env.KID != activeID
248248
}
249-
250-
// gcmSeal encrypts plaintext with AES-256-GCM under key, returning
251-
// nonce|ciphertext|tag. aad is additional authenticated data (may be nil).
252-
func gcmSeal(key, plaintext, aad []byte) ([]byte, error) {
253-
block, err := aes.NewCipher(key)
254-
if err != nil {
255-
return nil, err
256-
}
257-
gcm, err := cipher.NewGCM(block)
258-
if err != nil {
259-
return nil, err
260-
}
261-
nonce := make([]byte, gcm.NonceSize())
262-
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
263-
return nil, err
264-
}
265-
return gcm.Seal(nonce, nonce, plaintext, aad), nil
266-
}
267-
268-
// gcmOpen reverses gcmSeal.
269-
func gcmOpen(key, ciphertext, aad []byte) ([]byte, error) {
270-
block, err := aes.NewCipher(key)
271-
if err != nil {
272-
return nil, err
273-
}
274-
gcm, err := cipher.NewGCM(block)
275-
if err != nil {
276-
return nil, err
277-
}
278-
if len(ciphertext) < gcm.NonceSize() {
279-
return nil, errors.New("malformed ciphertext")
280-
}
281-
return gcm.Open(nil, ciphertext[:gcm.NonceSize()], ciphertext[gcm.NonceSize():], aad)
282-
}

0 commit comments

Comments
 (0)