fix: require https VAULT_ADDR and never follow redirects in set-testing
This commit is contained in:
@@ -44,8 +44,9 @@ It mints a JWT from `${VAULT_ADDR}/v1/identity/oidc/token/${ENCAPI_OIDC_ROLE}`
|
||||
(defaults `https://vault.service.consul:8200`, `encapi-testing`) using
|
||||
`VAULT_TOKEN` or `~/.vault-token`, trusting `VAULT_CACERT` when set, then
|
||||
`PUT`s `{"environment":"testing"}` to
|
||||
`${ENCAPI_URL}/api/v1/nodes/<certname>/environment`. `ENCAPI_URL` must be
|
||||
`https`; the bearer JWT is never sent over plain http.
|
||||
`${ENCAPI_URL}/api/v1/nodes/<certname>/environment`. `ENCAPI_URL` and
|
||||
`VAULT_ADDR` must be `https` and redirects are never followed, so neither
|
||||
the Vault token nor the JWT is sent over plain http or to another host.
|
||||
|
||||
## Behaviour (drop-in for the python ENC)
|
||||
|
||||
|
||||
+17
-3
@@ -24,8 +24,8 @@ func setTesting(certname string, out io.Writer) error {
|
||||
if baseURL == "" {
|
||||
baseURL = defaultBaseURL
|
||||
}
|
||||
if parsed, err := url.Parse(baseURL); err != nil || parsed.Scheme != "https" {
|
||||
return fmt.Errorf("set-testing requires an https ENCAPI_URL, got %q", baseURL)
|
||||
if err := requireHTTPS("ENCAPI_URL", baseURL); err != nil {
|
||||
return err
|
||||
}
|
||||
client, err := vaultClient()
|
||||
if err != nil {
|
||||
@@ -55,6 +55,9 @@ func mintToken(client *http.Client) (string, error) {
|
||||
if addr == "" {
|
||||
addr = defaultVaultAddr
|
||||
}
|
||||
if err := requireHTTPS("VAULT_ADDR", addr); err != nil {
|
||||
return "", err
|
||||
}
|
||||
role := os.Getenv("ENCAPI_OIDC_ROLE")
|
||||
if role == "" {
|
||||
role = "encapi-testing"
|
||||
@@ -86,6 +89,13 @@ func mintToken(client *http.Client) (string, error) {
|
||||
return resp.Data.Token, nil
|
||||
}
|
||||
|
||||
func requireHTTPS(name, raw string) error {
|
||||
if parsed, err := url.Parse(raw); err != nil || parsed.Scheme != "https" {
|
||||
return fmt.Errorf("set-testing requires an https %s, got %q", name, raw)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func readVaultToken() (string, error) {
|
||||
if t := os.Getenv("VAULT_TOKEN"); t != "" {
|
||||
return t, nil
|
||||
@@ -102,7 +112,11 @@ func readVaultToken() (string, error) {
|
||||
}
|
||||
|
||||
func vaultClient() (*http.Client, error) {
|
||||
client := &http.Client{Timeout: httpTimeout}
|
||||
client := &http.Client{
|
||||
Timeout: httpTimeout,
|
||||
// Never follow redirects: they would re-send the vault token or JWT.
|
||||
CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse },
|
||||
}
|
||||
caFile := os.Getenv("VAULT_CACERT")
|
||||
if caFile == "" {
|
||||
return client, nil
|
||||
|
||||
+44
-2
@@ -14,7 +14,7 @@ import (
|
||||
|
||||
func fakeVault(t *testing.T, status int, body string) *httptest.Server {
|
||||
t.Helper()
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet || r.URL.Path != "/v1/identity/oidc/token/encapi-testing" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
@@ -27,6 +27,7 @@ func fakeVault(t *testing.T, status int, body string) *httptest.Server {
|
||||
_, _ = io.WriteString(w, body)
|
||||
}))
|
||||
t.Cleanup(srv.Close)
|
||||
trustServer(t, srv)
|
||||
return srv
|
||||
}
|
||||
|
||||
@@ -47,7 +48,8 @@ func fakeEncapi(t *testing.T, status int, seen *seenPut) *httptest.Server {
|
||||
return srv
|
||||
}
|
||||
|
||||
// trustServer points VAULT_CACERT at the TLS server's certificate.
|
||||
// trustServer points VAULT_CACERT at the TLS server's certificate; all
|
||||
// httptest TLS servers share one certificate, so this trusts both fakes.
|
||||
func trustServer(t *testing.T, srv *httptest.Server) {
|
||||
t.Helper()
|
||||
path := filepath.Join(t.TempDir(), "ca.pem")
|
||||
@@ -139,6 +141,46 @@ func TestSetTestingRefusesPlainHTTP(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetTestingRefusesPlainHTTPVault(t *testing.T) {
|
||||
vaultHit := false
|
||||
vault := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { vaultHit = true }))
|
||||
t.Cleanup(vault.Close)
|
||||
var seen seenPut
|
||||
t.Setenv("ENCAPI_URL", fakeEncapi(t, 200, &seen).URL)
|
||||
t.Setenv("VAULT_TOKEN", "vt")
|
||||
|
||||
for _, addr := range []string{vault.URL, "vault.example:8200", "://bad"} {
|
||||
t.Setenv("VAULT_ADDR", addr)
|
||||
err := run([]string{"encapic", "set-testing", "host.example"}, io.Discard)
|
||||
if err == nil || !strings.Contains(err.Error(), "requires an https VAULT_ADDR") {
|
||||
t.Errorf("VAULT_ADDR=%q: err = %v, want https refusal", addr, err)
|
||||
}
|
||||
}
|
||||
if vaultHit || seen.path != "" {
|
||||
t.Errorf("request sent despite non-https VAULT_ADDR: vault=%v encapi=%+v", vaultHit, seen)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetTestingDoesNotFollowRedirect(t *testing.T) {
|
||||
var hits []string
|
||||
encapi := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
hits = append(hits, r.URL.Path+" "+r.Header.Get("Authorization"))
|
||||
http.Redirect(w, r, "/elsewhere", http.StatusTemporaryRedirect)
|
||||
}))
|
||||
t.Cleanup(encapi.Close)
|
||||
t.Setenv("VAULT_ADDR", fakeVault(t, 200, `{"data":{"token":"jwt123"}}`).URL)
|
||||
t.Setenv("VAULT_TOKEN", "vt")
|
||||
t.Setenv("ENCAPI_URL", encapi.URL)
|
||||
|
||||
err := run([]string{"encapic", "set-testing", "host.example"}, io.Discard)
|
||||
if err == nil || !strings.Contains(err.Error(), "HTTP 307") {
|
||||
t.Fatalf("err = %v, want HTTP 307", err)
|
||||
}
|
||||
if len(hits) != 1 {
|
||||
t.Errorf("encapi requests = %v, want exactly one", hits)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetTestingVaultNoToken(t *testing.T) {
|
||||
for _, body := range []string{`{"data":{"token":""}}`, `{"data":{}}`, `{}`} {
|
||||
var seen seenPut
|
||||
|
||||
Reference in New Issue
Block a user