diff --git a/api/v1alpha1/agentgateway/agentgateway_policy_types.go b/api/v1alpha1/agentgateway/agentgateway_policy_types.go index 99bbe9a2ba2..bd703824c0c 100644 --- a/api/v1alpha1/agentgateway/agentgateway_policy_types.go +++ b/api/v1alpha1/agentgateway/agentgateway_policy_types.go @@ -540,22 +540,21 @@ type JWKS struct { Inline *string `json:"inline,omitempty"` } -// +kubebuilder:validation:ExactlyOneOf=uri;backendRef type RemoteJWKS struct { - // IdP jwks endpoint. Default tls settings are used to connect to this url. - // +kubebuilder:validation:Pattern=`^(https|http):\/\/[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?)*(:\d+)?\/.*$` - // +optional - JwksUri string `json:"uri,omitempty"` + // Path to IdP jwks endpoint. Default tls settings are used to connect to this url. + // +required + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=2000 + JwksPath string `json:"jwksPath"` // +optional // +kubebuilder:validation:XValidation:rule="matches(self, '^([0-9]{1,5}(h|m|s|ms)){1,4}$')",message="invalid duration value" // +kubebuilder:validation:XValidation:rule="duration(self) >= duration('5m')",message="cacheDuration must be at least 5m." // +kubebuilder:default="5m" CacheDuration *metav1.Duration `json:"cacheDuration,omitempty"` // backendRef references the remote JWKS server to reach. - // Not implemented yet, only jwksUri is currently supported. // Supported types: Service and Backend. - // +optional - BackendRef gwv1.BackendObjectReference `json:"backendRef,omitempty"` + // +required + BackendRef gwv1.BackendObjectReference `json:"backendRef"` } // +kubebuilder:validation:Enum=Strict;Optional diff --git a/hack/dummy-idp/dummy-idp.cert b/hack/dummy-idp/dummy-idp.cert index 805a6f6d683..c9409f42baf 100644 --- a/hack/dummy-idp/dummy-idp.cert +++ b/hack/dummy-idp/dummy-idp.cert @@ -1,21 +1,32 @@ -----BEGIN CERTIFICATE----- -MIIDZTCCAk2gAwIBAgIUdgqhaX5KZBbpKLFaKBTq+CyF5r4wDQYJKoZIhvcNAQEL -BQAwMzEVMBMGA1UECgwMa2dhdGV3YXkuZGV2MRowGAYDVQQDDBFkdW1teS1pZHAu -ZGVmYXVsdDAeFw0yNTExMTkxODQ0MDZaFw0zNTExMTcxODQ0MDZaMDMxFTATBgNV -BAoMDGtnYXRld2F5LmRldjEaMBgGA1UEAwwRZHVtbXktaWRwLmRlZmF1bHQwggEi -MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCveFzIPXbf8b89On8VHtgEDabs -Ghm52K9Hv2/9nHIaEzrnl67undW2onVBI/IMhrkPQXL59dTyw3lw1KQwtDbPLbHJ -vPUxX5pYE8HDCinMpxgueJQeXjGjA3B0zMzAGKGy1afSFkoemPgWGk3z9+RA31ZO -im+pdLu1a/7es+AxVzassnVvN3NBuVr4Baxj6f2MIaUHrnp31fmZZhEFDUajlPxx -/dfVe+eowV70rdSjGi1r4/HKeEfobWUJSTNYSEHttPIHl+i7NW/ArzKjZDb5gNML -Jzr3/mbMGB0QS60iDuMaCi7rzKdalPhNBEB5VMTxXnleFXkrWmHCFGL3Q4f7AgMB -AAGjcTBvMB0GA1UdDgQWBBTd/irgnmCNgU6tVEmTvAwB9PITQzAfBgNVHSMEGDAW -gBTd/irgnmCNgU6tVEmTvAwB9PITQzAPBgNVHRMBAf8EBTADAQH/MBwGA1UdEQQV -MBOCEWR1bW15LWlkcC5kZWZhdWx0MA0GCSqGSIb3DQEBCwUAA4IBAQBW6S8mMJsg -86mzDx3mSRy8sRHKfA83cbrouyQDOQn26QEFoK9pz9M67G19F87VQVzV/je4QySq -XizOoPQtMGRTjUZwumG3tTEKd7lxy2FadDWZq/NQkhs5Y5iyipfSJV8Iejs5yjDI -iqrPxehOO4MlGyUZDWXEt4mKdUZ67SdcLlUUZEv4jVbOkpgVrastJXwwdnbQaH/9 -6wPzcCTVcAeee1Jf0E5uTDg62skWAuuDIN0n3pQdIdB2vikinAE3rpELivrCs+Ae -uI9xYJwlp4Z3QeP2651npVTMfpyxqYz5Mk7Hvze0U/WArEAY1wRgQgbYB0MT+ITl -u7KRkPMMak84 +MIIFfDCCA2SgAwIBAgIUOBEwNkgGCBk5gTlks4MgZjBwcB0wDQYJKoZIhvcNAQEL +BQAwKzEpMCcGA1UEAwwgZHVtbXktaWRwLmRlZmF1bHQsTz1rZ2F0ZXdheS5kZXYw +HhcNMjUxMjEyMjIyNTAyWhcNMzUxMjEwMjIyNTAyWjArMSkwJwYDVQQDDCBkdW1t +eS1pZHAuZGVmYXVsdCxPPWtnYXRld2F5LmRldjCCAiIwDQYJKoZIhvcNAQEBBQAD +ggIPADCCAgoCggIBAKPDXO2JEDlruWLQACZqQyFoJTw9dUpay+QcVrgnDv8ULM9F +wSVpIgiT7/reqfWQsyWH8bhyZ60SD2v6BqRdvU8d5G7Lzjjiv7D1kRmdoM05rHeW +rFWrMsd3tTVYIdkDwsOqb/4/3YXhzZstI8N9I9mqQFfR0Oahjwub1fQqGkU4AldO +WGTgsllI0ZDV8IDuARlOQ8ZysxL2axxXJ4Io4eDMZ6uwbeW5JXv/ajLz3Gx9vpWf +LlfPHCB4/Z+EErw/g55PEM8ftvK5ijT2+QPULSdrkO/YjByV9IPNjYou9JEcI1KP +Q2q4VcjQV83dcRFDw11o6MhOicVNwdTFBia6aStpxU/fsYaoaPiK0OWOZ3SjtoNV +PT17geh5kX+4eTmzdC/9hFh+qncyzfHdomBFQlamQ5Pzg3ngLoNm5Iyk/OuUgLg8 +sgYf7coYDygzzagxxpTRS7VyfwqLlMaRbqBUrX9IHVpn17CqtsrI1ihadv9q4wc3 +Mxt2rdT1GfpE7yCB/NrAzCe2ZVWkNkX8Zb0taD79r/daOBgakHf9L/EqYTsgGO3s +XiF7G3lbRpLwOKHiHP9YbQCdoh8Y3qzGi9DLlmDIaQShtJPUmCb7u7kL9bW2SPRL ++zH2ZY5258CZWndAGe06wQVgLv0aI7kre+Sf1YfZxRbzE595TBWQO/RRT3I7AgMB +AAGjgZcwgZQwHQYDVR0OBBYEFAIkfyn6riDFT/LhatXG1uS5u8HKMB8GA1UdIwQY +MBaAFAIkfyn6riDFT/LhatXG1uS5u8HKMA8GA1UdEwEB/wQFMAMBAf8wQQYDVR0R +BDowOIIRZHVtbXktaWRwLmRlZmF1bHSCI2R1bW15LWlkcC5kZWZhdWx0LnN2Yy5j +bHVzdGVyLmxvY2FsMA0GCSqGSIb3DQEBCwUAA4ICAQAxzxHhT9uvTBHKeu+7zOdU +A+rju5gPjeItds3r2YdHqqjidkK53qWnvrqteoguT8lxGXaSL0QzL3l9eFp80BIP +8MmlI+zs8Q/cO9gCeEf+3ul+nx2YzF33W/PNahHfLDbLIFDoQMkhTyemEh1GEqmm +6frHgO2OgdIO6jyIF0GN0SFvCW6J32k3teRsN2OLRQCuCftJ/Q2dwuXZfmx0sf0R +Hz7JNBdH9U8iCYhSefd3VWCro2sPB3XT7evH9+orFikvbb5fggo4WGjvc7CPKlMj +59PGlloJCUP9FIhR5/oot6yH9NsdOzDWY51makMhE4nq/ET8omaawSCclTE8mDWk ++s/8MBQkk6T72zaVX6Eqnb0RatIHkr9C6zfy/ZE4E5A6Lw+EwdGPaXg5pCBO0miM +jImoFyNvXEGWY3w6AX8ho1L27ZiTApMTc2fYUYCy4QP+MDjEp1+yFrjFSFpUhF0Z ++Tl37cUWZcm4nUxEcu/pfedKyliR2yKBfi3jg7cDzVB86tSHzIvPgxpl2ivEEb0E +ohncCC1Z//SKb7QFs1Obry3hIIBpEyVVvGB580AdxgLY9nhrvv/6gw01JtEPXczV +1BTCWIUc6WafBlAiWrm3tR36kaRn2RrIlCAFrMznQMafCfMLCTWsYudkrabl7W9n +yamda6yFfH9bkPO+XBK3lQ== -----END CERTIFICATE----- diff --git a/hack/dummy-idp/dummy-idp.go b/hack/dummy-idp/dummy-idp.go index 2890331befb..73a7731bdd2 100644 --- a/hack/dummy-idp/dummy-idp.go +++ b/hack/dummy-idp/dummy-idp.go @@ -5,8 +5,16 @@ import ( "crypto/x509" "log" "net/http" + + _ "embed" ) +//go:embed dummy-idp.cert +var cert []byte + +//go:embed dummy-idp.key +var key []byte + func main() { roots := x509.NewCertPool() if !roots.AppendCertsFromPEM(cert) { @@ -31,6 +39,10 @@ func main() { w.Header().Add("content-type", "application/json") w.Write(orgThreeJwks) }) + mux.HandleFunc("/org-four/keys", func(w http.ResponseWriter, req *http.Request) { + w.Header().Add("content-type", "application/json") + w.Write(orgFourJwks) + }) mux.HandleFunc("/org-one/jwt", func(w http.ResponseWriter, req *http.Request) { w.Header().Add("content-type", "application/json") w.Write(orgOneJwt) @@ -43,6 +55,10 @@ func main() { w.Header().Add("content-type", "application/json") w.Write(orgThreeJwt) }) + mux.HandleFunc("/org-four/jwt", func(w http.ResponseWriter, req *http.Request) { + w.Header().Add("content-type", "application/json") + w.Write(orgFourJwt) + }) cfg := &tls.Config{ RootCAs: roots, @@ -61,70 +77,6 @@ func main() { } var ( - // self-signed cert with: - // Issuer: O=kgateway.dev, CN=dummy-idp.default - // Validity - // - // Not Before: Nov 19 18:44:06 2025 GMT - // Not After : Nov 17 18:44:06 2035 GMT - // - // Subject: O=kgateway.dev, CN=dummy-idp.default - // ... - // X509v3 extensions: - // - // X509v3 Subject Alternative Name: - // DNS:dummy-idp.default - cert = []byte(`-----BEGIN CERTIFICATE----- -MIIDZTCCAk2gAwIBAgIUdgqhaX5KZBbpKLFaKBTq+CyF5r4wDQYJKoZIhvcNAQEL -BQAwMzEVMBMGA1UECgwMa2dhdGV3YXkuZGV2MRowGAYDVQQDDBFkdW1teS1pZHAu -ZGVmYXVsdDAeFw0yNTExMTkxODQ0MDZaFw0zNTExMTcxODQ0MDZaMDMxFTATBgNV -BAoMDGtnYXRld2F5LmRldjEaMBgGA1UEAwwRZHVtbXktaWRwLmRlZmF1bHQwggEi -MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCveFzIPXbf8b89On8VHtgEDabs -Ghm52K9Hv2/9nHIaEzrnl67undW2onVBI/IMhrkPQXL59dTyw3lw1KQwtDbPLbHJ -vPUxX5pYE8HDCinMpxgueJQeXjGjA3B0zMzAGKGy1afSFkoemPgWGk3z9+RA31ZO -im+pdLu1a/7es+AxVzassnVvN3NBuVr4Baxj6f2MIaUHrnp31fmZZhEFDUajlPxx -/dfVe+eowV70rdSjGi1r4/HKeEfobWUJSTNYSEHttPIHl+i7NW/ArzKjZDb5gNML -Jzr3/mbMGB0QS60iDuMaCi7rzKdalPhNBEB5VMTxXnleFXkrWmHCFGL3Q4f7AgMB -AAGjcTBvMB0GA1UdDgQWBBTd/irgnmCNgU6tVEmTvAwB9PITQzAfBgNVHSMEGDAW -gBTd/irgnmCNgU6tVEmTvAwB9PITQzAPBgNVHRMBAf8EBTADAQH/MBwGA1UdEQQV -MBOCEWR1bW15LWlkcC5kZWZhdWx0MA0GCSqGSIb3DQEBCwUAA4IBAQBW6S8mMJsg -86mzDx3mSRy8sRHKfA83cbrouyQDOQn26QEFoK9pz9M67G19F87VQVzV/je4QySq -XizOoPQtMGRTjUZwumG3tTEKd7lxy2FadDWZq/NQkhs5Y5iyipfSJV8Iejs5yjDI -iqrPxehOO4MlGyUZDWXEt4mKdUZ67SdcLlUUZEv4jVbOkpgVrastJXwwdnbQaH/9 -6wPzcCTVcAeee1Jf0E5uTDg62skWAuuDIN0n3pQdIdB2vikinAE3rpELivrCs+Ae -uI9xYJwlp4Z3QeP2651npVTMfpyxqYz5Mk7Hvze0U/WArEAY1wRgQgbYB0MT+ITl -u7KRkPMMak84 ------END CERTIFICATE-----`) - - key = []byte(`-----BEGIN PRIVATE KEY----- -MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCveFzIPXbf8b89 -On8VHtgEDabsGhm52K9Hv2/9nHIaEzrnl67undW2onVBI/IMhrkPQXL59dTyw3lw -1KQwtDbPLbHJvPUxX5pYE8HDCinMpxgueJQeXjGjA3B0zMzAGKGy1afSFkoemPgW -Gk3z9+RA31ZOim+pdLu1a/7es+AxVzassnVvN3NBuVr4Baxj6f2MIaUHrnp31fmZ -ZhEFDUajlPxx/dfVe+eowV70rdSjGi1r4/HKeEfobWUJSTNYSEHttPIHl+i7NW/A -rzKjZDb5gNMLJzr3/mbMGB0QS60iDuMaCi7rzKdalPhNBEB5VMTxXnleFXkrWmHC -FGL3Q4f7AgMBAAECggEAN/sZ+sZlQRAi15lshuP2veBPI182WuzvCuBgDdTWMEx3 -TADADh+r2Z8d8oDRzb5Dl0LluCp+XE8R1PX6RhGQtOtan8aZoT1jg/sVo7B/4mti -0xykAMZDZsMHozPdlOGm4OO6LVPwJK/f4klVGmM5XpsCMX+IHVOBOWGxiAJyIbsu -Nadg0IXREomEIJK7p+kVAxBEYkWddIiCSJ5xfeLHPls1VG1FRzT+0Xq5MYih5BkM -JYBmmm8Iofg/yJb0SMMW5JRWBzTK0gwni8s6qq+mLXWmuSNnqh7TwPwEUO3Cxg44 -QLxlXnMJn8lgthjPbxAGs+uSYWcwQbBc02p0EQe0KQKBgQDko2ifLJhXkGhFSCBZ -qHF8I6TOumzicsVlP/tm1JXTrtPoEBdyIcMb+KTnUxTxfIOiFb9GtdYTLZW6xQEU -nutAXezRK25drZtTlSEiTWvZFVAFO2Io+S5ZtOLi+N6lp7We49a5uvvjavJrjAAp -b95zyrjK/VFdifXaRIG1ZcB9aQKBgQDEeBZ8myYdENRo8zt/n15El184Tpc/tAkb -XEEjddrsIz6ekIeEclFwaRi00p4UYV2EA5tmx8u651zIcZAF/rnix1Kz4JDuAOlc -+KEnBL81SIybHMzdq8smJmMO2NQSHSrKjsCtQDzb7INEUd9VaiiEOHoaCcqL1ZfA -SUUhx+bZwwKBgQDg37+k3q2vYf7MNZZr2HpVyJDuKvmw94Uign13tBrwqoENO9Zz -kLVfq3w1cMemg/rLzmvk1i+JiUo8+kqHx45GLpsfV4IjbP7ahFCkdlVem9Gqc6+l -8P8fiAOnjXMepwbBEgI9hqT8FlH8aSQ3nSnD5V0/eUsvnuNKHBsfGMbsEQKBgDQ5 -q9iRsW72g1AmoAFLztYy2sfv9Dql0+nm+xW/BWPR9ppV1wA5FzbnaP7gIc9PFnm6 -L7wBjkFvsPVDYsKFNMp4q55PKpdpvJ7PJJ9nnqA+Wcn9vOOMACNy/s/6iV0LTc2s -ZsFnGwZm93nYvaJJ5t1G2gZD5giHzZ/6mhrhtZbbAoGALWewB6J4CkCNAS0qJK85 -I6LrayNR7IcsaUKvLgaggg1FejZRPHkGECjsDfsGpcJAhAYZSBz6ToXODPdSCDJz -A2hJwkkTrX6zaKDhkDXvVhiz0JB6win4CBnU/zKzf6PExraX+Vd+pWzdSxCBupDl -TFwmV3iuItdcF82i2fBZlXk= ------END PRIVATE KEY-----`) - // jwks and jwts were generated using hack/utils/jwt/jwt-generator.go // jwts are valid until Aug 2035 // "iss": "https://kgateway.dev", @@ -137,4 +89,8 @@ TFwmV3iuItdcF82i2fBZlXk= orgThreeJwks = []byte(`{"keys":[{"use":"sig","kty":"RSA","kid":"8879871533137308459","n":"sjnFKA9NxpP39HykPZX6BqiFXmAAMC0YJ1WC2t_2Vo1kXbI64Pb__eKoGaT2my1xedCqnJVyWDjiRSHSzmiJkJ4_h8d62mzCVN2y3mMCDL75OFjz6Hyn2p5dWoIZ0b5SCiZNvBUxJ6ccN51qctzAeReeMP_xM8sWRAN-Xnp8JCltKLv2Kwme5U7UXwzxUxMJsbm6ZMFy-IUMDdmIHgHkIi8-AIvnP0ddtiH_MrJQ6bMwNjecRJ-f1Ut2FVhVTpLiU43UUYExEHLtMXl60ph0RI0mD--FvNmVaYPsysX7FejR49FyCOiCMznOrc_nnKB0M7oggvmjAr8dGghMmL_7VQ","e":"AQAB","x5c":["MIIC3jCCAcagAwIBAgIBIzANBgkqhkiG9w0BAQsFADAXMRUwEwYDVQQKEwxrZ2F0ZXdheS5kZXYwHhcNMjUxMTE5MTkxMjU4WhcNMjUxMTE5MjExMjU4WjAXMRUwEwYDVQQKEwxrZ2F0ZXdheS5kZXYwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCyOcUoD03Gk/f0fKQ9lfoGqIVeYAAwLRgnVYLa3/ZWjWRdsjrg9v/94qgZpPabLXF50KqclXJYOOJFIdLOaImQnj+Hx3rabMJU3bLeYwIMvvk4WPPofKfanl1aghnRvlIKJk28FTEnpxw3nWpy3MB5F54w//EzyxZEA35eenwkKW0ou/YrCZ7lTtRfDPFTEwmxubpkwXL4hQwN2YgeAeQiLz4Ai+c/R122If8yslDpszA2N5xEn5/VS3YVWFVOkuJTjdRRgTEQcu0xeXrSmHREjSYP74W82ZVpg+zKxfsV6NHj0XII6IIzOc6tz+ecoHQzuiCC+aMCvx0aCEyYv/tVAgMBAAGjNTAzMA4GA1UdDwEB/wQEAwIFoDATBgNVHSUEDDAKBggrBgEFBQcDATAMBgNVHRMBAf8EAjAAMA0GCSqGSIb3DQEBCwUAA4IBAQCB8Dj9WYuJ5bK89WNtCQw8XKlBIOUwUyYxU2X5bvIqQPRnOyBR62GaFDY3ER3gdCqVVwcW01cpBHk91cTPdZnWh5wnFTrQuUUA65FcbN8haNIY75OfCQmxxob+yPNJB1wqvTXcUXcF4lN7/7LVpy5jbaJDdWmIKhDPXumgb+pjNsN4VwsF5vbtkdXEDwfA9/BI2POyjlstbz1aYwvrLM6KlOFkE/2oq9r1IksMMg9RIHhAHX1vEDrmxGYdYmPF/mHpQzBu9vdgCUx2pR11vvShc7T2JxaZrsTB0eA4Zli6CayOjWJQILBGxt5btUJxNjKCAwTyaq87iY4CwtxB2jip"]}]}`) orgThreeJwt = []byte(`eyJhbGciOiJSUzI1NiIsImtpZCI6Ijg4Nzk4NzE1MzMxMzczMDg0NTkiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2tnYXRld2F5LmRldiIsInN1YiI6Imlnbm9yZUBrZ2F0ZXdheS5kZXYiLCJleHAiOjIwNzExNjM1NzgsIm5iZiI6MTc2MzU3OTU3OCwiaWF0IjoxNzYzNTc5NTc4fQ.IOrJpU5RY8uhU403MiwRuSa5u6SHAtTeGkTEzn9Hg1DH963AH0NAOMfhx4orSKYbqKhjCPfo-cpKpxizafKFP6j9Ln4Is8ycfk9oPC8Sor_GfhAsJuK3N8fC8mnhm5xQMGk9XErvn9ZY4FCXxpK8vUUMUNUhIsE_zKxJR_Wt6HQ43SGaxuLggR5ETbLvSMDESJEuUdeY_fB_5tYaAznYxOLJ4zp87gKeFPPmEqyzISnRgcEHpyev7BM88uRQGrvF34AiWZO2uDuDGv5zJF9dFm_HQ4-QPe7xEZPvj9w_mbSRQn_RilE2mXduXcU1t-XLxFUVmYj2poiAuUXpwLciXw`) + + orgFourJwks = []byte(`{"keys":[{"use":"sig","kty":"RSA","kid":"292910025153196340","n":"pq97a9fOT8ycnVo_xREFh4TW3Fo-zM-tk5xOxWv2rXRz1fWauxrKdTNaX8FgqKy8Pt2Y7UaWQQRnUnalPARBcPbYShTzOf1GbzIhwgjPbUTtD0WzeVVHk9so76Ab95O2kfaKhpWEnne43g06LKXKQMqOOUttXGjL6YzJT0F59oo5N-Je--XEDtV_QCfb3Qh73QbRO29rw7SAJePse32gKYB7-F1IGZm_P8S7nEXqZ1ZwudBifyQ7KBiP6PsKhonWZRA_4ocSTIwADnsU1VUACxi1FaS2rYl16t6UzT-uzYdhaVWlcRcJblsM66TZPDLwGZxw9IFgx9QAsIeZ_YAcKw","e":"AQAB","x5c":["MIIC3jCCAcagAwIBAgIBWzANBgkqhkiG9w0BAQsFADAXMRUwEwYDVQQKEwxrZ2F0ZXdheS5kZXYwHhcNMjUxMjEyMjA1NjE5WhcNMjUxMjEyMjI1NjE5WjAXMRUwEwYDVQQKEwxrZ2F0ZXdheS5kZXYwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCmr3tr185PzJydWj/FEQWHhNbcWj7Mz62TnE7Fa/atdHPV9Zq7Gsp1M1pfwWCorLw+3ZjtRpZBBGdSdqU8BEFw9thKFPM5/UZvMiHCCM9tRO0PRbN5VUeT2yjvoBv3k7aR9oqGlYSed7jeDTospcpAyo45S21caMvpjMlPQXn2ijk34l775cQO1X9AJ9vdCHvdBtE7b2vDtIAl4+x7faApgHv4XUgZmb8/xLucRepnVnC50GJ/JDsoGI/o+wqGidZlED/ihxJMjAAOexTVVQALGLUVpLatiXXq3pTNP67Nh2FpVaVxFwluWwzrpNk8MvAZnHD0gWDH1ACwh5n9gBwrAgMBAAGjNTAzMA4GA1UdDwEB/wQEAwIFoDATBgNVHSUEDDAKBggrBgEFBQcDATAMBgNVHRMBAf8EAjAAMA0GCSqGSIb3DQEBCwUAA4IBAQAfRX+g3QtPKqmejs+uu4r8x2g4cqU/KG7tqk/cAKIyiDLhlD2dJB47kcRMWmxvqEDamYROho/JPm8SJFPCMwNo9mVE0VcudYPbRkCn6yyKEGZFuimddQeL7KDoLLinbsDmGGXyEdHU/fPRi3zL8FlnCG1OWzSmevdq2p1HsNllJ9QdCiPEIgv0W9V0u+SxD0drMusF0jI/GUYRnbPniY7ieX0HDkdds5zmw1WNCV2gv1YZg2sUJll6BEEmy4TxmSu0+DzmjDbeqvs4HGJpzTDUvgwdTpawKyKlZaFcF6w7sZ41C6RTRDy903vaeDQBI8quP6iaUWBZ4ruJ41ns4l1+"]}]}`) + // "sub": "boom@kgateway.dev", + orgFourJwt = []byte(`eyJhbGciOiJSUzI1NiIsImtpZCI6IjI5MjkxMDAyNTE1MzE5NjM0MCIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2tnYXRld2F5LmRldiIsInN1YiI6ImJvb21Aa2dhdGV3YXkuZGV2IiwiZXhwIjoyMDczMTU2OTc5LCJuYmYiOjE3NjU1NzI5NzksImlhdCI6MTc2NTU3Mjk3OX0.juMOUmoChZEE_AQVZv3jwtZjytWfzN23-palLXA-DIsSa4-f-lmf3CQiwXz0n1YlSY_dt3rGO6OsDdkYn8wkYEVoQVh11crJvZ5FhpIlZlROOSp03KTW2mQ1XwGYRxffzdzBv65LrFYWK0iNQH2NKfqOzVo5xt3SLTJuxIvCE8-qnqXUWrADw3b2TIzE7SgN7xXzeRGwTpgltq4BswdkB0R5g_1xtbrcdFgT533vt3nCiumhqrBkmk4g02x3L1iSjDCnnwJX2YLHYfpUN0i7SooguTkta067lwBiOi3NOTQjRBOBlZmkoj6sz4YNQ9EwsD74pkNBW9pN-__2cVPBxw`) ) diff --git a/hack/dummy-idp/dummy-idp.key b/hack/dummy-idp/dummy-idp.key index 1b5b90756ac..366dc46d31e 100644 --- a/hack/dummy-idp/dummy-idp.key +++ b/hack/dummy-idp/dummy-idp.key @@ -1,28 +1,52 @@ -----BEGIN PRIVATE KEY----- -MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCveFzIPXbf8b89 -On8VHtgEDabsGhm52K9Hv2/9nHIaEzrnl67undW2onVBI/IMhrkPQXL59dTyw3lw -1KQwtDbPLbHJvPUxX5pYE8HDCinMpxgueJQeXjGjA3B0zMzAGKGy1afSFkoemPgW -Gk3z9+RA31ZOim+pdLu1a/7es+AxVzassnVvN3NBuVr4Baxj6f2MIaUHrnp31fmZ -ZhEFDUajlPxx/dfVe+eowV70rdSjGi1r4/HKeEfobWUJSTNYSEHttPIHl+i7NW/A -rzKjZDb5gNMLJzr3/mbMGB0QS60iDuMaCi7rzKdalPhNBEB5VMTxXnleFXkrWmHC -FGL3Q4f7AgMBAAECggEAN/sZ+sZlQRAi15lshuP2veBPI182WuzvCuBgDdTWMEx3 -TADADh+r2Z8d8oDRzb5Dl0LluCp+XE8R1PX6RhGQtOtan8aZoT1jg/sVo7B/4mti -0xykAMZDZsMHozPdlOGm4OO6LVPwJK/f4klVGmM5XpsCMX+IHVOBOWGxiAJyIbsu -Nadg0IXREomEIJK7p+kVAxBEYkWddIiCSJ5xfeLHPls1VG1FRzT+0Xq5MYih5BkM -JYBmmm8Iofg/yJb0SMMW5JRWBzTK0gwni8s6qq+mLXWmuSNnqh7TwPwEUO3Cxg44 -QLxlXnMJn8lgthjPbxAGs+uSYWcwQbBc02p0EQe0KQKBgQDko2ifLJhXkGhFSCBZ -qHF8I6TOumzicsVlP/tm1JXTrtPoEBdyIcMb+KTnUxTxfIOiFb9GtdYTLZW6xQEU -nutAXezRK25drZtTlSEiTWvZFVAFO2Io+S5ZtOLi+N6lp7We49a5uvvjavJrjAAp -b95zyrjK/VFdifXaRIG1ZcB9aQKBgQDEeBZ8myYdENRo8zt/n15El184Tpc/tAkb -XEEjddrsIz6ekIeEclFwaRi00p4UYV2EA5tmx8u651zIcZAF/rnix1Kz4JDuAOlc -+KEnBL81SIybHMzdq8smJmMO2NQSHSrKjsCtQDzb7INEUd9VaiiEOHoaCcqL1ZfA -SUUhx+bZwwKBgQDg37+k3q2vYf7MNZZr2HpVyJDuKvmw94Uign13tBrwqoENO9Zz -kLVfq3w1cMemg/rLzmvk1i+JiUo8+kqHx45GLpsfV4IjbP7ahFCkdlVem9Gqc6+l -8P8fiAOnjXMepwbBEgI9hqT8FlH8aSQ3nSnD5V0/eUsvnuNKHBsfGMbsEQKBgDQ5 -q9iRsW72g1AmoAFLztYy2sfv9Dql0+nm+xW/BWPR9ppV1wA5FzbnaP7gIc9PFnm6 -L7wBjkFvsPVDYsKFNMp4q55PKpdpvJ7PJJ9nnqA+Wcn9vOOMACNy/s/6iV0LTc2s -ZsFnGwZm93nYvaJJ5t1G2gZD5giHzZ/6mhrhtZbbAoGALWewB6J4CkCNAS0qJK85 -I6LrayNR7IcsaUKvLgaggg1FejZRPHkGECjsDfsGpcJAhAYZSBz6ToXODPdSCDJz -A2hJwkkTrX6zaKDhkDXvVhiz0JB6win4CBnU/zKzf6PExraX+Vd+pWzdSxCBupDl -TFwmV3iuItdcF82i2fBZlXk= +MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQCjw1ztiRA5a7li +0AAmakMhaCU8PXVKWsvkHFa4Jw7/FCzPRcElaSIIk+/63qn1kLMlh/G4cmetEg9r ++gakXb1PHeRuy8444r+w9ZEZnaDNOax3lqxVqzLHd7U1WCHZA8LDqm/+P92F4c2b +LSPDfSPZqkBX0dDmoY8Lm9X0KhpFOAJXTlhk4LJZSNGQ1fCA7gEZTkPGcrMS9msc +VyeCKOHgzGersG3luSV7/2oy89xsfb6Vny5XzxwgeP2fhBK8P4OeTxDPH7byuYo0 +9vkD1C0na5Dv2IwclfSDzY2KLvSRHCNSj0NquFXI0FfN3XERQ8NdaOjITonFTcHU +xQYmumkracVP37GGqGj4itDljmd0o7aDVT09e4HoeZF/uHk5s3Qv/YRYfqp3Ms3x +3aJgRUJWpkOT84N54C6DZuSMpPzrlIC4PLIGH+3KGA8oM82oMcaU0Uu1cn8Ki5TG +kW6gVK1/SB1aZ9ewqrbKyNYoWnb/auMHNzMbdq3U9Rn6RO8ggfzawMwntmVVpDZF +/GW9LWg+/a/3WjgYGpB3/S/xKmE7IBjt7F4hext5W0aS8Dih4hz/WG0AnaIfGN6s +xovQy5ZgyGkEobST1Jgm+7u5C/W1tkj0S/sx9mWOdufAmVp3QBntOsEFYC79GiO5 +K3vkn9WH2cUW8xOfeUwVkDv0UU9yOwIDAQABAoICADowdfm1ZT1yCKqSzCraQUZC +klCIaf/kqWRBqZK/1jVO/ukbmRjo9X3kyzXyHqQyKWXhm0N161709uPgMLIA2P6e +2YeBBaLy9zgy2b8nYnbp0l2f/VZngaCc6Zet3EEXdAJAspC+MsGazYshC/8vnSh/ +tWM/G6KHr6NpWSnsN+92imZpYUFfEiBlnol/a3j0mRFb3PtL0FOhmce1NEWsry5I +k2HPTA1/r1ETHbJDMoSeXbUencH3xr/HHh+rN1kKnIMJBykB1OkEgpZ1V2BD6VOD +hjmjUY3qvrqCtJNwQdi6DdcNLuBh5aF4Xmq5D68Zy/aKC4T5WzzvdztaUug5/poq +4aBYjA2sx64MoqYbESDlX8ZsSyUmyEPu2qjm9AX4GV7XNr1t2Xl0sgmQBknEUfmP +wivRVOmfCT3Kt1C3OCWo369Map0aQlbjpbDnRIuZXz3EcA0+wnpBoeAK/fIU5X6I +8+XCIB2p/835cmH3QmwKXDsV6jupac2Uy7Fu5K8rd1UG9Ks/DDVM0zwsbNWwBshI +50k0Jp1kV/dUYHJBpswRXmiEOdjFJo2YiKlCsPZac+ky3T+1k6aM4PItbQ/3KUta +JBe8pIV0tVRouQ0++CYmttGK3Fkyti0EYztTv6SYOT4K/RAXHLUZ93x2rzCee1nX +FgJSJYLzyf62mmzsIlFxAoIBAQDnP5npvvmbzLpcgVC2Z/jdCGzjHZ+/TXfhFwDI +Vct9GcwgqmzIvMMKO+CvxNHi4EyChy3HcLOafS5T4z65PAixb5HqC1XhR0ol9bVD +g4UUxVLBM9uA35aPxtSecL7x0Uo1AvHXP2MmEwOGlatZSGjvSiMkVPT0AUyVy0VI +TMBhdMHYvmy2H++PEn1XzCoNRG4IzT5wIJos3O4SWxkKX6KbrfcZf/oF6GZyOgX5 +XOBgEyQK8swaQ1sLRsI6O3lQx1PelstaCGeDvKnqhlbziGdBO6VR4pNzrIt8bbls +2OT0SF1rtWcutH50pVgIWxzNn1IiGRuRVigpX0TECuUXxQOjAoIBAQC1SpvIrD8k +UaIu3/VvAoHOTU4l22XphoTEVcfy0lK3glcapiKro9CQLTZNfqSHbt666wxUaRrm +HmuZkogUkqwE92bO5dxqwvAOGpYS01LQX2cdGaWQI0BFJmlTkU9ojzHe5DQxVChs +9INclnvowv5WqCqo6Ue/OD7/KEtE7LYSGltvOuZI3ZXp888nm0FkZyKx5b6JxVVU +tEdksbPsU4rn4S0JFSGnRS67u2PB96gCkZrwiRKco8TkzZAMKayTeYzb7osxksFv +1rdXhv9ajt4Yve8yOpwUmWBC0hCu6JMUAfN+O5d2pnn0tzLooDBnUxuJp9riTt9v +4g8GHRQ1pYCJAoIBAQDKtbUM8uzJx0BCINWI4EGGOIGC4ZAMWTNR7CEyfArB4iBP +LjXoDZgHW4/NZIH8GFTZQvg9US0PqtY2kQiFxft01vGYsVEaBq0X83hu81Zwa7zs +QbIUGWtZI5l0Pi8TuTwMlkU7Q4R9dsNrcb3fqRrbUCjYC2UilBT3ZlWYWDd39qqK +ffq79+i+iR25He5q7OZr7sbh8aJU22ISUyrzLfxT7b6i6s1X6m4LiZN9EQ5bCcxZ +kJacGxkvHFrcQxBIYELD53ngtWdQUsy8GgEUwCiLRWbZDf3ls4Yej+ywXrzs6paB ++WW2yl/jjqFJydzI3vVXkJza3SSdh7a3BznliT1zAoIBAFWQxT5Nre+iZ/3fzqN6 +d3G7ourTRqKVzwwrwJenFUtvxVTanPqN8t5ZuIuS/my9s3pdfhBJG10JdpehRYEQ +SQh/DJMSgpoRL+Q4QCTagPfAT5a4iCAjwy3gF05OA0DCdTNSWh/+LKZgMa/9hrYx +coHMFXZrxGVIUtNzXTNG1hhtQEKbdh1mGF+3p0p2SMLuur1kRi4YaVH1VMz94oHk +XGddLDhhNOUSDiClLUxba3zq5EorKBGuajvjmd1jvgRNFmKnkCLxtULoANja6qL/ +U2wyGTeH2isOyRRAuYcUVnbXBOnLjo19vYn2/Zw2HyJlY1XxV6oAuAzCkXy9NNCt +MKECggEAHjPG/8pCcuj5WDR+6tkpKVwzAamvYy8utTTcJvT5RRwXJW4lqDcqsSLv +AOeJGlfXWBe0mQdHs9JpWCBIWOkoZepvjIbHHLWwyaZNqe/o0WoDonWFQZidMaP3 +I/DT4obwfYqkpCYksHXJgqTCMpQRIwR+aVUXF8kHiGFrAq9G4gMmiL3Cly9jwl6M +FtYlmNF7g8il/2ehmIg+4bBkveV5L5sgB7VDC3ghHqzHsPJ47CZVX+lFLva1zcz+ +88nL2O8RAfEZ86H6RZ7SHZXLk+8T7ss2s4X/HHhY50ZoDoAyaxIXQ3nJGUhFZTkR +L4GocbPxqGETwDmOZlQWwauXbbuxNw== -----END PRIVATE KEY----- diff --git a/hack/utils/jwt/jwt-generator.go b/hack/utils/jwt/jwt-generator.go index d18bde63860..2364355bbbe 100644 --- a/hack/utils/jwt/jwt-generator.go +++ b/hack/utils/jwt/jwt-generator.go @@ -33,14 +33,22 @@ func main() { os.Exit(1) } - jwt, err := generateJwt(kid, key) + jwt, err := generateJwt("ignore@kgateway.dev", kid, key) + if err != nil { + fmt.Printf("error generating jwt: %s", err.Error()) + os.Exit(1) + } + + jwt1, err := generateJwt("boom@kgateway.dev", kid, key) if err != nil { fmt.Printf("error generating jwt: %s", err.Error()) os.Exit(1) } fmt.Printf("jwks: %s\n", string(serializedJwks)) - fmt.Printf("jwt: %s\n", jwt) + fmt.Printf("jwt, sub: 'ignore@kgateway.dev': %s\n", jwt) + fmt.Printf("jwt, sub: 'boom@kgateway.dev': %s\n", jwt1) + } func generateJWKS(kid string) (*jose.JSONWebKeySet, *rsa.PrivateKey, error) { @@ -86,10 +94,10 @@ func generateJWKS(kid string) (*jose.JSONWebKeySet, *rsa.PrivateKey, error) { }, rsaKey, nil } -func generateJwt(kid string, key *rsa.PrivateKey) (string, error) { +func generateJwt(sub, kid string, key *rsa.PrivateKey) (string, error) { token := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.RegisteredClaims{ Issuer: "https://kgateway.dev", - Subject: "ignore@kgateway.dev", + Subject: sub, IssuedAt: jwt.NewNumericDate(time.Now()), NotBefore: jwt.NewNumericDate(time.Now()), ExpiresAt: jwt.NewNumericDate(time.Now().Add(85440 * time.Hour)), // 10 years diff --git a/install/helm/agentgateway-crds/templates/agentgateway.dev_agentgatewaybackends.yaml b/install/helm/agentgateway-crds/templates/agentgateway.dev_agentgatewaybackends.yaml index c28f5dcc81f..ed61ddaed49 100644 --- a/install/helm/agentgateway-crds/templates/agentgateway.dev_agentgatewaybackends.yaml +++ b/install/helm/agentgateway-crds/templates/agentgateway.dev_agentgatewaybackends.yaml @@ -1987,7 +1987,6 @@ spec: backendRef: description: |- backendRef references the remote JWKS server to reach. - Not implemented yet, only jwksUri is currently supported. Supported types: Service and Backend. properties: group: @@ -2069,18 +2068,17 @@ spec: - message: cacheDuration must be at least 5m. rule: duration(self) >= duration('5m') - uri: - description: IdP jwks endpoint. Default - tls settings are used to connect to - this url. - pattern: ^(https|http):\/\/[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?)*(:\d+)?\/.*$ + jwksPath: + description: Path to IdP jwks endpoint. + Default tls settings are used to connect + to this url. + maxLength: 2000 + minLength: 1 type: string + required: + - backendRef + - jwksPath type: object - x-kubernetes-validations: - - message: exactly one of the fields in - [uri backendRef] must be set - rule: '[has(self.uri),has(self.backendRef)].filter(x,x==true).size() - == 1' provider: description: McpIDP specifies the identity provider to use for authentication @@ -3424,7 +3422,6 @@ spec: backendRef: description: |- backendRef references the remote JWKS server to reach. - Not implemented yet, only jwksUri is currently supported. Supported types: Service and Backend. properties: group: @@ -3502,17 +3499,16 @@ spec: rule: matches(self, '^([0-9]{1,5}(h|m|s|ms)){1,4}$') - message: cacheDuration must be at least 5m. rule: duration(self) >= duration('5m') - uri: - description: IdP jwks endpoint. Default tls settings - are used to connect to this url. - pattern: ^(https|http):\/\/[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?)*(:\d+)?\/.*$ + jwksPath: + description: Path to IdP jwks endpoint. Default tls + settings are used to connect to this url. + maxLength: 2000 + minLength: 1 type: string + required: + - backendRef + - jwksPath type: object - x-kubernetes-validations: - - message: exactly one of the fields in [uri backendRef] - must be set - rule: '[has(self.uri),has(self.backendRef)].filter(x,x==true).size() - == 1' provider: description: McpIDP specifies the identity provider to use for authentication diff --git a/install/helm/agentgateway-crds/templates/agentgateway.dev_agentgatewaypolicies.yaml b/install/helm/agentgateway-crds/templates/agentgateway.dev_agentgatewaypolicies.yaml index ed25f8935d8..b37ec2b89d8 100644 --- a/install/helm/agentgateway-crds/templates/agentgateway.dev_agentgatewaypolicies.yaml +++ b/install/helm/agentgateway-crds/templates/agentgateway.dev_agentgatewaypolicies.yaml @@ -1139,7 +1139,6 @@ spec: backendRef: description: |- backendRef references the remote JWKS server to reach. - Not implemented yet, only jwksUri is currently supported. Supported types: Service and Backend. properties: group: @@ -1217,17 +1216,16 @@ spec: rule: matches(self, '^([0-9]{1,5}(h|m|s|ms)){1,4}$') - message: cacheDuration must be at least 5m. rule: duration(self) >= duration('5m') - uri: - description: IdP jwks endpoint. Default tls settings - are used to connect to this url. - pattern: ^(https|http):\/\/[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?)*(:\d+)?\/.*$ + jwksPath: + description: Path to IdP jwks endpoint. Default tls + settings are used to connect to this url. + maxLength: 2000 + minLength: 1 type: string + required: + - backendRef + - jwksPath type: object - x-kubernetes-validations: - - message: exactly one of the fields in [uri backendRef] - must be set - rule: '[has(self.uri),has(self.backendRef)].filter(x,x==true).size() - == 1' provider: description: McpIDP specifies the identity provider to use for authentication @@ -2982,7 +2980,6 @@ spec: backendRef: description: |- backendRef references the remote JWKS server to reach. - Not implemented yet, only jwksUri is currently supported. Supported types: Service and Backend. properties: group: @@ -3060,17 +3057,16 @@ spec: rule: matches(self, '^([0-9]{1,5}(h|m|s|ms)){1,4}$') - message: cacheDuration must be at least 5m. rule: duration(self) >= duration('5m') - uri: - description: IdP jwks endpoint. Default tls - settings are used to connect to this url. - pattern: ^(https|http):\/\/[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?)*(:\d+)?\/.*$ + jwksPath: + description: Path to IdP jwks endpoint. Default + tls settings are used to connect to this url. + maxLength: 2000 + minLength: 1 type: string + required: + - backendRef + - jwksPath type: object - x-kubernetes-validations: - - message: exactly one of the fields in [uri backendRef] - must be set - rule: '[has(self.uri),has(self.backendRef)].filter(x,x==true).size() - == 1' type: object x-kubernetes-validations: - message: exactly one of the fields in [remote inline] diff --git a/pkg/agentgateway/jwks/config_map_syncer.go b/pkg/agentgateway/jwks/config_map_syncer.go index fe989dcf37e..3625a2e95e0 100644 --- a/pkg/agentgateway/jwks/config_map_syncer.go +++ b/pkg/agentgateway/jwks/config_map_syncer.go @@ -29,7 +29,8 @@ func JwksStoreConfigMapLabel(storePrefix string) map[string]string { return map[string]string{jwksStoreComponentLabel: storePrefix} } -// configMapSyncer is used for writing/reading jwks' to/from ConfigMaps. +// util-like functions to help with handling of ConfigMaps used to store jwks data. +// TODO (dmitri-d) there's probably a better name for this file. type configMapSyncer struct { storePrefix string deploymentNamespace string diff --git a/pkg/agentgateway/jwks/jwks_cache.go b/pkg/agentgateway/jwks/jwks_cache.go index 252702da9d2..6a4582b5cfb 100644 --- a/pkg/agentgateway/jwks/jwks_cache.go +++ b/pkg/agentgateway/jwks/jwks_cache.go @@ -8,6 +8,9 @@ import ( "github.com/go-jose/go-jose/v4" ) +// jwksCache is an implementation of a jwks storage, used internally by JwksStore. +// Note use of the mutex when accessing jwks + type jwksCache struct { l sync.Mutex jwks map[string]string // jwks uri -> jwks diff --git a/pkg/agentgateway/jwks/jwks_fetcher.go b/pkg/agentgateway/jwks/jwks_fetcher.go index 631894787e9..4ba82638bb3 100644 --- a/pkg/agentgateway/jwks/jwks_fetcher.go +++ b/pkg/agentgateway/jwks/jwks_fetcher.go @@ -8,6 +8,7 @@ import ( "fmt" "net/http" "net/url" + "reflect" "sync" "time" @@ -16,15 +17,14 @@ import ( ) // JwksFetcher is used for fetching and periodic updates of jwks. -// Fetched jwks are stored in jwksCache. All access to jwksCache is synchronized via mu mutex. -// When a jwks is updated, registered subscribers are sent the update. +// Fetched jwks are stored in jwksCache. When a jwks is updated, registered subscribers are sent an update. type JwksFetcher struct { - mu sync.Mutex - cache *jwksCache - jwksClient JwksHttpClient - keysetSources map[string]*JwksSource - schedule FetchingSchedule - subscribers []chan map[string]string + mu sync.Mutex + cache *jwksCache + defaultJwksClient JwksHttpClient + keysetSources map[string]*JwksSource + schedule FetchingSchedule + subscribers []chan map[string]string } type FetchingSchedule []fetchAt @@ -35,9 +35,10 @@ type JwksHttpClient interface { } type JwksSource struct { - JwksURL string - Ttl time.Duration - Deleted bool + JwksURL string + Ttl time.Duration + Deleted bool + TlsConfig *tls.Config } func (js JwksSource) ResourceName() string { @@ -46,7 +47,7 @@ func (js JwksSource) ResourceName() string { func (js JwksSource) Equals(other JwksSource) bool { return js.JwksURL == other.JwksURL && - js.Ttl == other.Ttl && js.Deleted == other.Deleted + js.Ttl == other.Ttl && js.Deleted == other.Deleted && reflect.DeepEqual(js.TlsConfig, other.TlsConfig) } type fetchAt struct { @@ -61,16 +62,11 @@ type jwksHttpClientImpl struct { func NewJwksFetcher(cache *jwksCache) *JwksFetcher { toret := &JwksFetcher{ - cache: cache, - jwksClient: &jwksHttpClientImpl{ - Client: &http.Client{Transport: &http.Transport{ - TLSClientConfig: &tls.Config{ - InsecureSkipVerify: true, //nolint:gosec - }, - }}}, - keysetSources: make(map[string]*JwksSource), - schedule: make([]fetchAt, 0), - subscribers: make([]chan map[string]string, 0), + cache: cache, + defaultJwksClient: &jwksHttpClientImpl{Client: &http.Client{}}, + keysetSources: make(map[string]*JwksSource), + schedule: make([]fetchAt, 0), + subscribers: make([]chan map[string]string, 0), } heap.Init(&toret.schedule) @@ -132,7 +128,7 @@ func (f *JwksFetcher) maybeFetchJwks(ctx context.Context) { logger.Debug("fetching remote jwks", "jwksUri", fetch.keysetSource.JwksURL) - jwks, err := f.jwksClient.FetchJwks(ctx, fetch.keysetSource.JwksURL) + jwks, err := f.fetchJwks(ctx, fetch.keysetSource.JwksURL, fetch.keysetSource.TlsConfig) if err != nil { logger.Error("error fetching jwks", "jwksUri", fetch.keysetSource.JwksURL, "error", err) if fetch.retryAttempt < 5 { // backoff by 5s * retry attempt number @@ -173,6 +169,7 @@ func (f *JwksFetcher) SubscribeToUpdates() chan map[string]string { return subscriber } +// handle http, https jwks source (default http(s) client), or a client with tls.Options func (f *JwksFetcher) AddOrUpdateKeyset(source JwksSource) error { if _, err := url.Parse(source.JwksURL); err != nil { return fmt.Errorf("error parsing jwks url %w", err) @@ -208,6 +205,14 @@ func (f *JwksFetcher) RemoveKeyset(source JwksSource) { } } +func (f *JwksFetcher) fetchJwks(ctx context.Context, jwksURL string, tlsConfig *tls.Config) (jose.JSONWebKeySet, error) { + if tlsConfig != nil { + c := &jwksHttpClientImpl{Client: &http.Client{Transport: &http.Transport{TLSClientConfig: tlsConfig}}} + return c.FetchJwks(ctx, jwksURL) + } + return f.defaultJwksClient.FetchJwks(ctx, jwksURL) +} + func (c *jwksHttpClientImpl) FetchJwks(ctx context.Context, jwksURL string) (jose.JSONWebKeySet, error) { log := log.FromContext(ctx) log.Info("fetching jwks", "url", jwksURL) diff --git a/pkg/agentgateway/jwks/jwks_fetcher_test.go b/pkg/agentgateway/jwks/jwks_fetcher_test.go index e10bd4f41e4..db1b437a8dd 100644 --- a/pkg/agentgateway/jwks/jwks_fetcher_test.go +++ b/pkg/agentgateway/jwks/jwks_fetcher_test.go @@ -63,7 +63,7 @@ func TestSuccessfulJwksFetch(t *testing.T) { f := NewJwksFetcher(NewJwksCache()) ctrl := gomock.NewController(t) jwksClient := mocks.NewMockJwksHttpClient(ctrl) - f.jwksClient = jwksClient + f.defaultJwksClient = jwksClient f.AddOrUpdateKeyset(JwksSource{JwksURL: "https://test/jwks", Ttl: 5 * time.Minute}) updates := f.SubscribeToUpdates() @@ -104,7 +104,7 @@ func TestSuccessfulJwksFetchButNoChanges(t *testing.T) { f := NewJwksFetcher(NewJwksCache()) ctrl := gomock.NewController(t) jwksClient := mocks.NewMockJwksHttpClient(ctrl) - f.jwksClient = jwksClient + f.defaultJwksClient = jwksClient f.AddOrUpdateKeyset(JwksSource{JwksURL: "https://test/jwks", Ttl: 5 * time.Minute}) f.cache.jwks["https://test/jwks"] = jwks @@ -144,7 +144,7 @@ func TestFetchJwksWithError(t *testing.T) { f := NewJwksFetcher(NewJwksCache()) ctrl := gomock.NewController(t) jwksClient := mocks.NewMockJwksHttpClient(ctrl) - f.jwksClient = jwksClient + f.defaultJwksClient = jwksClient f.AddOrUpdateKeyset(JwksSource{JwksURL: "https://test/jwks", Ttl: 5 * time.Minute}) updates := f.SubscribeToUpdates() diff --git a/pkg/agentgateway/jwks/jwks_store.go b/pkg/agentgateway/jwks/jwks_store.go index 29dc1235ef1..6555588a3c7 100644 --- a/pkg/agentgateway/jwks/jwks_store.go +++ b/pkg/agentgateway/jwks/jwks_store.go @@ -12,6 +12,9 @@ import ( "github.com/kgateway-dev/kgateway/v2/pkg/pluginsdk/collections" ) +// JwksStore is a top-level abstraction that relies on jwksCache and jwksFetcher to +// retrieve and keep jwks up to date. + var logger = logging.New("jwks_store") const DefaultJwksStorePrefix = "jwks-store" @@ -21,9 +24,6 @@ var JwksConfigMapNamespacedName = func(jwksUri string) *types.NamespacedName { return nil } -// JwksStore handles initial fetching and periodic updates of jwks. Jwks are persisted -// in ConfigMaps, a jwks per ConfigMap. The ConfigMaps are used to re-create internal -// JwksStore state on startup and by traffic-plugins as source of remote jwks. type JwksStore struct { storePrefix string jwksCache *jwksCache @@ -103,6 +103,7 @@ func (s *JwksStore) updateJwksSources(ctx context.Context) { if jwksUpdate.Deleted { logger.Debug("deleting keyset", "jwksUri", jwksUpdate.JwksURL, "ConfigMap", JwksConfigMapName(s.storePrefix, jwksUpdate.JwksURL)) s.jwksFetcher.RemoveKeyset(jwksUpdate) + s.l.Lock() delete(s.cmNameToJwks, JwksConfigMapName(s.storePrefix, jwksUpdate.JwksURL)) s.l.Unlock() @@ -111,7 +112,9 @@ func (s *JwksStore) updateJwksSources(ctx context.Context) { err := s.jwksFetcher.AddOrUpdateKeyset(jwksUpdate) if err != nil { logger.Error("error adding/updating a jwks keyset", "error", err, "uri", jwksUpdate.JwksURL) + continue } + s.l.Lock() s.cmNameToJwks[JwksConfigMapName(s.storePrefix, jwksUpdate.JwksURL)] = jwksUpdate.JwksURL s.l.Unlock() diff --git a/pkg/agentgateway/jwks_url/remote_jwks_url_builder.go b/pkg/agentgateway/jwks_url/remote_jwks_url_builder.go new file mode 100644 index 00000000000..258411eb660 --- /dev/null +++ b/pkg/agentgateway/jwks_url/remote_jwks_url_builder.go @@ -0,0 +1,215 @@ +package jwks_url + +import ( + "crypto/tls" + "crypto/x509" + "fmt" + + "istio.io/istio/pkg/kube/krt" + "istio.io/istio/pkg/ptr" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" + v1 "sigs.k8s.io/gateway-api/apis/v1" + + "github.com/kgateway-dev/kgateway/v2/api/v1alpha1/agentgateway" + "github.com/kgateway-dev/kgateway/v2/pkg/kgateway/wellknown" + krtpkg "github.com/kgateway-dev/kgateway/v2/pkg/utils/krtutil" + "github.com/kgateway-dev/kgateway/v2/pkg/utils/kubeutils" +) + +type JwksUrlBuilder interface { + BuildJwksUrlAndTlsConfig(krtctx krt.HandlerContext, policyName, defaultNS string, remoteProvider *agentgateway.RemoteJWKS) (string, *tls.Config, error) +} + +var JwksUrlBuilderFactory func() JwksUrlBuilder = func() JwksUrlBuilder { return &emptyJwksUrlFactory{} } + +type emptyJwksUrlFactory struct{} + +func (f *emptyJwksUrlFactory) BuildJwksUrlAndTlsConfig(_ krt.HandlerContext, _, _ string, _ *agentgateway.RemoteJWKS) (string, *tls.Config, error) { + return "", nil, fmt.Errorf("JwksUrlBuilderFactory must be initialized before use") +} + +type TargetRefIndexKey struct { + Group string + Kind string + Name string + Namespace string +} + +func (k TargetRefIndexKey) String() string { + return fmt.Sprintf("%s:%s:%s:%s", k.Group, k.Kind, k.Namespace, k.Name) +} + +type defaultJwksUrlFactory struct { + cfgmaps krt.Collection[*corev1.ConfigMap] + policiesByTargetRefIndex krt.Index[TargetRefIndexKey, *agentgateway.AgentgatewayPolicy] + backends krt.Collection[*agentgateway.AgentgatewayBackend] + agentgatewayPolicies krt.Collection[*agentgateway.AgentgatewayPolicy] +} + +func NewJwksUrlFactory(cfgmaps krt.Collection[*corev1.ConfigMap], + backends krt.Collection[*agentgateway.AgentgatewayBackend], + agentgatewayPolicies krt.Collection[*agentgateway.AgentgatewayPolicy]) JwksUrlBuilder { + + policiesByTargetRefIndex := krtpkg.UnnamedIndex(agentgatewayPolicies, func(in *agentgateway.AgentgatewayPolicy) []TargetRefIndexKey { + keys := make([]TargetRefIndexKey, 0) + for _, ref := range in.Spec.TargetRefs { + keys = append(keys, TargetRefIndexKey{ + Name: string(ref.Name), + Kind: string(ref.Kind), + Group: string(ref.Group), + Namespace: in.Namespace, + }) + } + return keys + }) + + return &defaultJwksUrlFactory{ + cfgmaps: cfgmaps, + policiesByTargetRefIndex: policiesByTargetRefIndex, + backends: backends, + agentgatewayPolicies: agentgatewayPolicies, + } +} + +func (f *defaultJwksUrlFactory) BuildJwksUrlAndTlsConfig(krtctx krt.HandlerContext, policyName, defaultNS string, remoteProvider *agentgateway.RemoteJWKS) (string, *tls.Config, error) { + ref := remoteProvider.BackendRef + + refName := string(ref.Name) + refNamespace := string(ptr.OrDefault(ref.Namespace, v1.Namespace(defaultNS))) + + switch string(*ref.Kind) { + case wellknown.AgentgatewayBackendGVK.Kind: + backendRef := types.NamespacedName{ + Name: refName, + Namespace: refNamespace, + } + backend := ptr.Flatten(krt.FetchOne(krtctx, f.backends, krt.FilterObjectName(backendRef))) + if backend == nil { + return "", nil, fmt.Errorf("backend %s not found, policy %s", backendRef, types.NamespacedName{Namespace: defaultNS, Name: policyName}) + } + if backend.Spec.Static == nil { + return "", nil, fmt.Errorf("only static backends are supported; backend: %s, policy: %s", backendRef, types.NamespacedName{Namespace: defaultNS, Name: policyName}) + } + + // TODO (dmitri-d) only inline tls config is supported atm, do we want to support attching AgentgatewayPolicy too? + var tlsConfig *tls.Config + if backend.Spec.Policies != nil && backend.Spec.Policies.TLS != nil { + tlsc, err := GetTLSConfig(krtctx, f.cfgmaps, refNamespace, backend.Spec.Policies.TLS) + if err != nil { + return "", nil, fmt.Errorf("error setting tls options; backend: %s, policy: %s, %w", + backendRef, types.NamespacedName{Namespace: refNamespace, Name: policyName}, err) + } + tlsConfig = tlsc + } else { // check if a + agwPolicy := ptr.Flatten(krt.FetchOne(krtctx, f.agentgatewayPolicies, krt.FilterIndex(f.policiesByTargetRefIndex, TargetRefIndexKey{ + Name: refName, + Kind: string(*ref.Kind), + Group: string(ptr.OrEmpty(ref.Group)), + Namespace: refNamespace, + // no port, as policy targetRef may not have it + }))) + + if agwPolicy != nil && agwPolicy.Spec.Backend != nil && agwPolicy.Spec.Backend.TLS != nil { + tlsc, err := GetTLSConfig(krtctx, f.cfgmaps, refNamespace, agwPolicy.Spec.Backend.TLS) + if err != nil { + return "", nil, fmt.Errorf("error setting tls options; service %s/%s, policy: %s %w", + refName, refNamespace, types.NamespacedName{Namespace: refNamespace, Name: policyName}, err) + } + tlsConfig = tlsc + } + } + + var url string + if tlsConfig == nil { + url = fmt.Sprintf("http://%s:%d/%s", backend.Spec.Static.Host, backend.Spec.Static.Port, remoteProvider.JwksPath) + } else { + url = fmt.Sprintf("https://%s:%d/%s", backend.Spec.Static.Host, backend.Spec.Static.Port, remoteProvider.JwksPath) + } + + return url, tlsConfig, nil + case wellknown.ServiceKind: + agwPolicy := ptr.Flatten(krt.FetchOne(krtctx, f.agentgatewayPolicies, krt.FilterIndex(f.policiesByTargetRefIndex, TargetRefIndexKey{ + Name: refName, + Kind: string(*ref.Kind), + Group: string(ptr.OrEmpty(ref.Group)), + Namespace: refNamespace, + // no port, as policy targetRef may not have it + }))) + + var tlsConfig *tls.Config + if agwPolicy != nil && agwPolicy.Spec.Backend != nil && agwPolicy.Spec.Backend.TLS != nil { + tlsc, err := GetTLSConfig(krtctx, f.cfgmaps, refNamespace, agwPolicy.Spec.Backend.TLS) + if err != nil { + return "", nil, fmt.Errorf("error setting tls options; service %s/%s, policy: %s %w", + refName, refNamespace, types.NamespacedName{Namespace: refNamespace, Name: policyName}, err) + } + tlsConfig = tlsc + } + + host := kubeutils.GetServiceHostname(refName, refNamespace) + var fqdn string + if port := ptr.OrEmpty(ref.Port); port != 0 { + fqdn = fmt.Sprintf("%s:%d", host, port) + } else { + fqdn = host + } + + var url string + if tlsConfig == nil { + url = fmt.Sprintf("http://%s/%s", fqdn, remoteProvider.JwksPath) + } else { + url = fmt.Sprintf("https://%s/%s", fqdn, remoteProvider.JwksPath) + } + + return url, tlsConfig, nil + } + + return "", nil, fmt.Errorf("unsupported target kind in remote jwks provider; kind: %s, policy: %s", string(*ref.Kind), types.NamespacedName{Namespace: refNamespace, Name: policyName}) +} + +func GetTLSConfig( + krtctx krt.HandlerContext, + cfgmaps krt.Collection[*corev1.ConfigMap], + namespace string, + btls *agentgateway.BackendTLS, +) (*tls.Config, error) { + toret := tls.Config{ + ServerName: ptr.OrEmpty(btls.Sni), + InsecureSkipVerify: insecureSkipVerify(btls.InsecureSkipVerify), //nolint:gosec + NextProtos: ptr.OrEmpty(btls.AlpnProtocols), + } + + if len(btls.CACertificateRefs) > 0 { + certPool := x509.NewCertPool() + for _, ref := range btls.CACertificateRefs { + nn := types.NamespacedName{ + Name: string(ref.Name), + Namespace: namespace, + } + cfgmap := krt.FetchOne(krtctx, cfgmaps, krt.FilterObjectName(nn)) + if cfgmap == nil { + return nil, fmt.Errorf("ConfigMap %s not found", nn) + } + success := appendPoolWithCertsFromConfigMap(certPool, ptr.Flatten(cfgmap)) + if !success { + return nil, fmt.Errorf("error extracting CA cert from ConfigMap %s", nn) + } + } + toret.RootCAs = certPool + } + + return &toret, nil +} + +func appendPoolWithCertsFromConfigMap(pool *x509.CertPool, cm *corev1.ConfigMap) bool { + caCrts, ok := cm.Data["ca.crt"] + if !ok { + return false + } + return pool.AppendCertsFromPEM([]byte(caCrts)) +} + +func insecureSkipVerify(mode *agentgateway.InsecureTLSMode) bool { + return mode != nil +} diff --git a/pkg/agentgateway/jwksstore/cm_controller.go b/pkg/agentgateway/jwksstore/cm_controller.go index 554b0ac2816..fb692b1e335 100644 --- a/pkg/agentgateway/jwksstore/cm_controller.go +++ b/pkg/agentgateway/jwksstore/cm_controller.go @@ -20,6 +20,9 @@ import ( "github.com/kgateway-dev/kgateway/v2/pkg/logging" ) +// JwksStoreConfigMapsController is responsible for reliable synchronisation of internal jwks store state (jwks uri + jwks) to ConfigMaps. +// A single ConfigMap is used to store all jwks for a given jwks url. + var cmLogger = logging.New("jwks_store_config_map_controller") type JwksStoreConfigMapsController struct { @@ -85,6 +88,8 @@ func (jcm *JwksStoreConfigMapsController) Start(ctx context.Context) error { for { select { case u := <-jcm.jwksUpdates: + // TODO (dmitri-d) could add an interface for adding events directly to the queue + // jwks store and cm controller are tightly coupled anyway, this indirection isn't very useful for uri := range u { jcm.eventQueue.AddObject(jcm.newJwksStoreConfigMap(jwks.JwksConfigMapName(jcm.storePrefix, uri))) } diff --git a/pkg/agentgateway/jwksstore/policy_controller.go b/pkg/agentgateway/jwksstore/policy_controller.go index 578257999c0..7cd41f00af7 100644 --- a/pkg/agentgateway/jwksstore/policy_controller.go +++ b/pkg/agentgateway/jwksstore/policy_controller.go @@ -2,7 +2,6 @@ package agentjwksstore import ( "context" - "time" "istio.io/istio/pkg/kube/controllers" "istio.io/istio/pkg/kube/kclient" @@ -11,28 +10,33 @@ import ( "github.com/kgateway-dev/kgateway/v2/api/v1alpha1/agentgateway" "github.com/kgateway-dev/kgateway/v2/pkg/agentgateway/jwks" + "github.com/kgateway-dev/kgateway/v2/pkg/agentgateway/jwks_url" "github.com/kgateway-dev/kgateway/v2/pkg/agentgateway/plugins" "github.com/kgateway-dev/kgateway/v2/pkg/apiclient" "github.com/kgateway-dev/kgateway/v2/pkg/kgateway/wellknown" "github.com/kgateway-dev/kgateway/v2/pkg/logging" ) +// JwksStorePolicyController watches AgentgatewayPolicies and Backends. When a resource containing +// new or updated remote jwks source is detected, an jwks store is notifed of an update. type JwksStorePolicyController struct { - agw *plugins.AgwCollections - apiClient apiclient.Client - jwks krt.Collection[jwks.JwksSource] - jwksChanges chan jwks.JwksSource - waitForSync []cache.InformerSynced + agw *plugins.AgwCollections + apiClient apiclient.Client + jwks krt.Collection[jwks.JwksSource] + jwksChanges chan jwks.JwksSource + jwksUrlFactory func() jwks_url.JwksUrlBuilder + waitForSync []cache.InformerSynced } var polLogger = logging.New("jwks_store_policy_controller") -func NewJWKSStorePolicyController(apiClient apiclient.Client, agw *plugins.AgwCollections) *JwksStorePolicyController { +func NewJWKSStorePolicyController(apiClient apiclient.Client, agw *plugins.AgwCollections, jwksUrlFactory func() jwks_url.JwksUrlBuilder) *JwksStorePolicyController { polLogger.Info("creating jwks store policy controller") return &JwksStorePolicyController{ - agw: agw, - apiClient: apiClient, - jwksChanges: make(chan jwks.JwksSource), + agw: agw, + apiClient: apiClient, + jwksChanges: make(chan jwks.JwksSource), + jwksUrlFactory: jwksUrlFactory, } } @@ -42,12 +46,10 @@ func (j *JwksStorePolicyController) Init(ctx context.Context) { wellknown.AgentgatewayBackendGVR, kclient.Filter{ObjectFilter: j.agw.Client.ObjectFilter()}, ), j.agw.KrtOpts.ToOptions("AgentgatewayBackend")...) - policyCol := krt.WrapClient(kclient.NewFilteredDelayed[*agentgateway.AgentgatewayPolicy]( - j.apiClient, - wellknown.AgentgatewayPolicyGVR, - kclient.Filter{ObjectFilter: j.agw.Client.ObjectFilter()}, - ), j.agw.KrtOpts.ToOptions("AgentgatewayPolicy")...) - j.jwks = krt.NewManyCollection(policyCol, func(kctx krt.HandlerContext, p *agentgateway.AgentgatewayPolicy) []jwks.JwksSource { + + // TODO JwksSource should be per-policy, i.e. the same jwks url for multiple policies should result in multiple JwksSources + // Otherwise changes to one policy (removal for example) could result in disruption of traffic for other policies (while ConfigMaps are re-synced) + j.jwks = krt.NewManyCollection(j.agw.AgentgatewayPolicies, func(kctx krt.HandlerContext, p *agentgateway.AgentgatewayPolicy) []jwks.JwksSource { toret := make([]jwks.JwksSource, 0) // enqueue Traffic JWT providers (if present) @@ -56,24 +58,17 @@ func (j *JwksStorePolicyController) Init(ctx context.Context) { if provider.JWKS.Remote == nil { continue } - toret = append(toret, jwks.JwksSource{ - JwksURL: provider.JWKS.Remote.JwksUri, - Ttl: provider.JWKS.Remote.CacheDuration.Duration, - }) + + if s := j.buildJwksSource(kctx, p.Name, p.Namespace, provider.JWKS.Remote); s != nil { + toret = append(toret, *s) + } } } // enqueue Backend MCP authentication JWKS (if present) if p.Spec.Backend != nil && p.Spec.Backend.MCP != nil && p.Spec.Backend.MCP.Authentication != nil { - ttl := 5 * time.Minute - if p.Spec.Backend.MCP.Authentication.JWKS.CacheDuration != nil { - ttl = p.Spec.Backend.MCP.Authentication.JWKS.CacheDuration.Duration - } - if p.Spec.Backend.MCP.Authentication.JWKS.JwksUri != "" { - toret = append(toret, jwks.JwksSource{ - JwksURL: p.Spec.Backend.MCP.Authentication.JWKS.JwksUri, - Ttl: ttl, - }) + if s := j.buildJwksSource(kctx, p.Name, p.Namespace, &p.Spec.Backend.MCP.Authentication.JWKS); s != nil { + toret = append(toret, *s) } } @@ -84,15 +79,8 @@ func (j *JwksStorePolicyController) Init(ctx context.Context) { continue } if b.Spec.Policies != nil && b.Spec.Policies.MCP != nil && b.Spec.Policies.MCP.Authentication != nil { - ttl := 5 * time.Minute - if b.Spec.Policies.MCP.Authentication.JWKS.CacheDuration != nil { - ttl = b.Spec.Policies.MCP.Authentication.JWKS.CacheDuration.Duration - } - if b.Spec.Policies.MCP.Authentication.JWKS.JwksUri != "" { - toret = append(toret, jwks.JwksSource{ - JwksURL: b.Spec.Policies.MCP.Authentication.JWKS.JwksUri, - Ttl: ttl, - }) + if s := j.buildJwksSource(kctx, p.Name, p.Namespace, &p.Spec.Backend.MCP.Authentication.JWKS); s != nil { + toret = append(toret, *s) } } } @@ -101,7 +89,6 @@ func (j *JwksStorePolicyController) Init(ctx context.Context) { }, j.agw.KrtOpts.ToOptions("JwksSources")...) j.waitForSync = []cache.InformerSynced{ - policyCol.HasSynced, backendCol.HasSynced, } } @@ -138,3 +125,17 @@ func (j *JwksStorePolicyController) NeedLeaderElection() bool { func (j *JwksStorePolicyController) JwksChanges() chan jwks.JwksSource { return j.jwksChanges } + +func (j *JwksStorePolicyController) buildJwksSource(krtctx krt.HandlerContext, policyName, defaultNS string, remoteProvider *agentgateway.RemoteJWKS) *jwks.JwksSource { + jwksUrl, tlsConfig, err := j.jwksUrlFactory().BuildJwksUrlAndTlsConfig(krtctx, policyName, defaultNS, remoteProvider) + if err != nil { + polLogger.Error("error generating remote jwks url or tls options", "error", err) + return nil + } + + return &jwks.JwksSource{ + JwksURL: jwksUrl, + TlsConfig: tlsConfig, + Ttl: remoteProvider.CacheDuration.Duration, + } +} diff --git a/pkg/agentgateway/plugins/backend_policies.go b/pkg/agentgateway/plugins/backend_policies.go index f7cbdd0aed0..abfb05923fa 100644 --- a/pkg/agentgateway/plugins/backend_policies.go +++ b/pkg/agentgateway/plugins/backend_policies.go @@ -16,6 +16,7 @@ import ( "github.com/kgateway-dev/kgateway/v2/api/v1alpha1/agentgateway" "github.com/kgateway-dev/kgateway/v2/api/v1alpha1/shared" + "github.com/kgateway-dev/kgateway/v2/pkg/agentgateway/jwks_url" "github.com/kgateway-dev/kgateway/v2/pkg/kgateway/translator/sslutils" "github.com/kgateway-dev/kgateway/v2/pkg/kgateway/wellknown" "github.com/kgateway-dev/kgateway/v2/pkg/utils/kubeutils" @@ -302,9 +303,14 @@ func translateBackendMCPAuthentication(ctx PolicyCtx, policy *agentgateway.Agent idp = api.BackendPolicySpec_McpAuthentication_KEYCLOAK } - translatedInlineJwks, err := resolveRemoteJWKSInline(ctx, authnPolicy.JWKS.JwksUri) + jwksUrl, _, err := jwks_url.JwksUrlBuilderFactory().BuildJwksUrlAndTlsConfig(ctx.Krt, policy.Name, policy.Namespace, &authnPolicy.JWKS) if err != nil { - logger.Error("failed resolving jwks", "jwks_uri", authnPolicy.JWKS.JwksUri, "error", err) + logger.Error("failed resolving jwks url", "error", err) + return nil, err + } + translatedInlineJwks, err := resolveRemoteJWKSInline(ctx, jwksUrl) + if err != nil { + logger.Error("failed resolving jwks", "jwks_uri", jwksUrl, "error", err) return nil, err } diff --git a/pkg/agentgateway/plugins/traffic_plugin.go b/pkg/agentgateway/plugins/traffic_plugin.go index 2c7e513615d..3015d66efa8 100644 --- a/pkg/agentgateway/plugins/traffic_plugin.go +++ b/pkg/agentgateway/plugins/traffic_plugin.go @@ -32,6 +32,7 @@ import ( "github.com/kgateway-dev/kgateway/v2/api/v1alpha1/agentgateway" "github.com/kgateway-dev/kgateway/v2/api/v1alpha1/shared" + "github.com/kgateway-dev/kgateway/v2/pkg/agentgateway/jwks_url" "github.com/kgateway-dev/kgateway/v2/pkg/agentgateway/utils" "github.com/kgateway-dev/kgateway/v2/pkg/kgateway/wellknown" "github.com/kgateway-dev/kgateway/v2/pkg/logging" @@ -532,7 +533,12 @@ func processJWTAuthenticationPolicy(ctx PolicyCtx, jwt *agentgateway.JWTAuthenti continue } if r := pp.JWKS.Remote; r != nil { - inline, err := resolveRemoteJWKSInline(ctx, pp.JWKS.Remote.JwksUri) + jwksUrl, _, err := jwks_url.JwksUrlBuilderFactory().BuildJwksUrlAndTlsConfig(ctx.Krt, policy.Name, policy.Namespace, pp.JWKS.Remote) + if err != nil { + errs = append(errs, err) + continue + } + inline, err := resolveRemoteJWKSInline(ctx, jwksUrl) if err != nil { errs = append(errs, err) continue diff --git a/pkg/kgateway/agentgatewaysyncer/backend/testdata/backend/mcp-authentication-jwks-store-init.yaml b/pkg/kgateway/agentgatewaysyncer/backend/testdata/backend/mcp-authentication-jwks-store-init.yaml index ad38f196939..0dd5755a90d 100644 --- a/pkg/kgateway/agentgatewaysyncer/backend/testdata/backend/mcp-authentication-jwks-store-init.yaml +++ b/pkg/kgateway/agentgatewaysyncer/backend/testdata/backend/mcp-authentication-jwks-store-init.yaml @@ -15,7 +15,12 @@ spec: mcp: authentication: jwks: - uri: http://store-uninitialized/ + jwksPath: / + backendRef: + group: "" + kind: Service + name: store-uninitialized + port: 8080 --- # Output output: [] diff --git a/pkg/kgateway/agentgatewaysyncer/backend/translate_test.go b/pkg/kgateway/agentgatewaysyncer/backend/translate_test.go index 839ecf8ff7c..07d05f99f3e 100644 --- a/pkg/kgateway/agentgatewaysyncer/backend/translate_test.go +++ b/pkg/kgateway/agentgatewaysyncer/backend/translate_test.go @@ -1,11 +1,13 @@ package agentgatewaybackend_test import ( + "crypto/tls" "fmt" "testing" "github.com/agentgateway/agentgateway/go/api" "google.golang.org/protobuf/proto" + "istio.io/istio/pkg/kube/krt" "istio.io/istio/pkg/slices" "istio.io/istio/pkg/test/util/assert" "istio.io/istio/pkg/util/protomarshal" @@ -16,15 +18,23 @@ import ( "github.com/kgateway-dev/kgateway/v2/api/v1alpha1/agentgateway" agwir "github.com/kgateway-dev/kgateway/v2/pkg/agentgateway/ir" + "github.com/kgateway-dev/kgateway/v2/pkg/agentgateway/jwks_url" "github.com/kgateway-dev/kgateway/v2/pkg/agentgateway/plugins" "github.com/kgateway-dev/kgateway/v2/pkg/agentgateway/testutils" agentgatewaybackend "github.com/kgateway-dev/kgateway/v2/pkg/kgateway/agentgatewaysyncer/backend" "github.com/kgateway-dev/kgateway/v2/pkg/utils/kubeutils" ) +type jwksUrlFactoryForTesting struct{} + +func (f *jwksUrlFactoryForTesting) BuildJwksUrlAndTlsConfig(_ krt.HandlerContext, _, _ string, _ *agentgateway.RemoteJWKS) (string, *tls.Config, error) { + return "http://store-uninitialized/", nil, nil +} + func TestTranslateAgwBackend(t *testing.T) { testutils.RunForDirectory(t, "testdata/backend", func(t *testing.T, ctx plugins.PolicyCtx) (*agentgateway.AgentgatewayBackendStatus, []*api.Resource) { backend := testutils.GetTestResource(t, ctx.Collections.Backends) + jwks_url.JwksUrlBuilderFactory = func() jwks_url.JwksUrlBuilder { return &jwksUrlFactoryForTesting{} } status, results := agentgatewaybackend.TranslateAgwBackend(ctx, backend) return status, slices.Map(results, func(r agwir.AgwResource) *api.Resource { return r.Resource diff --git a/pkg/kgateway/agentgatewaysyncer/testdata/inputs/backend/mcp-auth-remote.yaml b/pkg/kgateway/agentgatewaysyncer/testdata/inputs/backend/mcp-auth-remote.yaml index f86579792b5..f869e10b9da 100644 --- a/pkg/kgateway/agentgatewaysyncer/testdata/inputs/backend/mcp-auth-remote.yaml +++ b/pkg/kgateway/agentgatewaysyncer/testdata/inputs/backend/mcp-auth-remote.yaml @@ -101,7 +101,12 @@ spec: authentication: issuer: http://keycloak:7080/realms/mcp jwks: - uri: http://keycloak.default.svc.cluster.local:7080/realms/mcp/protocol/openid-connect/certs + jwksPath: realms/mcp/protocol/openid-connect/certs + backendRef: + group: "" + kind: Service + name: keycloak + port: 7080 audiences: - "account" provider: Keycloak diff --git a/pkg/kgateway/setup/setup.go b/pkg/kgateway/setup/setup.go index 62d64adfee9..6fced5e87ca 100644 --- a/pkg/kgateway/setup/setup.go +++ b/pkg/kgateway/setup/setup.go @@ -25,6 +25,7 @@ import ( apisettings "github.com/kgateway-dev/kgateway/v2/api/settings" "github.com/kgateway-dev/kgateway/v2/pkg/agentgateway/jwks" + "github.com/kgateway-dev/kgateway/v2/pkg/agentgateway/jwks_url" agentjwksstore "github.com/kgateway-dev/kgateway/v2/pkg/agentgateway/jwksstore" agwplugins "github.com/kgateway-dev/kgateway/v2/pkg/agentgateway/plugins" "github.com/kgateway-dev/kgateway/v2/pkg/apiclient" @@ -436,6 +437,9 @@ func (s *setup) Start(ctx context.Context) error { slog.Error("error creating agw common collections", "error", err) return err } + + jwksUrlFactory := jwks_url.NewJwksUrlFactory(agwCollections.ConfigMaps, agwCollections.Backends, agwCollections.AgentgatewayPolicies) + jwks_url.JwksUrlBuilderFactory = func() jwks_url.JwksUrlBuilder { return jwksUrlFactory } } for _, mgrCfgFunc := range s.extraManagerConfig { @@ -581,7 +585,7 @@ func SetupLogging(levelStr string) { } func buildJwksStore(ctx context.Context, mgr manager.Manager, apiClient apiclient.Client, commonCollections *collections.CommonCollections, agwCollections *agwplugins.AgwCollections) error { - jwksStorePolicyCtrl := agentjwksstore.NewJWKSStorePolicyController(apiClient, agwCollections) + jwksStorePolicyCtrl := agentjwksstore.NewJWKSStorePolicyController(apiClient, agwCollections, jwks_url.JwksUrlBuilderFactory) if err := mgr.Add(jwksStorePolicyCtrl); err != nil { return err } diff --git a/test/e2e/features/agentgateway/mcp/testdata/remote-authn-auth0.yaml b/test/e2e/features/agentgateway/mcp/testdata/remote-authn-auth0.yaml index 9e799cdcf8c..35b7c9eadfd 100644 --- a/test/e2e/features/agentgateway/mcp/testdata/remote-authn-auth0.yaml +++ b/test/e2e/features/agentgateway/mcp/testdata/remote-authn-auth0.yaml @@ -123,7 +123,12 @@ spec: authentication: issuer: https://kgateway.dev jwks: - uri: http://auth0-mock.default.svc.cluster.local:9000/.well-known/jwks.json + jwksPath: .well-known/jwks.json + backendRef: + group: "" + kind: Service + name: auth0-mock + port: 9000 provider: Auth0 audiences: - "account" diff --git a/test/e2e/features/agentgateway/remotejwtauth/suite.go b/test/e2e/features/agentgateway/remotejwtauth/suite.go index c4f821ece17..d07338c9c7a 100644 --- a/test/e2e/features/agentgateway/remotejwtauth/suite.go +++ b/test/e2e/features/agentgateway/remotejwtauth/suite.go @@ -30,9 +30,11 @@ var _ e2e.NewSuiteFunc = NewTestingSuite const ( namespace = "default" // jwt subject is "ignore@kgateway.dev" - // could also retrieve these jwts from https://dummy-idp.default:8443/org-one/jwt, https://dummy-idp.default:8443/org-two/jwt, and https://dummy-idp.default:8443/org-three/jwt respectively + // could also retrieve these jwts from https://dummy-idp.default:8443/org-one/jwt, https://dummy-idp.default:8443/org-two/jwt JwtOrgOne = "eyJhbGciOiJSUzI1NiIsImtpZCI6IjUzNTAyMzEyMTkzMDYwMzg2OTIiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2tnYXRld2F5LmRldiIsInN1YiI6Imlnbm9yZUBrZ2F0ZXdheS5kZXYiLCJleHAiOjIwNzExNjM0MDcsIm5iZiI6MTc2MzU3OTQwNywiaWF0IjoxNzYzNTc5NDA3fQ.TsHCCdd0_629wibU4EviEi1-_UXaFUX1NuLgXCrC-tr7kqlcnUJIJC0WSab1EgXKtF8gTfwTUeQcAQNrunwngQU-K9DFcH5-2vnGeiXV3_X3SokkPq74ceRrCFEL2d7YNaGfhq_UNyvKRJsRz-pwdKK7QIPXALmWaUHn7EV7zU-CcPCKNwmt62P88qNp5HYSbgqz_WfnzIIH8LANpCC8fUqVedgTJMJ86E06pfDNUuuXe_fhjgMQXlfyDeUxIuzJunvS2qIqt4IYMzjcQbl2QI1QK3xz37tridSP_WVuuMUe2Lqo0oDjWVpxqPb5fb90W6a6khRP59Pf6qKMbQ9SQg" jwtOrgTwo = "eyJhbGciOiJSUzI1NiIsImtpZCI6IjI4OTk1NjQyMzcyMTQ2ODQ5NDciLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2tnYXRld2F5LmRldiIsInN1YiI6Imlnbm9yZUBrZ2F0ZXdheS5kZXYiLCJleHAiOjIwNzExNjM1MzIsIm5iZiI6MTc2MzU3OTUzMiwiaWF0IjoxNzYzNTc5NTMyfQ.kLazcb2o_zcVfJ7WECsQJdOaluxAJ-GdOkeuXUOJSeN8PvahjxfpftgeJjcGsp2sl-VIKXIuTLH6csHT_CBq7kI8bVKGDkk8qw3w8gem7MtiXKPMSYiYEHAoCCzsl8O-pGPF6G_PU-CfiWla8CIAjOewLzRmLeAYmwEiUYf8LQ7y6BbVDzvtxIQW3pTurHXFy0TZ6nUGqu_Xwh7uXe42WC0T-9LAI4zsGo5x_FKhlE_6N9_a7R0UIYFeRrbph_b1z47xTZ3YhZBmQmue2j1xR6hwRCnL7mOaCrxdte8SqXNUVA6vPSaiMTSkdmKyeRSzeTliDKiqAmP8eiIaqAoN5A" + // sub "boom@kgateway.dev" + jwtOrgFour = "eyJhbGciOiJSUzI1NiIsImtpZCI6IjI5MjkxMDAyNTE1MzE5NjM0MCIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2tnYXRld2F5LmRldiIsInN1YiI6ImJvb21Aa2dhdGV3YXkuZGV2IiwiZXhwIjoyMDczMTU2OTc5LCJuYmYiOjE3NjU1NzI5NzksImlhdCI6MTc2NTU3Mjk3OX0.juMOUmoChZEE_AQVZv3jwtZjytWfzN23-palLXA-DIsSa4-f-lmf3CQiwXz0n1YlSY_dt3rGO6OsDdkYn8wkYEVoQVh11crJvZ5FhpIlZlROOSp03KTW2mQ1XwGYRxffzdzBv65LrFYWK0iNQH2NKfqOzVo5xt3SLTJuxIvCE8-qnqXUWrADw3b2TIzE7SgN7xXzeRGwTpgltq4BswdkB0R5g_1xtbrcdFgT533vt3nCiumhqrBkmk4g02x3L1iSjDCnnwJX2YLHYfpUN0i7SooguTkta067lwBiOi3NOTQjRBOBlZmkoj6sz4YNQ9EwsD74pkNBW9pN-__2cVPBxw" ) var ( @@ -47,14 +49,32 @@ var ( } testCases = map[string]*base.TestCase{ - "TestRoutePolicy": { - Manifests: []string{insecureRouteManifest, secureRoutePolicyManifest}, + "TestRoutePolicySvc": { + Manifests: []string{secureRoutePolicyManifestSvc}, + }, + "TestRoutePolicySvcCaCert": { + Manifests: []string{secureRoutePolicyManifestSvcCaCert}, + }, + "TestRoutePolicyBackend": { + Manifests: []string{insecureRouteManifest, secureRoutePolicyManifestBackend}, + }, + "TestRoutePolicyBackendAndTlsPolicy": { + Manifests: []string{secureRoutePolicyManifestBackendAndTlsPolicy}, }, "TestRoutePolicyWithRbac": { Manifests: []string{secureRoutePolicyWithRbacManifest}, }, - "TestGatewayPolicy": { - Manifests: []string{secureGWPolicyManifest}, + "TestGatewayPolicySvc": { + Manifests: []string{secureGWPolicyManifestSvc}, + }, + "TestGatewayPolicySvcCaCert": { + Manifests: []string{secureGWPolicyManifestSvcCaCert}, + }, + "TestGatewayPolicyBackend": { + Manifests: []string{secureGWPolicyManifestBackend}, + }, + "TestGatewayPolicyBackendWithTlsPolicy": { + Manifests: []string{secureGWPolicyManifestBackendAndTlsPolicy}, }, "TestGatewayPolicyWithRbac": { Manifests: []string{secureGWPolicyWithRbacManifest}, @@ -78,14 +98,20 @@ func NewTestingSuite(ctx context.Context, testInst *e2e.TestInstallation) suite. } var ( - insecureRouteManifest = getTestFile("insecure-route.yaml") - secureGWPolicyManifest = getTestFile("secured-gateway-policy.yaml") - secureGWPolicyWithRbacManifest = getTestFile("secured-gateway-policy-with-rbac.yaml") - secureRoutePolicyManifest = getTestFile("secured-route.yaml") - secureRoutePolicyWithRbacManifest = getTestFile("secured-route-with-rbac.yaml") + insecureRouteManifest = getTestFile("insecure-route.yaml") + secureGWPolicyManifestBackend = getTestFile("secured-gateway-policy-with-backend.yaml") + secureGWPolicyManifestBackendAndTlsPolicy = getTestFile("secured-gateway-policy-with-backend-and-ref.yaml") + secureGWPolicyManifestSvc = getTestFile("secured-gateway-policy-with-svc.yaml") + secureGWPolicyManifestSvcCaCert = getTestFile("secured-gateway-policy-with-svc-ca-cert.yaml") + secureGWPolicyWithRbacManifest = getTestFile("secured-gateway-policy-with-rbac.yaml") + secureRoutePolicyManifestBackend = getTestFile("secured-route-with-backend.yaml") + secureRoutePolicyManifestBackendAndTlsPolicy = getTestFile("secured-route-with-backend-and-ref.yaml") + secureRoutePolicyManifestSvc = getTestFile("secured-route-with-svc.yaml") + secureRoutePolicyManifestSvcCaCert = getTestFile("secured-route-with-svc-ca-cert.yaml") + secureRoutePolicyWithRbacManifest = getTestFile("secured-route-with-rbac.yaml") ) -func (s *testingSuite) TestRoutePolicy() { +func (s *testingSuite) TestRoutePolicyBackend() { s.TestInstallation.Assertions.EventuallyHTTPRouteCondition( s.Ctx, "route-example-insecure", @@ -111,7 +137,73 @@ func (s *testingSuite) TestRoutePolicy() { s.assertResponseWithoutAuth("secureroute.com", http.StatusUnauthorized) } -func (s *testingSuite) TestGatewayPolicy() { +func (s *testingSuite) TestRoutePolicyBackendAndTlsPolicy() { + s.TestInstallation.Assertions.EventuallyHTTPRouteCondition( + s.Ctx, + "route-secure", + "default", + gwv1.RouteConditionAccepted, + metav1.ConditionTrue, + ) + // verify a provider with a single key in jwks works + s.assertResponse("secureroute.com", JwtOrgOne, http.StatusOK) + // verify invalid/missing tokens are caught + s.assertResponse("secureroute.com", "nosuchkey", http.StatusUnauthorized) + s.assertResponseWithoutAuth("secureroute.com", http.StatusUnauthorized) +} + +func (s *testingSuite) TestRoutePolicySvcCaCert() { + s.TestRoutePolicySvc() +} + +func (s *testingSuite) TestRoutePolicySvc() { + s.TestInstallation.Assertions.EventuallyHTTPRouteCondition( + s.Ctx, + "route-secure", + "default", + gwv1.RouteConditionAccepted, + metav1.ConditionTrue, + ) + // verify a provider with a single key in jwks works + s.assertResponse("secureroute.com", JwtOrgOne, http.StatusOK) + // verify invalid/missing tokens are caught + s.assertResponse("secureroute.com", "nosuchkey", http.StatusUnauthorized) + s.assertResponseWithoutAuth("secureroute.com", http.StatusUnauthorized) +} + +func (s *testingSuite) TestRoutePolicyWithRbac() { + s.TestInstallation.Assertions.EventuallyHTTPRouteCondition( + s.Ctx, + "route-secure", + "default", + gwv1.RouteConditionAccepted, + metav1.ConditionTrue, + ) + // verify a jwt with expected subject works + s.assertResponse("secureroute.com", JwtOrgOne, http.StatusOK) + // verify a jwt with unexpected subject is denied + s.assertResponse("secureroute.com", jwtOrgFour, http.StatusForbidden) +} + +func (s *testingSuite) TestGatewayPolicySvc() { + s.TestInstallation.Assertions.EventuallyHTTPRouteCondition( + s.Ctx, + "route-secure-gw", + "default", + gwv1.RouteConditionAccepted, + metav1.ConditionTrue, + ) + s.assertResponse("securegateways.com", JwtOrgOne, http.StatusOK) + // verify invalid/missing tokens are caught + s.assertResponse("securegateways.com", "nosuchkey", http.StatusUnauthorized) + s.assertResponseWithoutAuth("securegateways.com", http.StatusUnauthorized) +} + +func (s *testingSuite) TestGatewayPolicySvcCaCert() { + s.TestGatewayPolicySvc() +} + +func (s *testingSuite) TestGatewayPolicyBackend() { s.TestInstallation.Assertions.EventuallyHTTPRouteCondition( s.Ctx, "route-secure-gw", @@ -126,6 +218,34 @@ func (s *testingSuite) TestGatewayPolicy() { s.assertResponseWithoutAuth("securegateways.com", http.StatusUnauthorized) } +func (s *testingSuite) TestGatewayPolicyBackendWithTlsPolicy() { + s.TestInstallation.Assertions.EventuallyHTTPRouteCondition( + s.Ctx, + "route-secure-gw", + "default", + gwv1.RouteConditionAccepted, + metav1.ConditionTrue, + ) + s.assertResponse("securegateways.com", JwtOrgOne, http.StatusOK) + // verify invalid/missing tokens are caught + s.assertResponse("securegateways.com", "nosuchkey", http.StatusUnauthorized) + s.assertResponseWithoutAuth("securegateways.com", http.StatusUnauthorized) +} + +func (s *testingSuite) TestGatewayPolicyWithRbac() { + s.TestInstallation.Assertions.EventuallyHTTPRouteCondition( + s.Ctx, + "route-secure-gw", + "default", + gwv1.RouteConditionAccepted, + metav1.ConditionTrue, + ) + // verify a jwt with expected subject works + s.assertResponse("securegateways.com", JwtOrgOne, http.StatusOK) + // verify a jwt with unexpected subject is denied + s.assertResponse("securegateways.com", jwtOrgFour, http.StatusForbidden) +} + func (s *testingSuite) assertResponse(hostHeader, authHeader string, expectedStatus int) { s.testInstallation.Assertions.AssertEventualCurlResponse( s.Ctx, diff --git a/test/e2e/features/agentgateway/remotejwtauth/testdata/common.yaml b/test/e2e/features/agentgateway/remotejwtauth/testdata/common.yaml index 0fbfcc0ac28..cee4937f8e5 100644 --- a/test/e2e/features/agentgateway/remotejwtauth/testdata/common.yaml +++ b/test/e2e/features/agentgateway/remotejwtauth/testdata/common.yaml @@ -10,4 +10,44 @@ spec: name: http allowedRoutes: namespaces: - from: All \ No newline at end of file + from: All +--- +apiVersion: v1 +data: + ca.crt: | + -----BEGIN CERTIFICATE----- + MIIFfDCCA2SgAwIBAgIUOBEwNkgGCBk5gTlks4MgZjBwcB0wDQYJKoZIhvcNAQEL + BQAwKzEpMCcGA1UEAwwgZHVtbXktaWRwLmRlZmF1bHQsTz1rZ2F0ZXdheS5kZXYw + HhcNMjUxMjEyMjIyNTAyWhcNMzUxMjEwMjIyNTAyWjArMSkwJwYDVQQDDCBkdW1t + eS1pZHAuZGVmYXVsdCxPPWtnYXRld2F5LmRldjCCAiIwDQYJKoZIhvcNAQEBBQAD + ggIPADCCAgoCggIBAKPDXO2JEDlruWLQACZqQyFoJTw9dUpay+QcVrgnDv8ULM9F + wSVpIgiT7/reqfWQsyWH8bhyZ60SD2v6BqRdvU8d5G7Lzjjiv7D1kRmdoM05rHeW + rFWrMsd3tTVYIdkDwsOqb/4/3YXhzZstI8N9I9mqQFfR0Oahjwub1fQqGkU4AldO + WGTgsllI0ZDV8IDuARlOQ8ZysxL2axxXJ4Io4eDMZ6uwbeW5JXv/ajLz3Gx9vpWf + LlfPHCB4/Z+EErw/g55PEM8ftvK5ijT2+QPULSdrkO/YjByV9IPNjYou9JEcI1KP + Q2q4VcjQV83dcRFDw11o6MhOicVNwdTFBia6aStpxU/fsYaoaPiK0OWOZ3SjtoNV + PT17geh5kX+4eTmzdC/9hFh+qncyzfHdomBFQlamQ5Pzg3ngLoNm5Iyk/OuUgLg8 + sgYf7coYDygzzagxxpTRS7VyfwqLlMaRbqBUrX9IHVpn17CqtsrI1ihadv9q4wc3 + Mxt2rdT1GfpE7yCB/NrAzCe2ZVWkNkX8Zb0taD79r/daOBgakHf9L/EqYTsgGO3s + XiF7G3lbRpLwOKHiHP9YbQCdoh8Y3qzGi9DLlmDIaQShtJPUmCb7u7kL9bW2SPRL + +zH2ZY5258CZWndAGe06wQVgLv0aI7kre+Sf1YfZxRbzE595TBWQO/RRT3I7AgMB + AAGjgZcwgZQwHQYDVR0OBBYEFAIkfyn6riDFT/LhatXG1uS5u8HKMB8GA1UdIwQY + MBaAFAIkfyn6riDFT/LhatXG1uS5u8HKMA8GA1UdEwEB/wQFMAMBAf8wQQYDVR0R + BDowOIIRZHVtbXktaWRwLmRlZmF1bHSCI2R1bW15LWlkcC5kZWZhdWx0LnN2Yy5j + bHVzdGVyLmxvY2FsMA0GCSqGSIb3DQEBCwUAA4ICAQAxzxHhT9uvTBHKeu+7zOdU + A+rju5gPjeItds3r2YdHqqjidkK53qWnvrqteoguT8lxGXaSL0QzL3l9eFp80BIP + 8MmlI+zs8Q/cO9gCeEf+3ul+nx2YzF33W/PNahHfLDbLIFDoQMkhTyemEh1GEqmm + 6frHgO2OgdIO6jyIF0GN0SFvCW6J32k3teRsN2OLRQCuCftJ/Q2dwuXZfmx0sf0R + Hz7JNBdH9U8iCYhSefd3VWCro2sPB3XT7evH9+orFikvbb5fggo4WGjvc7CPKlMj + 59PGlloJCUP9FIhR5/oot6yH9NsdOzDWY51makMhE4nq/ET8omaawSCclTE8mDWk + +s/8MBQkk6T72zaVX6Eqnb0RatIHkr9C6zfy/ZE4E5A6Lw+EwdGPaXg5pCBO0miM + jImoFyNvXEGWY3w6AX8ho1L27ZiTApMTc2fYUYCy4QP+MDjEp1+yFrjFSFpUhF0Z + +Tl37cUWZcm4nUxEcu/pfedKyliR2yKBfi3jg7cDzVB86tSHzIvPgxpl2ivEEb0E + ohncCC1Z//SKb7QFs1Obry3hIIBpEyVVvGB580AdxgLY9nhrvv/6gw01JtEPXczV + 1BTCWIUc6WafBlAiWrm3tR36kaRn2RrIlCAFrMznQMafCfMLCTWsYudkrabl7W9n + yamda6yFfH9bkPO+XBK3lQ== + -----END CERTIFICATE----- +kind: ConfigMap +metadata: + name: ca + namespace: default diff --git a/test/e2e/features/agentgateway/remotejwtauth/testdata/secured-gateway-policy-with-backend-and-ref.yaml b/test/e2e/features/agentgateway/remotejwtauth/testdata/secured-gateway-policy-with-backend-and-ref.yaml new file mode 100644 index 00000000000..1265c873315 --- /dev/null +++ b/test/e2e/features/agentgateway/remotejwtauth/testdata/secured-gateway-policy-with-backend-and-ref.yaml @@ -0,0 +1,59 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: route-secure-gw +spec: + parentRefs: + - name: super-gateway + hostnames: + - "securegateways.com" + rules: + - backendRefs: + - name: backend-0 + port: 8080 +--- +# jwks were generated using hack/utils/jwt/jwt-generator.go +apiVersion: agentgateway.dev/v1alpha1 +kind: AgentgatewayPolicy +metadata: + name: gw-policy +spec: + targetRefs: + - group: gateway.networking.k8s.io + kind: Gateway + name: super-gateway + traffic: + jwtAuthentication: + mode: Strict + providers: + - issuer: https://kgateway.dev + jwks: + remote: + jwksPath: org-one/keys + backendRef: + group: agentgateway.dev + kind: AgentgatewayBackend + name: dummy-idp + port: 8443 +--- +apiVersion: agentgateway.dev/v1alpha1 +kind: AgentgatewayBackend +metadata: + name: dummy-idp +spec: + static: + host: dummy-idp.default + port: 8443 +--- +apiVersion: agentgateway.dev/v1alpha1 +kind: AgentgatewayPolicy +metadata: + name: idp-policy +spec: + targetRefs: + - group: agentgateway.dev + kind: AgentgatewayBackend + name: dummy-idp + backend: + tls: + insecureSkipVerify: All \ No newline at end of file diff --git a/test/e2e/features/agentgateway/remotejwtauth/testdata/secured-gateway-policy-with-backend.yaml b/test/e2e/features/agentgateway/remotejwtauth/testdata/secured-gateway-policy-with-backend.yaml new file mode 100644 index 00000000000..3df4900dee6 --- /dev/null +++ b/test/e2e/features/agentgateway/remotejwtauth/testdata/secured-gateway-policy-with-backend.yaml @@ -0,0 +1,71 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: route-secure-gw +spec: + parentRefs: + - name: super-gateway + hostnames: + - "securegateways.com" + rules: + - backendRefs: + - name: backend-0 + port: 8080 +--- +# jwks were generated using hack/utils/jwt/jwt-generator.go +apiVersion: agentgateway.dev/v1alpha1 +kind: AgentgatewayPolicy +metadata: + name: gw-policy +spec: + targetRefs: + - group: gateway.networking.k8s.io + kind: Gateway + name: super-gateway + traffic: + jwtAuthentication: + mode: Strict + providers: + - issuer: https://kgateway.dev + jwks: + remote: + jwksPath: org-one/keys + backendRef: + group: agentgateway.dev + kind: AgentgatewayBackend + name: dummy-idp-1 + port: 8443 + - issuer: https://kgateway.dev + jwks: + remote: + jwksPath: org-two/keys + backendRef: + group: agentgateway.dev + kind: AgentgatewayBackend + name: dummy-idp + port: 8443 +--- +apiVersion: agentgateway.dev/v1alpha1 +kind: AgentgatewayBackend +metadata: + name: dummy-idp +spec: + policies: + tls: + insecureSkipVerify: All + static: + host: dummy-idp.default + port: 8443 +--- +apiVersion: agentgateway.dev/v1alpha1 +kind: AgentgatewayBackend +metadata: + name: dummy-idp-1 +spec: + policies: + tls: + caCertificateRefs: + - name: ca + static: + host: dummy-idp.default + port: 8443 \ No newline at end of file diff --git a/test/e2e/features/agentgateway/remotejwtauth/testdata/secured-gateway-policy-with-rbac.yaml b/test/e2e/features/agentgateway/remotejwtauth/testdata/secured-gateway-policy-with-rbac.yaml new file mode 100644 index 00000000000..081b7b5ae16 --- /dev/null +++ b/test/e2e/features/agentgateway/remotejwtauth/testdata/secured-gateway-policy-with-rbac.yaml @@ -0,0 +1,64 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: route-secure-gw +spec: + parentRefs: + - name: super-gateway + hostnames: + - "securegateways.com" + rules: + - backendRefs: + - name: backend-0 + port: 8080 +--- +# jwks were generated using hack/utils/jwt/jwt-generator.go +apiVersion: agentgateway.dev/v1alpha1 +kind: AgentgatewayPolicy +metadata: + name: gw-policy +spec: + targetRefs: + - group: gateway.networking.k8s.io + kind: Gateway + name: super-gateway + traffic: + authorization: + action: Allow + policy: + matchExpressions: + - 'jwt.sub == "ignore@kgateway.dev"' + jwtAuthentication: + mode: Strict + providers: + - issuer: https://kgateway.dev + jwks: + remote: + jwksPath: org-one/keys + backendRef: + group: "" + kind: Service + name: dummy-idp + port: 8443 + - issuer: https://kgateway.dev + jwks: + remote: + jwksPath: org-four/keys + backendRef: + group: "" + kind: Service + name: dummy-idp + port: 8443 +--- +apiVersion: agentgateway.dev/v1alpha1 +kind: AgentgatewayPolicy +metadata: + name: idp-policy +spec: + targetRefs: + - group: "" + kind: Service + name: dummy-idp + backend: + tls: + insecureSkipVerify: All diff --git a/test/e2e/features/agentgateway/remotejwtauth/testdata/secured-gateway-policy-with-svc-ca-cert.yaml b/test/e2e/features/agentgateway/remotejwtauth/testdata/secured-gateway-policy-with-svc-ca-cert.yaml new file mode 100644 index 00000000000..52bcc85d0e6 --- /dev/null +++ b/test/e2e/features/agentgateway/remotejwtauth/testdata/secured-gateway-policy-with-svc-ca-cert.yaml @@ -0,0 +1,51 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: route-secure-gw +spec: + parentRefs: + - name: super-gateway + hostnames: + - "securegateways.com" + rules: + - backendRefs: + - name: backend-0 + port: 8080 +--- +# jwks were generated using hack/utils/jwt/jwt-generator.go +apiVersion: agentgateway.dev/v1alpha1 +kind: AgentgatewayPolicy +metadata: + name: gw-policy +spec: + targetRefs: + - group: gateway.networking.k8s.io + kind: Gateway + name: super-gateway + traffic: + jwtAuthentication: + mode: Strict + providers: + - issuer: https://kgateway.dev + jwks: + remote: + jwksPath: org-one/keys + backendRef: + group: "" + kind: Service + name: dummy-idp + port: 8443 +--- +apiVersion: agentgateway.dev/v1alpha1 +kind: AgentgatewayPolicy +metadata: + name: idp-policy +spec: + targetRefs: + - group: "" + kind: Service + name: dummy-idp + backend: + tls: + caCertificateRefs: + - name: ca \ No newline at end of file diff --git a/test/e2e/features/agentgateway/remotejwtauth/testdata/secured-gateway-policy.yaml b/test/e2e/features/agentgateway/remotejwtauth/testdata/secured-gateway-policy-with-svc.yaml similarity index 62% rename from test/e2e/features/agentgateway/remotejwtauth/testdata/secured-gateway-policy.yaml rename to test/e2e/features/agentgateway/remotejwtauth/testdata/secured-gateway-policy-with-svc.yaml index c5db4c7dbc4..3e1568851f0 100644 --- a/test/e2e/features/agentgateway/remotejwtauth/testdata/secured-gateway-policy.yaml +++ b/test/e2e/features/agentgateway/remotejwtauth/testdata/secured-gateway-policy-with-svc.yaml @@ -29,8 +29,22 @@ spec: - issuer: https://kgateway.dev jwks: remote: - uri: https://dummy-idp.default:8443/org-one/keys - - issuer: https://kgateway.dev - jwks: - remote: - uri: https://dummy-idp.default:8443/org-two/keys + jwksPath: org-one/keys + backendRef: + group: "" + kind: Service + name: dummy-idp + port: 8443 +--- +apiVersion: agentgateway.dev/v1alpha1 +kind: AgentgatewayPolicy +metadata: + name: idp-policy +spec: + targetRefs: + - group: "" + kind: Service + name: dummy-idp + backend: + tls: + insecureSkipVerify: All diff --git a/test/e2e/features/agentgateway/remotejwtauth/testdata/secured-route-with-backend-and-ref.yaml b/test/e2e/features/agentgateway/remotejwtauth/testdata/secured-route-with-backend-and-ref.yaml new file mode 100644 index 00000000000..53112fd56b6 --- /dev/null +++ b/test/e2e/features/agentgateway/remotejwtauth/testdata/secured-route-with-backend-and-ref.yaml @@ -0,0 +1,59 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: route-secure +spec: + parentRefs: + - name: super-gateway + hostnames: + - "secureroute.com" + rules: + - backendRefs: + - name: backend-0 + port: 8080 +--- +# jwks were generated using hack/utils/jwt/jwt-generator.go +apiVersion: agentgateway.dev/v1alpha1 +kind: AgentgatewayPolicy +metadata: + name: route-policy +spec: + targetRefs: + - group: gateway.networking.k8s.io + kind: HTTPRoute + name: route-secure + traffic: + jwtAuthentication: + mode: Strict + providers: + - issuer: https://kgateway.dev + jwks: + remote: + jwksPath: org-one/keys + backendRef: + group: agentgateway.dev + kind: AgentgatewayBackend + name: dummy-idp + port: 8443 +--- +apiVersion: agentgateway.dev/v1alpha1 +kind: AgentgatewayBackend +metadata: + name: dummy-idp +spec: + static: + host: dummy-idp.default + port: 8443 +--- +apiVersion: agentgateway.dev/v1alpha1 +kind: AgentgatewayPolicy +metadata: + name: idp-policy +spec: + targetRefs: + - group: agentgateway.dev + kind: AgentgatewayBackend + name: dummy-idp + backend: + tls: + insecureSkipVerify: All \ No newline at end of file diff --git a/test/e2e/features/agentgateway/remotejwtauth/testdata/secured-route-with-backend.yaml b/test/e2e/features/agentgateway/remotejwtauth/testdata/secured-route-with-backend.yaml new file mode 100644 index 00000000000..12e22dc6d81 --- /dev/null +++ b/test/e2e/features/agentgateway/remotejwtauth/testdata/secured-route-with-backend.yaml @@ -0,0 +1,71 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: route-secure +spec: + parentRefs: + - name: super-gateway + hostnames: + - "secureroute.com" + rules: + - backendRefs: + - name: backend-0 + port: 8080 +--- +# jwks were generated using hack/utils/jwt/jwt-generator.go +apiVersion: agentgateway.dev/v1alpha1 +kind: AgentgatewayPolicy +metadata: + name: route-policy +spec: + targetRefs: + - group: gateway.networking.k8s.io + kind: HTTPRoute + name: route-secure + traffic: + jwtAuthentication: + mode: Strict + providers: + - issuer: https://kgateway.dev + jwks: + remote: + jwksPath: org-one/keys + backendRef: + group: agentgateway.dev + kind: AgentgatewayBackend + name: dummy-idp-1 + port: 8443 + - issuer: https://kgateway.dev + jwks: + remote: + jwksPath: org-two/keys + backendRef: + group: agentgateway.dev + kind: AgentgatewayBackend + name: dummy-idp + port: 8443 +--- +apiVersion: agentgateway.dev/v1alpha1 +kind: AgentgatewayBackend +metadata: + name: dummy-idp +spec: + policies: + tls: + insecureSkipVerify: All + static: + host: dummy-idp.default + port: 8443 +--- +apiVersion: agentgateway.dev/v1alpha1 +kind: AgentgatewayBackend +metadata: + name: dummy-idp-1 +spec: + policies: + tls: + caCertificateRefs: + - name: ca + static: + host: dummy-idp.default + port: 8443 \ No newline at end of file diff --git a/test/e2e/features/agentgateway/remotejwtauth/testdata/secured-route-with-rbac.yaml b/test/e2e/features/agentgateway/remotejwtauth/testdata/secured-route-with-rbac.yaml new file mode 100644 index 00000000000..912b2a021e1 --- /dev/null +++ b/test/e2e/features/agentgateway/remotejwtauth/testdata/secured-route-with-rbac.yaml @@ -0,0 +1,64 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: route-secure +spec: + parentRefs: + - name: super-gateway + hostnames: + - "secureroute.com" + rules: + - backendRefs: + - name: backend-0 + port: 8080 +--- +# jwks were generated using hack/utils/jwt/jwt-generator.go +apiVersion: agentgateway.dev/v1alpha1 +kind: AgentgatewayPolicy +metadata: + name: route-policy +spec: + targetRefs: + - group: gateway.networking.k8s.io + kind: HTTPRoute + name: route-secure + traffic: + authorization: + action: Allow + policy: + matchExpressions: + - 'jwt.sub == "ignore@kgateway.dev"' + jwtAuthentication: + mode: Strict + providers: + - issuer: https://kgateway.dev + jwks: + remote: + jwksPath: org-one/keys + backendRef: + group: "" + kind: Service + name: dummy-idp + port: 8443 + - issuer: https://kgateway.dev + jwks: + remote: + jwksPath: org-four/keys + backendRef: + group: "" + kind: Service + name: dummy-idp + port: 8443 +--- +apiVersion: agentgateway.dev/v1alpha1 +kind: AgentgatewayPolicy +metadata: + name: idp-policy +spec: + targetRefs: + - group: "" + kind: Service + name: dummy-idp + backend: + tls: + insecureSkipVerify: All diff --git a/test/e2e/features/agentgateway/remotejwtauth/testdata/secured-route-with-svc-ca-cert.yaml b/test/e2e/features/agentgateway/remotejwtauth/testdata/secured-route-with-svc-ca-cert.yaml new file mode 100644 index 00000000000..13bd5a97320 --- /dev/null +++ b/test/e2e/features/agentgateway/remotejwtauth/testdata/secured-route-with-svc-ca-cert.yaml @@ -0,0 +1,51 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: route-secure +spec: + parentRefs: + - name: super-gateway + hostnames: + - "secureroute.com" + rules: + - backendRefs: + - name: backend-0 + port: 8080 +--- +# jwks were generated using hack/utils/jwt/jwt-generator.go +apiVersion: agentgateway.dev/v1alpha1 +kind: AgentgatewayPolicy +metadata: + name: route-policy +spec: + targetRefs: + - group: gateway.networking.k8s.io + kind: HTTPRoute + name: route-secure + traffic: + jwtAuthentication: + mode: Strict + providers: + - issuer: https://kgateway.dev + jwks: + remote: + jwksPath: org-one/keys + backendRef: + group: "" + kind: Service + name: dummy-idp + port: 8443 +--- +apiVersion: agentgateway.dev/v1alpha1 +kind: AgentgatewayPolicy +metadata: + name: idp-policy +spec: + targetRefs: + - group: "" + kind: Service + name: dummy-idp + backend: + tls: + caCertificateRefs: + - name: ca \ No newline at end of file diff --git a/test/e2e/features/agentgateway/remotejwtauth/testdata/secured-route.yaml b/test/e2e/features/agentgateway/remotejwtauth/testdata/secured-route-with-svc.yaml similarity index 62% rename from test/e2e/features/agentgateway/remotejwtauth/testdata/secured-route.yaml rename to test/e2e/features/agentgateway/remotejwtauth/testdata/secured-route-with-svc.yaml index 90ad2d66a55..072a85f2d60 100644 --- a/test/e2e/features/agentgateway/remotejwtauth/testdata/secured-route.yaml +++ b/test/e2e/features/agentgateway/remotejwtauth/testdata/secured-route-with-svc.yaml @@ -29,8 +29,22 @@ spec: - issuer: https://kgateway.dev jwks: remote: - uri: https://dummy-idp.default:8443/org-one/keys - - issuer: https://kgateway.dev - jwks: - remote: - uri: https://dummy-idp.default:8443/org-two/keys + jwksPath: org-one/keys + backendRef: + group: "" + kind: Service + name: dummy-idp + port: 8443 +--- +apiVersion: agentgateway.dev/v1alpha1 +kind: AgentgatewayPolicy +metadata: + name: idp-policy +spec: + targetRefs: + - group: "" + kind: Service + name: dummy-idp + backend: + tls: + insecureSkipVerify: All