fix: refuse non-https encapi URL in set-testing; cover vault CA and token errors
This commit is contained in:
@@ -44,7 +44,8 @@ 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}/api/v1/nodes/<certname>/environment`. `ENCAPI_URL` must be
|
||||
`https`; the bearer JWT is never sent over plain http.
|
||||
|
||||
## Behaviour (drop-in for the python ENC)
|
||||
|
||||
|
||||
+7
-4
@@ -20,6 +20,13 @@ const defaultVaultAddr = "https://vault.service.consul:8200"
|
||||
// setTesting pins certname to the testing environment via encapi, using a
|
||||
// Vault identity OIDC token for the encapi-testing role.
|
||||
func setTesting(certname string, out io.Writer) error {
|
||||
baseURL := os.Getenv("ENCAPI_URL")
|
||||
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)
|
||||
}
|
||||
client, err := vaultClient()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -29,10 +36,6 @@ func setTesting(certname string, out io.Writer) error {
|
||||
return err
|
||||
}
|
||||
|
||||
baseURL := os.Getenv("ENCAPI_URL")
|
||||
if baseURL == "" {
|
||||
baseURL = defaultBaseURL
|
||||
}
|
||||
u := strings.TrimRight(baseURL, "/") + "/api/v1/nodes/" + url.PathEscape(certname) + "/environment"
|
||||
req, err := http.NewRequest(http.MethodPut, u, bytes.NewBufferString(`{"environment":"testing"}`))
|
||||
if err != nil {
|
||||
|
||||
+109
-1
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/pem"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
@@ -33,7 +34,7 @@ type seenPut struct{ path, auth, body string }
|
||||
|
||||
func fakeEncapi(t *testing.T, status int, seen *seenPut) *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) {
|
||||
b, _ := io.ReadAll(r.Body)
|
||||
if r.Method == http.MethodPut {
|
||||
*seen = seenPut{r.URL.Path, r.Header.Get("Authorization"), string(b)}
|
||||
@@ -42,9 +43,21 @@ func fakeEncapi(t *testing.T, status int, seen *seenPut) *httptest.Server {
|
||||
_, _ = io.WriteString(w, `{"error":"forbidden"}`)
|
||||
}))
|
||||
t.Cleanup(srv.Close)
|
||||
trustServer(t, srv)
|
||||
return srv
|
||||
}
|
||||
|
||||
// trustServer points VAULT_CACERT at the TLS server's certificate.
|
||||
func trustServer(t *testing.T, srv *httptest.Server) {
|
||||
t.Helper()
|
||||
path := filepath.Join(t.TempDir(), "ca.pem")
|
||||
b := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: srv.Certificate().Raw})
|
||||
if err := os.WriteFile(path, b, 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Setenv("VAULT_CACERT", path)
|
||||
}
|
||||
|
||||
func TestSetTestingSuccess(t *testing.T) {
|
||||
var seen seenPut
|
||||
t.Setenv("VAULT_ADDR", fakeVault(t, 200, `{"data":{"token":"jwt123"}}`).URL)
|
||||
@@ -106,3 +119,98 @@ func TestSetTestingEncapiForbidden(t *testing.T) {
|
||||
t.Fatalf("err = %v, want HTTP 403 with body", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetTestingRefusesPlainHTTP(t *testing.T) {
|
||||
vaultHit := false
|
||||
vault := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { vaultHit = true }))
|
||||
t.Cleanup(vault.Close)
|
||||
t.Setenv("VAULT_ADDR", vault.URL)
|
||||
t.Setenv("VAULT_TOKEN", "vt")
|
||||
|
||||
for _, encapiURL := range []string{"http://encapi.example", "", "encapi.example", "://bad"} {
|
||||
t.Setenv("ENCAPI_URL", encapiURL)
|
||||
err := run([]string{"encapic", "set-testing", "host.example"}, io.Discard)
|
||||
if err == nil || !strings.Contains(err.Error(), "requires an https ENCAPI_URL") {
|
||||
t.Errorf("ENCAPI_URL=%q: err = %v, want https refusal", encapiURL, err)
|
||||
}
|
||||
}
|
||||
if vaultHit {
|
||||
t.Error("vault called despite non-https ENCAPI_URL")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetTestingVaultNoToken(t *testing.T) {
|
||||
for _, body := range []string{`{"data":{"token":""}}`, `{"data":{}}`, `{}`} {
|
||||
var seen seenPut
|
||||
t.Setenv("VAULT_ADDR", fakeVault(t, 200, body).URL)
|
||||
t.Setenv("VAULT_TOKEN", "vt")
|
||||
t.Setenv("ENCAPI_URL", fakeEncapi(t, 200, &seen).URL)
|
||||
|
||||
err := run([]string{"encapic", "set-testing", "host.example"}, io.Discard)
|
||||
if err == nil || !strings.Contains(err.Error(), "no OIDC token") {
|
||||
t.Errorf("body %s: err = %v, want no OIDC token", body, err)
|
||||
}
|
||||
if seen.path != "" {
|
||||
t.Errorf("body %s: encapi called: %+v", body, seen)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetTestingNoVaultToken(t *testing.T) {
|
||||
var seen seenPut
|
||||
t.Setenv("HOME", t.TempDir())
|
||||
t.Setenv("VAULT_TOKEN", "")
|
||||
t.Setenv("VAULT_ADDR", fakeVault(t, 200, `{"data":{"token":"jwt123"}}`).URL)
|
||||
t.Setenv("ENCAPI_URL", fakeEncapi(t, 200, &seen).URL)
|
||||
|
||||
err := run([]string{"encapic", "set-testing", "host.example"}, io.Discard)
|
||||
if err == nil || !strings.Contains(err.Error(), "no VAULT_TOKEN and cannot read ~/.vault-token") {
|
||||
t.Fatalf("err = %v, want missing vault token", err)
|
||||
}
|
||||
if seen.path != "" {
|
||||
t.Errorf("encapi called: %+v", seen)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVaultClientCACertUnreadable(t *testing.T) {
|
||||
t.Setenv("VAULT_CACERT", filepath.Join(t.TempDir(), "missing.pem"))
|
||||
if _, err := vaultClient(); err == nil || !strings.Contains(err.Error(), "read VAULT_CACERT") {
|
||||
t.Fatalf("err = %v, want read VAULT_CACERT", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVaultClientCACertNoPEM(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "ca.pem")
|
||||
if err := os.WriteFile(path, []byte("not a certificate\n"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Setenv("VAULT_CACERT", path)
|
||||
if _, err := vaultClient(); err == nil || !strings.Contains(err.Error(), "no certificates in VAULT_CACERT") {
|
||||
t.Fatalf("err = %v, want no certificates", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVaultClientCACertTrustsServer(t *testing.T) {
|
||||
srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
|
||||
t.Cleanup(srv.Close)
|
||||
|
||||
t.Setenv("VAULT_CACERT", "")
|
||||
plain, err := vaultClient()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := plain.Get(srv.URL); err == nil {
|
||||
t.Fatal("untrusted TLS server accepted without VAULT_CACERT")
|
||||
}
|
||||
|
||||
trustServer(t, srv)
|
||||
client, err := vaultClient()
|
||||
if err != nil {
|
||||
t.Fatalf("vaultClient: %v", err)
|
||||
}
|
||||
resp, err := client.Get(srv.URL)
|
||||
if err != nil {
|
||||
t.Fatalf("GET with VAULT_CACERT: %v", err)
|
||||
}
|
||||
_ = resp.Body.Close()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user