Skip to content

Commit a5fd438

Browse files
authored
Merge pull request #2164 from felixfontein/hv
HC Vault: add allowlist support for acceptable HC Vault URLs
2 parents aa029e4 + 2505e03 commit a5fd438

3 files changed

Lines changed: 250 additions & 0 deletions

File tree

README.rst

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -558,6 +558,17 @@ To easily deploy Vault locally: (DO NOT DO THIS FOR PRODUCTION!!!)
558558
559559
$ sops encrypt --verbose prod/raw.yaml > prod/encrypted.yaml
560560
561+
Restricting HC Vault servers that SOPS can talk to
562+
**************************************************
563+
564+
If you want to restrict which HC Vault servers SOPS is allowed to talk to, you can set the ``SOPS_HC_VAULT_ALLOWLIST`` environment variable.
565+
When set to ``all`` (the default value), there is no restriction.
566+
When set to ``none``, SOPS will not allow any access to HC Vault servers for decryption or encryption.
567+
568+
When set to any other value, this value will be interpreted as a comma-separated list of strings.
569+
If SOPS attempts to contact a vault URL that starts with one of these strings, SOPS will attempt to contact that URL.
570+
If there is no matching prefix in ``SOPS_HC_VAULT_ALLOWLIST``, SOPS will not contact that URL.
571+
561572
Encrypting using HuaweiCloud KMS
562573
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
563574

hcvault/keysource.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,75 @@ import (
2626
const (
2727
// KeyTypeIdentifier is the string used to identify a Vault MasterKey.
2828
KeyTypeIdentifier = "hc_vault"
29+
// SopsHCVaultAllowlist can be set as an environment variable with a string list
30+
// of age keys as value.
31+
SopsHCVaultAllowlist = "SOPS_HC_VAULT_ALLOWLIST"
32+
// Special value for allowlist that allows all hosts
33+
AllowlistAllHosts = "all"
34+
// Special value for allowlist that allows no hosts
35+
AllowlistNoHosts = "none"
36+
// Default value of allowlist. Should eventually be changed to "none".
37+
AllowlistDefault = AllowlistAllHosts
2938
)
3039

40+
type allowList struct {
41+
All bool
42+
URIs []string
43+
}
44+
45+
func (al *allowList) Allows(address string) bool {
46+
if al.All {
47+
return true
48+
}
49+
if !strings.HasSuffix(address, "/") {
50+
address = address + "/"
51+
}
52+
for _, uri := range al.URIs {
53+
if strings.HasPrefix(address, uri) {
54+
return true
55+
}
56+
}
57+
return false
58+
}
59+
60+
func parseAllowlistString(allowlistStr string) (allowList, error) {
61+
switch allowlistStr {
62+
case AllowlistAllHosts:
63+
return allowList{
64+
All: true,
65+
URIs: nil,
66+
}, nil
67+
case AllowlistNoHosts:
68+
return allowList{
69+
All: false,
70+
URIs: nil,
71+
}, nil
72+
}
73+
uris := strings.Split(allowlistStr, ",")
74+
for idx, uri := range uris {
75+
uri = strings.Trim(uri, " ")
76+
if uri == "" {
77+
return allowList{}, fmt.Errorf("%s's entry %d is empty", SopsHCVaultAllowlist, idx+1)
78+
}
79+
if !strings.HasSuffix(uri, "/") {
80+
uri = uri + "/"
81+
}
82+
uris[idx] = uri
83+
}
84+
return allowList{
85+
All: false,
86+
URIs: uris,
87+
}, nil
88+
}
89+
90+
func getAllowlist() (allowList, error) {
91+
var allowlistStr = AllowlistDefault
92+
if allowlist, ok := os.LookupEnv(SopsHCVaultAllowlist); ok && len(allowlist) > 0 {
93+
allowlistStr = allowlist
94+
}
95+
return parseAllowlistString(allowlistStr)
96+
}
97+
3198
func init() {
3299
log = logging.NewLogger("VAULT_TRANSIT")
33100
}
@@ -331,6 +398,14 @@ func vaultClient(address, token string, hc *http.Client) (*api.Client, error) {
331398
cfg := api.DefaultConfig()
332399
cfg.Address = address
333400

401+
allowlist, err := getAllowlist()
402+
if err != nil {
403+
return nil, err
404+
}
405+
if !allowlist.Allows(address) {
406+
return nil, fmt.Errorf("Allowlist does not allow %s", address)
407+
}
408+
334409
if hc != nil {
335410
cfg.HttpClient = hc
336411
}

hcvault/keysource_test.go

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,20 @@ var (
2727
// testVaultAddress is the HTTP/S address of the Vault server, it is set
2828
// by TestMain after booting it.
2929
testVaultAddress string
30+
// Whether to skip all Docker-based tests.
31+
testSkipDocker = false
3032
)
3133

3234
// TestMain initializes a Vault server using Docker, writes the HTTP address to
3335
// testVaultAddress, waits for it to become ready to serve requests, and enables
3436
// Vault Transit on the testEnginePath. It then runs all the tests, which can
3537
// make use of the various `test*` variables.
3638
func TestMain(m *testing.M) {
39+
if testSkipDocker {
40+
os.Exit(m.Run())
41+
return
42+
}
43+
3744
// Uses a sensible default on Windows (TCP/HTTP) and Linux/MacOS (socket)
3845
pool, err := dockertest.NewPool("")
3946
if err != nil {
@@ -179,6 +186,10 @@ func TestNewMasterKeyFromURI(t *testing.T) {
179186
}
180187

181188
func TestMasterKey_Encrypt(t *testing.T) {
189+
if testSkipDocker {
190+
return
191+
}
192+
182193
key := NewMasterKey(testVaultAddress, testEnginePath, "encrypt")
183194
(Token(testVaultToken)).ApplyToMasterKey(key)
184195
assert.NoError(t, createVaultKey(key))
@@ -207,6 +218,10 @@ func TestMasterKey_Encrypt(t *testing.T) {
207218
}
208219

209220
func TestMasterKey_EncryptIfNeeded(t *testing.T) {
221+
if testSkipDocker {
222+
return
223+
}
224+
210225
key := NewMasterKey(testVaultAddress, testEnginePath, "encrypt-if-needed")
211226
(Token(testVaultToken)).ApplyToMasterKey(key)
212227
assert.NoError(t, createVaultKey(key))
@@ -226,6 +241,10 @@ func TestMasterKey_EncryptedDataKey(t *testing.T) {
226241
}
227242

228243
func TestMasterKey_Decrypt(t *testing.T) {
244+
if testSkipDocker {
245+
return
246+
}
247+
229248
key := NewMasterKey(testVaultAddress, testEnginePath, "decrypt")
230249
(Token(testVaultToken)).ApplyToMasterKey(key)
231250
assert.NoError(t, createVaultKey(key))
@@ -254,6 +273,10 @@ func TestMasterKey_Decrypt(t *testing.T) {
254273
}
255274

256275
func TestMasterKey_EncryptDecrypt_RoundTrip(t *testing.T) {
276+
if testSkipDocker {
277+
return
278+
}
279+
257280
token := Token(testVaultToken)
258281

259282
encryptKey := NewMasterKey(testVaultAddress, testEnginePath, "roundtrip")
@@ -519,3 +542,144 @@ func createVaultKey(key *MasterKey) error {
519542
_, err = client.Logical().Read(p)
520543
return err
521544
}
545+
546+
func TestAllowlistParse(t *testing.T) {
547+
t.Run("success", func(t *testing.T) {
548+
al, err := parseAllowlistString("all")
549+
assert.NoError(t, err)
550+
assert.Equal(t, allowList{
551+
All: true,
552+
URIs: nil,
553+
}, al)
554+
555+
al, err = parseAllowlistString("none")
556+
assert.NoError(t, err)
557+
assert.Equal(t, allowList{
558+
All: false,
559+
URIs: nil,
560+
}, al)
561+
562+
al, err = parseAllowlistString("non")
563+
assert.NoError(t, err)
564+
assert.Equal(t, allowList{
565+
All: false,
566+
URIs: []string{
567+
"non/",
568+
},
569+
}, al)
570+
571+
al, err = parseAllowlistString("foo,bar/,baz")
572+
assert.NoError(t, err)
573+
assert.Equal(t, allowList{
574+
All: false,
575+
URIs: []string{
576+
"foo/",
577+
"bar/",
578+
"baz/",
579+
},
580+
}, al)
581+
582+
al, err = parseAllowlistString(" foo/ , bar, baz ")
583+
assert.NoError(t, err)
584+
assert.Equal(t, allowList{
585+
All: false,
586+
URIs: []string{
587+
"foo/",
588+
"bar/",
589+
"baz/",
590+
},
591+
}, al)
592+
})
593+
594+
t.Run("error", func(t *testing.T) {
595+
al, err := parseAllowlistString("")
596+
assert.Error(t, err)
597+
assert.Equal(t, "SOPS_HC_VAULT_ALLOWLIST's entry 1 is empty", err.Error())
598+
assert.Equal(t, allowList{
599+
All: false,
600+
URIs: nil,
601+
}, al)
602+
603+
al, err = parseAllowlistString(",")
604+
assert.Error(t, err)
605+
assert.Equal(t, "SOPS_HC_VAULT_ALLOWLIST's entry 1 is empty", err.Error())
606+
assert.Equal(t, allowList{
607+
All: false,
608+
URIs: nil,
609+
}, al)
610+
611+
al, err = parseAllowlistString(",a")
612+
assert.Error(t, err)
613+
assert.Equal(t, "SOPS_HC_VAULT_ALLOWLIST's entry 1 is empty", err.Error())
614+
assert.Equal(t, allowList{
615+
All: false,
616+
URIs: nil,
617+
}, al)
618+
619+
al, err = parseAllowlistString("a,")
620+
assert.Error(t, err)
621+
assert.Equal(t, "SOPS_HC_VAULT_ALLOWLIST's entry 2 is empty", err.Error())
622+
assert.Equal(t, allowList{
623+
All: false,
624+
URIs: nil,
625+
}, al)
626+
})
627+
}
628+
629+
func TestAllowlistAllow(t *testing.T) {
630+
al, _ := parseAllowlistString("all")
631+
assert.Equal(t, al.Allows(""), true)
632+
assert.Equal(t, al.Allows("foo"), true)
633+
assert.Equal(t, al.Allows("bar"), true)
634+
assert.Equal(t, al.Allows("http://example.com"), true)
635+
assert.Equal(t, al.Allows("http://example.com/"), true)
636+
assert.Equal(t, al.Allows("https://example.com/foo"), true)
637+
638+
al, _ = parseAllowlistString("none")
639+
assert.Equal(t, al.Allows(""), false)
640+
assert.Equal(t, al.Allows("foo"), false)
641+
assert.Equal(t, al.Allows("bar"), false)
642+
assert.Equal(t, al.Allows("http://example.com"), false)
643+
assert.Equal(t, al.Allows("http://example.com/"), false)
644+
assert.Equal(t, al.Allows("https://example.com/foo"), false)
645+
646+
al, _ = parseAllowlistString("http://example.com")
647+
assert.Equal(t, al.Allows("http://example.co"), false)
648+
assert.Equal(t, al.Allows("http://example.com"), true)
649+
assert.Equal(t, al.Allows("http://example.comm"), false)
650+
assert.Equal(t, al.Allows("http://example.com:80"), false)
651+
assert.Equal(t, al.Allows("http://example.com/"), true)
652+
assert.Equal(t, al.Allows("http://example.com/foo"), true)
653+
assert.Equal(t, al.Allows("http://fiz@example.com/"), false)
654+
assert.Equal(t, al.Allows("http://example.com:123/"), false)
655+
assert.Equal(t, al.Allows("https://example.com"), false)
656+
assert.Equal(t, al.Allows("https://example.com/"), false)
657+
assert.Equal(t, al.Allows(""), false)
658+
659+
al, _ = parseAllowlistString("http://example.com, https://example.org/bar/,http://foo:80")
660+
assert.Equal(t, al.Allows("http://example.com"), true)
661+
assert.Equal(t, al.Allows("http://example.com/"), true)
662+
assert.Equal(t, al.Allows("http://example.com/foo"), true)
663+
assert.Equal(t, al.Allows("http://fiz@example.com/"), false)
664+
assert.Equal(t, al.Allows("http://example.com:123/"), false)
665+
assert.Equal(t, al.Allows("https://example.com"), false)
666+
assert.Equal(t, al.Allows("https://example.com/"), false)
667+
assert.Equal(t, al.Allows("http://example.org"), false)
668+
assert.Equal(t, al.Allows("http://example.org/"), false)
669+
assert.Equal(t, al.Allows("http://example.org/foo"), false)
670+
assert.Equal(t, al.Allows("http://fiz@example.org/"), false)
671+
assert.Equal(t, al.Allows("http://example.org:123/"), false)
672+
assert.Equal(t, al.Allows("https://example.org"), false)
673+
assert.Equal(t, al.Allows("https://example.org/"), false)
674+
assert.Equal(t, al.Allows("https://example.org/bar"), true)
675+
assert.Equal(t, al.Allows("https://example.org/barr"), false)
676+
assert.Equal(t, al.Allows("https://example.org/bar/"), true)
677+
assert.Equal(t, al.Allows("https://example.org/bar/baz"), true)
678+
assert.Equal(t, al.Allows("http://foo"), false)
679+
assert.Equal(t, al.Allows("http://foo/"), false)
680+
assert.Equal(t, al.Allows("http://foo:80"), true)
681+
assert.Equal(t, al.Allows("http://foo:80/"), true)
682+
assert.Equal(t, al.Allows("http://foo:8080"), false)
683+
assert.Equal(t, al.Allows("http://foo:8080/"), false)
684+
assert.Equal(t, al.Allows(""), false)
685+
}

0 commit comments

Comments
 (0)