mirror of
https://github.com/hanzoai/ingress.git
synced 2026-10-26 02:47:15 +00:00
edgeSecrets ran on every boot, so an edge that orders no certificates still had to reach KMS before it served anything — including routes whose certificates come from a Kubernetes Secret, and plain-HTTP routes. It now returns the identity seal when no resolver orders certificates, and reaches KMS only when there is ACME state to seal. Adoption covers ONE store. It admits the first unsealed document and the same document again — a store is read more than once before the write that seals it lands, and a replica that cannot write reads it every poll — but not a different one. A seal left adopting was taking whatever plaintext it was handed for as long as the process ran. The setting's comment now says what it does: it writes, it covers one store, and it adopts again after a restart, which is why it is removed once it has done its work. One spelling for the KMS credential. The bare IAM_CLIENT_ID fallback undid the reason the names are prefixed — a pod carrying another subsystem's IAM client had it picked up here. k8s/hanzo/deployment.yaml and service.yaml are deleted. They described a hostNetwork DaemonSet named hanzo-ingress binding hostPort 80 and 443, while what runs in that cluster is a Deployment named ingress declared in universe. `kubectl apply -f k8s/hanzo/` appears in this README and in the sibling gateway's Makefile, so the file was one command from taking :80 and :443 on every node beside the workload that already had them. Universe is the one place that says what runs; the cluster-level objects stay here. The OTLP endpoint paths in the docs and the vendored Knative fixture are restored: /v1/metrics is the OpenTelemetry receiver path, not a route of ours.
197 lines
5.8 KiB
Go
197 lines
5.8 KiB
Go
package kms
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// fakeKMS answers the two calls the client makes, and records what it was
|
|
// asked, so the test can assert the URL shape rather than trusting it.
|
|
type fakeKMS struct {
|
|
token string
|
|
secrets map[string]string
|
|
gotPath string
|
|
gotEnv string
|
|
gotAuth string
|
|
logins int
|
|
}
|
|
|
|
func (f *fakeKMS) server(t *testing.T) *httptest.Server {
|
|
t.Helper()
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/v1/kms/auth/login", func(w http.ResponseWriter, r *http.Request) {
|
|
f.logins++
|
|
var in struct {
|
|
ClientID string `json:"clientId"`
|
|
ClientSecret string `json:"clientSecret"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
if err := json.NewEncoder(w).Encode(map[string]string{"accessToken": f.token}); err != nil {
|
|
t.Error(err)
|
|
}
|
|
})
|
|
mux.HandleFunc("/v1/kms/secrets/", func(w http.ResponseWriter, r *http.Request) {
|
|
f.gotPath = strings.TrimPrefix(r.URL.Path, "/v1/kms/secrets/")
|
|
f.gotEnv = r.URL.Query().Get("env")
|
|
f.gotAuth = r.Header.Get("Authorization")
|
|
value, ok := f.secrets[f.gotPath]
|
|
if !ok {
|
|
http.Error(w, `{"error":"not found"}`, http.StatusNotFound)
|
|
return
|
|
}
|
|
if err := json.NewEncoder(w).Encode(map[string]any{"secret": map[string]string{"value": value}}); err != nil {
|
|
t.Error(err)
|
|
}
|
|
})
|
|
srv := httptest.NewServer(mux)
|
|
t.Cleanup(srv.Close)
|
|
return srv
|
|
}
|
|
|
|
// configured points a client at a test server. It builds the struct rather
|
|
// than going through FromEnv because these tests are about what goes over the
|
|
// wire, and FromEnv is about what an operator configured — including that the
|
|
// endpoint is https, which a test server on loopback is not.
|
|
func configured(t *testing.T, endpoint string) *Client {
|
|
t.Helper()
|
|
return &Client{
|
|
endpoint: endpoint,
|
|
clientID: "ingress",
|
|
secret: "shhh",
|
|
org: "hanzo",
|
|
env: "default",
|
|
path: "ingress",
|
|
http: &http.Client{Timeout: 5 * time.Second},
|
|
}
|
|
}
|
|
|
|
// The client secret is in the body of the login and the bearer is in a header
|
|
// of every read, so a plaintext endpoint is a configuration to fix rather than
|
|
// a degraded mode to run in.
|
|
func TestFromEnv_RequiresHTTPS(t *testing.T) {
|
|
t.Setenv(envClientID, "ingress")
|
|
t.Setenv(envSecret, "shhh")
|
|
|
|
t.Setenv(envEndpoint, "http://kms.hanzo.ai")
|
|
if _, err := FromEnv(); err == nil {
|
|
t.Error("FromEnv accepted a plaintext endpoint")
|
|
}
|
|
|
|
t.Setenv(envEndpoint, "https://kms.hanzo.ai")
|
|
c, err := FromEnv()
|
|
if err != nil || c == nil {
|
|
t.Errorf("FromEnv(https) = (%v, %v), want a client", c, err)
|
|
}
|
|
}
|
|
|
|
func TestClient_Get(t *testing.T) {
|
|
f := &fakeKMS{token: "bearer-value", secrets: map[string]string{
|
|
"ingress/" + AccountSeal: strings.Repeat("0f", 32),
|
|
}}
|
|
srv := f.server(t)
|
|
c := configured(t, srv.URL)
|
|
|
|
got, err := c.Get(context.Background(), AccountSeal)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got != f.secrets["ingress/"+AccountSeal] {
|
|
t.Errorf("value = %q", got)
|
|
}
|
|
if f.gotPath != "ingress/"+AccountSeal {
|
|
t.Errorf("path = %q, want %q", f.gotPath, "ingress/"+AccountSeal)
|
|
}
|
|
if f.gotEnv != "default" {
|
|
t.Errorf("env = %q, want default", f.gotEnv)
|
|
}
|
|
if f.gotAuth != "Bearer bearer-value" {
|
|
t.Errorf("authorization = %q", f.gotAuth)
|
|
}
|
|
}
|
|
|
|
// A missing secret is an error, never an empty value: an empty sealing key
|
|
// would build a seal that protects nothing.
|
|
func TestClient_MissingSecretIsAnError(t *testing.T) {
|
|
f := &fakeKMS{token: "t", secrets: map[string]string{}}
|
|
c := configured(t, f.server(t).URL)
|
|
if _, err := c.Get(context.Background(), AccountSeal); err == nil {
|
|
t.Error("a missing secret returned no error")
|
|
}
|
|
}
|
|
|
|
// Unconfigured is not an error — that deployment reads no secrets from KMS and
|
|
// the caller decides what that means.
|
|
func TestFromEnv_Unconfigured(t *testing.T) {
|
|
t.Setenv(envEndpoint, "")
|
|
c, err := FromEnv()
|
|
if err != nil || c != nil {
|
|
t.Errorf("FromEnv() = (%v, %v), want (nil, nil)", c, err)
|
|
}
|
|
}
|
|
|
|
// Half-configured IS an error: an endpoint with no credentials is a deployment
|
|
// that meant to use KMS and would otherwise start without it.
|
|
func TestFromEnv_HalfConfiguredIsAnError(t *testing.T) {
|
|
t.Setenv(envEndpoint, "https://kms.hanzo.ai")
|
|
t.Setenv(envClientID, "")
|
|
t.Setenv(envSecret, "")
|
|
t.Setenv("IAM_CLIENT_ID", "")
|
|
t.Setenv("IAM_CLIENT_SECRET", "")
|
|
if _, err := FromEnv(); err == nil {
|
|
t.Error("an endpoint with no credentials started without error")
|
|
}
|
|
}
|
|
|
|
// One spelling for the credential. A pod that also carries another subsystem's
|
|
// IAM client must not have it picked up here.
|
|
func TestFromEnv_TakesOnlyItsOwnCredential(t *testing.T) {
|
|
t.Setenv(envEndpoint, "https://kms.hanzo.ai")
|
|
t.Setenv(envClientID, "")
|
|
t.Setenv(envSecret, "")
|
|
t.Setenv("IAM_CLIENT_ID", "someone-elses")
|
|
t.Setenv("IAM_CLIENT_SECRET", "someone-elses")
|
|
if _, err := FromEnv(); err == nil {
|
|
t.Error("FromEnv used a credential that was not addressed to it")
|
|
}
|
|
|
|
t.Setenv(envClientID, "ingress")
|
|
t.Setenv(envSecret, "shhh")
|
|
t.Setenv(envOrg, "lux")
|
|
c, err := FromEnv()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if c.clientID != "ingress" {
|
|
t.Errorf("clientID = %q, want ingress", c.clientID)
|
|
}
|
|
if c.Org() != "lux" {
|
|
t.Errorf("org = %q, want lux", c.Org())
|
|
}
|
|
}
|
|
|
|
// The login body carries the client secret. It must never reach an error
|
|
// string, because that string goes to a log.
|
|
func TestClient_LoginFailureDoesNotEchoTheBody(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
http.Error(w, `{"error":"invalid credentials","clientSecret":"shhh"}`, http.StatusUnauthorized)
|
|
}))
|
|
t.Cleanup(srv.Close)
|
|
c := configured(t, srv.URL)
|
|
|
|
_, err := c.Get(context.Background(), AccountSeal)
|
|
if err == nil {
|
|
t.Fatal("a failed login returned no error")
|
|
}
|
|
if strings.Contains(err.Error(), "shhh") {
|
|
t.Errorf("the login error carries the credential: %v", err)
|
|
}
|
|
}
|