├── LICENSE ├── README.md ├── app-deployment.yaml ├── authz_server ├── Dockerfile ├── go.mod ├── go.sum └── grpc_server.go ├── backend ├── Dockerfile ├── app.js └── package.json ├── certs ├── CA_crt.pem ├── svc_crt.pem └── svc_key.pem ├── ext_authz.yaml.tmpl ├── ext_authz_rules.yaml.tmpl ├── frontend ├── Dockerfile ├── app.js └── package.json ├── images ├── authz_ns_flow_fe.png ├── authz_ns_flow_full.png ├── config_img.png ├── default-traffic.png └── istio-extauthz.svg ├── istio-app-config.yaml ├── istio-ingress-gateway.yaml ├── istio-lb-certs.yaml └── jwt_client ├── generateToken.go ├── go.mod └── go.sum /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # External Authorization Server with Istio 2 | 3 | Tutorial to setup an external authorization server for istio. In this setup, the `ingresss-gateway` will first send the inbound request headers to another istio service which check the header values submitted by the remote user/client. If the header values passes some criteria, the external authorization server will instruct the authorization server to proceed with the request upstream. 4 | 5 | The check criteria can be anything (kerberos ticket, custom JWT) but in this example, it is the simple presence of the header value match as defined in configuration. 6 | 7 | In this setup, it is important to ensure the authorization server is always (and exclusively) called by the ingress gateway and that the upstream services must accept the custom JWT token issued by the authorization server. 8 | 9 | To that end, this configuration sets up `mTLS`, `RBAC` and `ORIGIN` authentication. RBAC ensures service->service traffic flows between the gateway, authorization server and the upstream systems. Each upstream service will only allow `ORIGIN` JWT tokens issued by the authorization server. 10 | 11 | 12 | ![images/istio-extauthz.svg](images/istio-extauthz.svg) 13 | 14 | This tutorial is a continuation of the [istio helloworld](https://github.com/salrashid123/istio_helloworld) application. 15 | 16 | >> `12/11/24`: Use minikube 17 | >> `11/25/21`: Updated for example to NOT use an actual service account. Instead, use the istio built [gen-jwtpy](https://istio.io/v1.10/docs/tasks/security/authentication/authn-policy/#end-user-authentication) in JWT issuers 18 | 19 | >> `3/20/21`: Updated for [istio 1.9: Integrate external authorization system (e.g. OPA, oauth2-proxy, etc.) with Istio using AuthorizationPolicy](https://istio.io/latest/blog/2021/better-external-authz/). Part of the upgrade is to use the `v3` API (`go-control-plane/envoy/config/core/v3`, `go-control-plane/envoy/service/auth/v3`) 20 | 21 | ### References 22 | 23 | - [Envoy External Authorization](https://www.envoyproxy.io/docs/envoy/latest/api-v2/config/filter/http/ext_authz/v2/ext_authz.proto) 24 | - [Envoy External Authorization server (envoy.ext_authz) HelloWorld](https://github.com/salrashid123/envoy_external_authz) 25 | - [Istio Security](https://istio.io/docs/concepts/security/) 26 | - [External authorization with custom action](https://istio.io/latest/docs/tasks/security/authorization/authz-custom/) 27 | 28 | 29 | ### Setup 30 | 31 | The following setup uses a minikube and a convenient JWK endpoint provided by an Istio sample JWT authentication tutorial. 32 | 33 | First install istio 34 | 35 | ```bash 36 | minikube start --driver=kvm2 --cpus=4 --kubernetes-version=v1.28 --host-only-cidr 192.168.39.1/24 37 | minikube addons enable metallb 38 | 39 | ## in a new window 40 | minikube dashboard 41 | 42 | ## get the IP, for me it was the following 43 | $ minikube ip 44 | 192.168.39.1 45 | 46 | ## setup a loadbalancer metallb, enter the ip range shown below 47 | minikube addons configure metallb 48 | # -- Enter Load Balancer Start IP: 192.168.39.104 49 | # -- Enter Load Balancer End IP: 192.168.39.110 50 | 51 | ## download and install istio 52 | export ISTIO_VERSION=1.24.0 53 | export ISTIO_VERSION_MINOR=1.24 54 | 55 | wget -P /tmp/ https://github.com/istio/istio/releases/download/$ISTIO_VERSION/istio-$ISTIO_VERSION-linux-amd64.tar.gz 56 | tar xvf /tmp/istio-$ISTIO_VERSION-linux-amd64.tar.gz -C /tmp/ 57 | rm /tmp/istio-$ISTIO_VERSION-linux-amd64.tar.gz 58 | 59 | export PATH=/tmp/istio-$ISTIO_VERSION/bin:$PATH 60 | 61 | istioctl install --set profile=demo \ 62 | --set meshConfig.enableAutoMtls=true \ 63 | --set values.gateways.istio-ingressgateway.runAsRoot=true \ 64 | --set meshConfig.outboundTrafficPolicy.mode=REGISTRY_ONLY \ 65 | --set meshConfig.defaultConfig.gatewayTopology.forwardClientCertDetails=SANITIZE_SET 66 | ``` 67 | 68 | 69 | ### Build and push images 70 | 71 | You can use the following prebuilt containers for this tutorial if you want to. 72 | 73 | If you would rather build and stage your own, the `Dockerfile` for each container is provided in this repo. 74 | 75 | The images we will use here has the following endpoints enabled: 76 | 77 | * `salrashid123/svc`: Frontend service 78 | - `/version`: Displays a static "version" number for the image. If using `salrashid123/svc:1` then the version is `1`. If using `salrashid123/svc:2` the version is `2` 79 | - `/backend`: Makes an HTTP Ret call to the backend service's `/backend` and `/headerz` endpoints. 80 | 81 | * `salrashid123/besvc`: Backend Service 82 | - `/headerz`: Displays the http headers 83 | - `/backend`: Displays the pod name 84 | 85 | * `salrashid123/ext-authz-server`: External Authorization gRPC Server 86 | - gRPC Authorization server running in namespace `authz-ns` as service `authz` 87 | - Authorization server reads an environment variable that lists the set of authorized (eg `authzallowedusers: "alice,bob"`) 88 | This server will read the "Authorization: Bearer " header value from the incoming request to determine the username 89 | 90 | 91 | To build your own, create a public dockerhub images with the names specified below: 92 | 93 | - Build External Authorization Server (you can ofcourse use your own dockerhub repo!) 94 | 95 | ```bash 96 | cd authz_server/ 97 | docker build -t salrashid123/ext-authz-server . 98 | docker push salrashid123/ext-authz-server 99 | ``` 100 | - Build Frontend 101 | ```bash 102 | cd frontend 103 | docker build --build-arg VER=1 -t salrashid123/svc:1 . 104 | docker build --build-arg VER=2 -t salrashid123/svc:2 . 105 | docker push salrashid123/svc:1 106 | docker push salrashid123/svc:2 107 | ``` 108 | 109 | - Build Backend 110 | ```bash 111 | cd backend 112 | docker build --build-arg VER=1 -t salrashid123/besvc:1 . 113 | docker build --build-arg VER=1 -t salrashid123/besvc:2 . 114 | 115 | docker push salrashid123/besvc:1 116 | docker push salrashid123/besvc:2 117 | ``` 118 | 119 | ### Verify istio is installed 120 | 121 | ```bash 122 | kubectl label namespace default istio-injection=enabled 123 | kubectl get no,po,rc,svc,ing,deployment -n istio-system 124 | ``` 125 | 126 | ### Deploy Istio Gateway and services 127 | 128 | ```bash 129 | kubectl apply -f istio-lb-certs.yaml 130 | sleep 10 131 | ## create the ingress gateway 132 | kubectl apply -f istio-ingress-gateway.yaml 133 | 134 | ## kill and restart the ingress pod since the LB cert's may not have been loaded 135 | INGRESS_POD_NAME=$(kubectl get po -n istio-system | grep ingressgateway\- | awk '{print$1}'); echo ${INGRESS_POD_NAME}; 136 | kubectl delete po/$INGRESS_POD_NAME -n istio-system 137 | 138 | kubectl apply -f istio-app-config.yaml 139 | 140 | export GATEWAY_IP=$(kubectl -n istio-system get service istio-ingressgateway -o jsonpath='{.status.loadBalancer.ingress[0].ip}') 141 | echo $GATEWAY_IP 142 | ``` 143 | 144 | #### Debugging ingress-gateway 145 | 146 | ```bash 147 | INGRESS_POD_NAME=$(kubectl get po -n istio-system | grep ingressgateway\- | awk '{print$1}'); echo ${INGRESS_POD_NAME}; 148 | kubectl exec --namespace=istio-system $INGRESS_POD_NAME -c istio-proxy -- curl -X POST http://localhost:15000/logging\?level\=debug 149 | kubectl logs $INGRESS_POD_NAME -n istio-system 150 | ``` 151 | 152 | 153 | ### Deploy application 154 | 155 | Deploy the baseline application without the external authorization server 156 | 157 | ```bash 158 | $ kubectl apply -f app-deployment.yaml 159 | 160 | $ kubectl get po,svc 161 | NAME READY STATUS RESTARTS AGE 162 | pod/be-v1-8589f84d6-ll82f 2/2 Running 0 74s 163 | pod/be-v2-6ff75fccd8-chj92 2/2 Running 0 74s 164 | pod/svc1-bdb4d7c59-fgfk5 2/2 Running 0 74s 165 | pod/svc2-7f65cc98f-hxcw9 2/2 Running 0 74s 166 | 167 | NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE 168 | service/be ClusterIP 10.116.6.105 8080/TCP 74s 169 | service/kubernetes ClusterIP 10.116.0.1 443/TCP 5m26s 170 | service/svc1 ClusterIP 10.116.9.247 8080/TCP 75s 171 | service/svc2 ClusterIP 10.116.12.54 8080/TCP 74s 172 | ``` 173 | 174 | 175 | 176 | ### Send Traffic 177 | 178 | Verify traffic for the frontend and backend services. (we're using [jq](https://stedolan.github.io/jq/download/) to help parse the response) 179 | 180 | ```bash 181 | # Access the frontend for svc1,svc2 182 | curl -s --cacert certs/CA_crt.pem -w " %{http_code}\n" --resolve svc1.example.com:443:$GATEWAY_IP https://svc1.example.com/version 183 | curl -s --cacert certs/CA_crt.pem -w " %{http_code}\n" --resolve svc2.example.com:443:$GATEWAY_IP https://svc2.example.com/version 184 | 185 | # Access the backend through svc1,svc2 186 | curl -s --cacert certs/CA_crt.pem -w " %{http_code}\n" --resolve svc1.example.com:443:$GATEWAY_IP https://svc1.example.com/backend | jq '.' 187 | curl -s --cacert certs/CA_crt.pem -w " %{http_code}\n" --resolve svc2.example.com:443:$GATEWAY_IP https://svc2.example.com/backend | jq '.' 188 | ``` 189 | 190 | If you would rather run this in a loop: 191 | 192 | ```bash 193 | for i in {1..1000}; do curl -s -w " %{http_code}\n" --cacert certs/CA_crt.pem --resolve svc1.example.com:443:$GATEWAY_IP https://svc1.example.com/version; sleep 1; done 194 | ``` 195 | 196 | ##### Kiali Dashboard 197 | 198 | If you want, launch the kiali dashboard (default password is `admin/admin`). In a new window, run: 199 | 200 | ```bash 201 | echo $ISTIO_VERSION 202 | echo $ISTIO_VERSION_MINOR 203 | kubectl apply -f https://raw.githubusercontent.com/istio/istio/release-$ISTIO_VERSION_MINOR/samples/addons/prometheus.yaml 204 | sleep 20 205 | kubectl apply -f https://raw.githubusercontent.com/istio/istio/release-$ISTIO_VERSION_MINOR/samples/addons/kiali.yaml 206 | kubectl apply -f https://raw.githubusercontent.com/istio/istio/release-$ISTIO_VERSION_MINOR/samples/addons/grafana.yaml 207 | kubectl apply -f https://raw.githubusercontent.com/istio/istio/release-$ISTIO_VERSION_MINOR/samples/addons/jaeger.yaml 208 | 209 | ### in a new window, install prometheus, kaili, jager and grafana 210 | ## open a tunnel and access the kiali dashboard at http://localhost:20001/kiali (admin/admi) 211 | kubectl -n istio-system port-forward $(kubectl -n istio-system get pod -l app.kubernetes.io/name=kiali -o jsonpath='{.items[0].metadata.name}') 20001:20001 212 | ``` 213 | 214 | ![images/default-traffic.png](images/default-traffic.png) 215 | 216 | ### Generate Authz config 217 | 218 | First we need to setup the auth* configs to use a convenient JWT/JWK issuer istio provides (you can use any jWT issuer, ofcourse; this is just a demo...do not use this in production!!!) 219 | 220 | ##### Use Istio's sample JWT issuer script 221 | 222 | Istio provides a convenient JWT issuer, JWK and script the gateway will for authentication. You are certainly supposed to use your own JWK/JWT issuer; we're just using this one since it has a convenient JWK endpoint to verify the tokens with 223 | 224 | We will use following script to issue a JWT and verify the JWK. This will be the same key that the external authorization server uses. 225 | 226 | ```bash 227 | wget --no-verbose https://raw.githubusercontent.com/istio/istio/release-$ISTIO_VERSION_MINOR/security/tools/jwt/samples/gen-jwt.py 228 | wget --no-verbose https://raw.githubusercontent.com/istio/istio/release-$ISTIO_VERSION_MINOR/security/tools/jwt/samples/key.pem 229 | 230 | # may need pip3 install jwcrypto 231 | python3 gen-jwt.py -aud some.audience -expire 3600 key.pem 232 | ``` 233 | 234 | ```json 235 | { 236 | "alg": "RS256", 237 | "kid": "DHFbpoIUqrY8t2zpA2qXfCmr5VO5ZEr4RzHU_-envvQ", 238 | "typ": "JWT" 239 | } 240 | 241 | { 242 | "aud": "some.audience", 243 | "exp": 1635174518, 244 | "iat": 1635170918, 245 | "iss": "testing@secure.istio.io", 246 | "sub": "testing@secure.istio.io" 247 | } 248 | ``` 249 | 250 | You can also see that its `kid` key-id is visible too `"DHFbpoIUqrY8t2zpA2qXfCmr5VO5ZEr4RzHU_-envvQ"` 251 | 252 | The JWK endpoint istio will use to validate a JWT issued by the authorization server is: 253 | 254 | ```json 255 | $ curl -s https://raw.githubusercontent.com/istio/istio/release-1.10/security/tools/jwt/samples/jwks.json | jq '.' 256 | { 257 | "keys": [ 258 | { 259 | "e": "AQAB", 260 | "kid": "DHFbpoIUqrY8t2zpA2qXfCmr5VO5ZEr4RzHU_-envvQ", 261 | "kty": "RSA", 262 | "n": "xAE7eB6qugXyCAG3yhh7pkDkT65pHymX-P7KfIupjf59vsdo91bSP9C8H07pSAGQO1MV_xFj9VswgsCg4R6otmg5PV2He95lZdHtOcU5DXIg_pbhLdKXbi66GlVeK6ABZOUW3WYtnNHD-91gVuoeJT_DwtGGcp4ignkgXfkiEm4sw-4sfb4qdt5oLbyVpmW6x9cfa7vs2WTfURiCrBoUqgBo_-4WTiULmmHSGZHOjzwa8WtrtOQGsAFjIbno85jp6MnGGGZPYZbDAa_b3y5u-YpW7ypZrvD8BgtKVjgtQgZhLAGezMt0ua3DRrWnKqTZ0BJ_EyxOGuHJrLsn00fnMQ" 263 | } 264 | ] 265 | } 266 | ``` 267 | 268 | NOTE: we will not be issuing these JWTs. The external authorization server will use the private key to reissue a JWT intended for a given service. 269 | 270 | 271 | Apply the preset environment variables to `ext_authz_filter.yaml`: 272 | 273 | ```bash 274 | export SERVICE_ACCOUNT_EMAIL="testing@secure.istio.io" 275 | wget --no-verbose https://raw.githubusercontent.com/istio/istio/release-$ISTIO_VERSION_MINOR/security/tools/jwt/samples/key.pem 276 | export KEY_ID="DHFbpoIUqrY8t2zpA2qXfCmr5VO5ZEr4RzHU_-envvQ" 277 | export SVC_ACCOUNT_KEY=`base64 -w 0 key.pem && echo` 278 | 279 | echo $SERVICE_ACCOUNT_EMAIL 280 | echo $KEY_ID 281 | echo $SVC_ACCOUNT_KEY 282 | ``` 283 | 284 | ### Apply Authz rules 285 | 286 | ```bash 287 | envsubst < "ext_authz_rules.yaml.tmpl" > "ext_authz_rules.yaml" 288 | kubectl apply -f ext_authz_rules.yaml 289 | ``` 290 | 291 | This will cause a 'deny' for everyone since we specified some headers that cannot be met (since we didnt' even deploy the authzserver in the first place that'd issue the JWT we just declared above!) 292 | 293 | 294 | ### Deploy ExtAuthz server 295 | 296 | Edit mesh-config 297 | 298 | ```bash 299 | kubectl edit configmap istio -n istio-system 300 | ``` 301 | 302 | append the section for `extensionProviders` to the top of the `mesh` definition as such (remember to delete the definition of `extensionProviders` already set with `envoyOtelAls`) 303 | 304 | ```yaml 305 | apiVersion: v1 306 | data: 307 | mesh: |- 308 | extensionProviders: 309 | - name: "my-ext-authz-grpc" 310 | envoyExtAuthzGrpc: 311 | service: "authz.authz-ns.svc.cluster.local" 312 | port: "50051" 313 | - name: otel 314 | envoyOtelAls: 315 | port: 4317 316 | service: opentelemetry-collector.istio-system.svc.cluster.local 317 | ``` 318 | ![images/config_image.png](images/config_img.png) 319 | 320 | please note the name for the provider: `"my-ext-authz-grpc"`. This is defined in the `ext_authz.yaml` provider filter 321 | 322 | ```yaml 323 | apiVersion: security.istio.io/v1beta1 324 | kind: AuthorizationPolicy 325 | metadata: 326 | name: ext-authz 327 | namespace: istio-system 328 | spec: 329 | selector: 330 | matchLabels: 331 | istio: ingressgateway 332 | action: CUSTOM 333 | provider: 334 | name: "my-ext-authz-grpc" 335 | rules: 336 | - to: 337 | - operation: 338 | paths: ["/*"] 339 | ``` 340 | 341 | Reload the gateway: 342 | 343 | ```bash 344 | INGRESS_POD_NAME=$(kubectl get po -n istio-system | grep ingressgateway\- | awk '{print$1}'); echo ${INGRESS_POD_NAME}; 345 | kubectl delete po/$INGRESS_POD_NAME -n istio-system 346 | ``` 347 | 348 | Apply the authz config 349 | 350 | ```bash 351 | envsubst < "ext_authz.yaml.tmpl" > "ext_authz.yaml" 352 | kubectl apply -f ext_authz.yaml 353 | ``` 354 | 355 | 356 | ```bash 357 | $ kubectl get PeerAuthentication,RequestAuthentication,AuthorizationPolicy -n authz-ns 358 | NAME MODE AGE 359 | peerauthentication.security.istio.io/ing-authzserver-peer-authn-policy STRICT 13s 360 | 361 | NAME ACTION AGE 362 | authorizationpolicy.security.istio.io/deny-all-authz-ns 13s 363 | authorizationpolicy.security.istio.io/ing-authzserver-authz-policy ALLOW 13s 364 | 365 | $ kubectl get PeerAuthentication,RequestAuthentication,AuthorizationPolicy -n default 366 | NAME AGE 367 | requestauthentication.security.istio.io/ing-svc1-request-authn-policy 109s 368 | requestauthentication.security.istio.io/ing-svc2-request-authn-policy 109s 369 | requestauthentication.security.istio.io/svc-be-v1-request-authn-policy 109s 370 | requestauthentication.security.istio.io/svc-be-v2-request-authn-policy 109s 371 | 372 | NAME ACTION AGE 373 | authorizationpolicy.security.istio.io/deny-all-default 109s 374 | authorizationpolicy.security.istio.io/ing-svc1-authz-policy ALLOW 109s 375 | authorizationpolicy.security.istio.io/ing-svc2-authz-policy ALLOW 109s 376 | authorizationpolicy.security.istio.io/svc1-be-v1-authz-policy ALLOW 109s 377 | authorizationpolicy.security.istio.io/svc1-be-v2-authz-policy ALLOW 109s 378 | 379 | ``` 380 | 381 | ### Access Frontend 382 | 383 | The static/demo configuration here uses two users (`alice`, `bob`), two frontend services (`svc1`,`svc2`) one backend service with two labled versions (`be`, `version=v1`,`version=v2`). 384 | 385 | The following conditions are coded into the authorization server: 386 | 387 | - If the authorization server sees `alice`, it issues a JWT token with `svc1` and `be` as the targets (multiple audiences) 388 | - If the authorization server sees `bob`, it issues a JWT token with `svc2` as the target 389 | - If the authorization server sees `carol`, it issues a JWT token with `svc1` as the target only. 390 | 391 | ```golang 392 | var aud []string 393 | if token == "alice" { 394 | aud = []string{"http://svc1.default.svc.cluster.local:8080/", "http://be.default.svc.cluster.local:8080/"} 395 | } else if token == "bob" { 396 | aud = []string{"http://svc2.default.svc.cluster.local:8080/"} 397 | } else if token == "carol" { 398 | aud = []string{"http://svc1.default.svc.cluster.local:8080/"} 399 | } else { 400 | aud = []string{} 401 | } 402 | ``` 403 | 404 | The net effect of that is `alice` can view `svc1`, `bob` can view `svc2` using `ORIGIN` authentication. 405 | 406 | As Alice: 407 | 408 | ```bash 409 | export USER=alice 410 | 411 | curl -s \ 412 | --cacert certs/CA_crt.pem --resolve svc1.example.com:443:$GATEWAY_IP \ 413 | -H "Authorization: Bearer $USER" \ 414 | -w " %{http_code}\n" \ 415 | https://svc1.example.com/version 416 | 417 | 418 | curl -s \ 419 | --cacert certs/CA_crt.pem --resolve svc2.example.com:443:$GATEWAY_IP \ 420 | -H "Authorization: Bearer $USER" \ 421 | -w " %{http_code}\n" \ 422 | https://svc2.example.com/version 423 | ``` 424 | 425 | ``` 426 | >>> 1 200 427 | >>> Audiences in Jwt are not allowed 403 428 | ``` 429 | 430 | If you want to view the authz logs 431 | 432 | ```bash 433 | AUTHZ_POD_NAME=$(kubectl get po -n authz-ns | grep authz\- | awk '{print$1}'); echo ${AUTHZ_POD_NAME}; 434 | 435 | kubectl logs -n authz-ns $AUTHZ_POD_NAME -c authz-container 436 | ``` 437 | You should see some debug logs as well as the actual reissued JWT header 438 | 439 | ```log 440 | 2024/11/13 12:33:41 Starting gRPC Server at :50051 441 | 2024/11/13 12:34:01 >>> Authorization called check() 442 | 2024/11/13 12:34:01 Authorization Header Bearer alice 443 | 2024/11/13 12:34:01 Using Claim {alice [http://svc1.default.svc.cluster.local:8080/ http://be.default.svc.cluster.local:8080/] {testing@secure.istio.io testing@secure.istio.io [] 2024-11-13 12:35:01.446845225 +0000 UTC m=+79.858574856 2024-11-13 12:34:01.446845119 +0000 UTC m=+19.858574750 }} 444 | 2024/11/13 12:34:01 Issuing outbound Header eyJhbGciOiJSUzI1NiIsImtpZCI6IkRIRmJwb0lVcXJZOHQyenBBMnFYZkNtcjVWTzVaRXI0UnpIVV8tZW52dlEiLCJ0eXAiOiJKV1QifQ.eyJ1aWQiOiJhbGljZSIsImF1ZCI6WyJodHRwOi8vc3ZjMS5kZWZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsOjgwODAvIiwiaHR0cDovL2JlLmRlZmF1bHQuc3ZjLmNsdXN0ZXIubG9jYWw6ODA4MC8iXSwiaXNzIjoidGVzdGluZ0BzZWN1cmUuaXN0aW8uaW8iLCJzdWIiOiJ0ZXN0aW5nQHNlY3VyZS5pc3Rpby5pbyIsImV4cCI6MTczMTUwMTMwMSwiaWF0IjoxNzMxNTAxMjQxfQ.Q-itZwt9AkSjpR3JjHxAyMMdhUOMwythPdB3IH-kZspwP4PH87BGyKNs71WzRqTCbOkd9U9EoiGEO16blV_EFpBQHKkHCIp-T070D2eyJu262MXr3tCwsrp0YHl-tx7qyICoLtMe77LNduI8bWj_2Q61fwALnAOslyH0Cj47u7Gq1FQ0-dFXssR8oMXM8eNSaF30oU2SHf_FGrd56TgJ8gCcl0Qhik6qC11ihjKl8S3_ccw1D48iCX8JlA8cWR5JMTqHhwQEEdTZtMJAR7HB0DuSAMKxWu2ENuyE6_lLDQmbLPbwTW6dy1nJa4JQGA9Eo6JtfWf3FHlAc7QuFfvz3Q 445 | 2024/11/13 12:34:01 >>> Authorization called check() 446 | 2024/11/13 12:34:01 Authorization Header Bearer alice 447 | 2024/11/13 12:34:01 Using Claim {alice [http://svc1.default.svc.cluster.local:8080/ http://be.default.svc.cluster.local:8080/] {testing@secure.istio.io testing@secure.istio.io [] 2024-11-13 12:35:01.504861628 +0000 UTC m=+79.916591235 2024-11-13 12:34:01.50486156 +0000 UTC m=+19.916591168 }} 448 | 2024/11/13 12:34:01 Issuing outbound Header eyJhbGciOiJSUzI1NiIsImtpZCI6IkRIRmJwb0lVcXJZOHQyenBBMnFYZkNtcjVWTzVaRXI0UnpIVV8tZW52dlEiLCJ0eXAiOiJKV1QifQ.eyJ1aWQiOiJhbGljZSIsImF1ZCI6WyJodHRwOi8vc3ZjMS5kZWZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsOjgwODAvIiwiaHR0cDovL2JlLmRlZmF1bHQuc3ZjLmNsdXN0ZXIubG9jYWw6ODA4MC8iXSwiaXNzIjoidGVzdGluZ0BzZWN1cmUuaXN0aW8uaW8iLCJzdWIiOiJ0ZXN0aW5nQHNlY3VyZS5pc3Rpby5pbyIsImV4cCI6MTczMTUwMTMwMSwiaWF0IjoxNzMxNTAxMjQxfQ.Q-itZwt9AkSjpR3JjHxAyMMdhUOMwythPdB3IH-kZspwP4PH87BGyKNs71WzRqTCbOkd9U9EoiGEO16blV_EFpBQHKkHCIp-T070D2eyJu262MXr3tCwsrp0YHl-tx7qyICoLtMe77LNduI8bWj_2Q61fwALnAOslyH0Cj47u7Gq1FQ0-dFXssR8oMXM8eNSaF30oU2SHf_FGrd56TgJ8gCcl0Qhik6qC11ihjKl8S3_ccw1D48iCX8JlA8cWR5JMTqHhwQEEdTZtMJAR7HB0DuSAMKxWu2ENuyE6_lLDQmbLPbwTW6dy1nJa4JQGA9Eo6JtfWf3FHlAc7QuFfvz3Q 449 | 450 | ``` 451 | 452 | note JWT headers include cliams and audiences 453 | 454 | ```json 455 | { 456 | "alg": "RS256", 457 | "kid": "DHFbpoIUqrY8t2zpA2qXfCmr5VO5ZEr4RzHU_-envvQ", 458 | "typ": "JWT" 459 | } 460 | 461 | { 462 | "uid": "alice", 463 | "aud": [ 464 | "http://svc1.default.svc.cluster.local:8080/", 465 | "http://be.default.svc.cluster.local:8080/" 466 | ], 467 | "exp": 1635173568, 468 | "iat": 1635173508, 469 | "iss": "testing@secure.istio.io", 470 | "sub": "testing@secure.istio.io" 471 | } 472 | ``` 473 | 474 | As Bob: 475 | 476 | ```bash 477 | export USER=bob 478 | 479 | curl -s \ 480 | --cacert certs/CA_crt.pem --resolve svc1.example.com:443:$GATEWAY_IP \ 481 | -H "Authorization: Bearer $USER" \ 482 | -w " %{http_code}\n" \ 483 | https://svc1.example.com/version 484 | 485 | 486 | curl -s \ 487 | --cacert certs/CA_crt.pem --resolve svc2.example.com:443:$GATEWAY_IP \ 488 | -H "Authorization: Bearer $USER" \ 489 | -w " %{http_code}\n" \ 490 | https://svc2.example.com/version 491 | ``` 492 | 493 | ``` 494 | >>> Audiences in Jwt are not allowed 403 495 | >>> 2 200 496 | ``` 497 | 498 | As Carol 499 | 500 | ```bash 501 | export USER=carol 502 | 503 | curl -s \ 504 | --cacert certs/CA_crt.pem --resolve svc1.example.com:443:$GATEWAY_IP \ 505 | -H "Authorization: Bearer $USER" \ 506 | -w " %{http_code}\n" \ 507 | https://svc1.example.com/version 508 | 509 | 510 | curl -s \ 511 | --cacert certs/CA_crt.pem --resolve svc2.example.com:443:$GATEWAY_IP \ 512 | -H "Authorization: Bearer $USER" \ 513 | -w " %{http_code}\n" \ 514 | https://svc2.example.com/version 515 | ``` 516 | 517 | ``` 518 | >>> 1 200 519 | >>> Audiences in Jwt are not allowed 403 520 | ``` 521 | 522 | ![images/authz_ns_flow_fe.png](images/authz_ns_flow_fe.png) 523 | 524 | >> note, it seems the traffic from the gateway to the authorization server isn't correctly detected to be associated with the ingress-gateway (maybe a bug or some label is missing) 525 | 526 | ### Access Backend 527 | 528 | The configuration also defines Authorization policies on the `svc1`-> `be` traffic using **BOTH** `PEER` and `ORIGIN`. 529 | 530 | - `PEER`: 531 | 532 | This is done using normal RBAC service identities: 533 | 534 | ```yaml 535 | apiVersion: security.istio.io/v1 536 | kind: AuthorizationPolicy 537 | metadata: 538 | name: svc1-be-v1-authz-policy 539 | namespace: default 540 | spec: 541 | action: ALLOW 542 | selector: 543 | matchLabels: 544 | app: be 545 | version: v1 546 | rules: 547 | - from: 548 | - source: 549 | principals: ["cluster.local/ns/default/sa/svc1-sa"] 550 | to: 551 | - operation: 552 | methods: ["GET"] 553 | ``` 554 | 555 | #### Backend PEER and ORIGIN 556 | 557 | Note the `from->source->principals` denotes the service account `svc1` runs as. 558 | 559 | - `ORIGIN` 560 | 561 | THis step is pretty unusual and requires some changes to application code to _forward_ its inbound authentication token. 562 | 563 | Recall the inbound JWT token to `svc1` for `alice` includes two audiences: 564 | 565 | ```golang 566 | aud = []string{"http://svc1.default.svc.cluster.local:8080/", "http://be.default.svc.cluster.local:8080/"} 567 | ``` 568 | 569 | This means we can use the same JWT token on the backend service if we setup an authentication and authz rule: 570 | 571 | ```yaml 572 | ## svc --> be-v1 573 | apiVersion: security.istio.io/v1 574 | kind: RequestAuthentication 575 | metadata: 576 | name: svc-be-v1-request-authn-policy 577 | namespace: default 578 | spec: 579 | selector: 580 | matchLabels: 581 | app: be 582 | version: v1 583 | jwtRules: 584 | - issuer: "$SERVICE_ACCOUNT_EMAIL" 585 | audiences: 586 | - "http://be.default.svc.cluster.local:8080/" 587 | jwksUri: "https://raw.githubusercontent.com/istio/istio/release-$ISTIO_VERSION_MINOR/security/tools/jwt/samples/jwks.json" 588 | outputPayloadToHeader: x-jwt-payload 589 | --- 590 | apiVersion: security.istio.io/v1 591 | kind: AuthorizationPolicy 592 | metadata: 593 | name: svc1-be-v1-authz-policy 594 | namespace: default 595 | spec: 596 | action: ALLOW 597 | selector: 598 | matchLabels: 599 | app: be 600 | version: v1 601 | rules: 602 | - from: 603 | - source: 604 | principals: ["cluster.local/ns/default/sa/svc1-sa"] 605 | to: 606 | - operation: 607 | methods: ["GET"] 608 | when: 609 | - key: request.auth.claims[iss] 610 | values: ["$SERVICE_ACCOUNT_EMAIL"] 611 | - key: request.auth.claims[aud] 612 | values: ["http://be.default.svc.cluster.local:8080/"] 613 | ``` 614 | 615 | The `RequestAuthentication` accepts a JWT token signed by the external authz server and must also include the audience of the backend (which alice's token has). The second authorization (redundantly) rule further parses out the token and looks for the same. 616 | 617 | Istio does not automatically forward the inbound token (though it maybe possible with `SIDECAR_INBOUND`->`SIDECAR_OUTBOUND` forwarding somehow...)...to achieve this requres some application code changes. The folloing snippet is the code within `frontend/app.js` which take the token and uses it on the backend api call. 618 | 619 | >> `4/27/20`: update on the comment "(though it maybe possble with `SIDECAR_INBOUND`->`SIDECAR_OUTBOUND` forwarding somehow...)" Its not; envoy doens't carry state from the filters forward like this. You need to either accept and forward the header in code as shown below: 620 | 621 | ```javascript 622 | var resp_promises = [] 623 | var urls = [ 624 | 'http://' + host + ':' + port + '/backend', 625 | 'http://' + host + ':' + port + '/headerz', 626 | ] 627 | 628 | out_headers = {}; 629 | if (FORWARD_AUTH_HEADER == 'true') { 630 | var auth_header = request.headers['authorization']; 631 | logger.info("Got Authorization Header: [" + auth_header + "]"); 632 | out_headers = { 633 | 'authorization': auth_header, 634 | }; 635 | } 636 | 637 | urls.forEach(element => { 638 | resp_promises.push( getURL(element,out_headers) ) 639 | }); 640 | ``` 641 | 642 | Or configure istio to make an `OUTBOUND` ext_authz filter call. The external authz filter will return a new Authorization server token intended for ust `svcb`. 643 | 644 | You will also need to set [allowed_client_headers](https://www.envoyproxy.io/docs/envoy/latest/api-v2/config/filter/http/ext_authz/v2/ext_authz.proto#envoy-api-msg-config-filter-http-ext-authz-v2-authorizationresponse) so that the auth token returned by ext-authz server is sent to the upstream (in this case, upstream is `svcb`) 645 | 646 | I think the config would be _something_ like this: 647 | 648 | ```yaml 649 | apiVersion: networking.istio.io/v1 650 | kind: EnvoyFilter 651 | metadata: 652 | name: ext-authz-service 653 | namespace: default 654 | spec: 655 | workloadLabels: 656 | app: svc1 657 | filters: 658 | - listenerMatch: 659 | listenerType: OUTBOUND # <<<< OUTBOUND svc1->* 660 | listenerProtocol: HTTP 661 | insertPosition: 662 | index: FIRST 663 | filterName: envoy.ext_authz 664 | filterType: HTTP 665 | filterConfig: 666 | grpc_service: 667 | envoy_grpc: 668 | cluster_name: patched.authz.authz-ns.svc.cluster.local 669 | authorization_response: 670 | allowed_client_headers: 671 | patterns: 672 | - exact: "Authorization" 673 | ``` 674 | (ofcourse changes are needed to ext-authz server as provided in this repo..) 675 | 676 | >> Note: i added both ORIGIN and PEER just to demonstrate this...Until its easier forward the token by envoy/istio, i woudn't recommend doing this bit.. 677 | 678 | 679 | Anwyay, to test all this out 680 | 681 | ```bash 682 | export USER=alice 683 | 684 | curl -s \ 685 | --cacert certs/CA_crt.pem --resolve svc1.example.com:443:$GATEWAY_IP \ 686 | -H "Authorization: Bearer $USER" \ 687 | -w " %{http_code}\n" \ 688 | https://svc1.example.com/backend | jq '.' 689 | 690 | 691 | export USER=bob 692 | 693 | curl -s \ 694 | --cacert certs/CA_crt.pem --resolve svc2.example.com:443:$GATEWAY_IP \ 695 | -H "Authorization: Bearer $USER" \ 696 | -w " %{http_code}\n" \ 697 | https://svc2.example.com/backend | jq '.' 698 | 699 | export USER=carol 700 | 701 | curl -s \ 702 | --cacert certs/CA_crt.pem --resolve svc1.example.com:443:$GATEWAY_IP \ 703 | -H "Authorization: Bearer $USER" \ 704 | -w " %{http_code}\n" \ 705 | https://svc1.example.com/backend | jq '.' 706 | ``` 707 | 708 | Sample output 709 | 710 | -Alice 711 | 712 | Alice's TOKEN issued by the authorization server includes two audiences: 713 | 714 | ```golang 715 | aud = []string{"http://svc1.default.svc.cluster.local:8080/", "http://be.default.svc.cluster.local:8080/"} 716 | ``` 717 | 718 | Which is allowed by backend services `RequestAuthentication` policy. 719 | 720 | ```bash 721 | export USER=alice 722 | curl -s \ 723 | --cacert certs/CA_crt.pem --resolve svc1.example.com:443:$GATEWAY_IP \ 724 | -H "Authorization: Bearer $USER" \ 725 | -w " %{http_code}\n" \ 726 | https://svc1.example.com/backend | jq '.' 727 | 728 | [ 729 | { 730 | "url": "http://be.default.svc.cluster.local:8080/backend", 731 | "body": "pod: [be-v2-64d9cf5fb4-mpsq5] node: [gke-istio-1-default-pool-b516bc56-xz2c]", 732 | "statusCode": 200 733 | }, 734 | { 735 | "url": "http://be.default.svc.cluster.local:8080/headerz", 736 | "body": "{\"host\":\"be.default.svc.cluster.local:8080\",\"x-forwarded-proto\":\"http\",\"x-request-id\":\"bb31942c-f04e-9b12-ba69-d68603a520af\",\"content-length\":\"0\",\"x-forwarded-client-cert\":\"By=spiffe://cluster.local/ns/default/sa/be-sa;Hash=2e0f9ca7bea6ac081f4c256de79ffdb4db2e55968b0ded2526e95cb89f4c36ac;Subject=\\\"\\\";URI=spiffe://cluster.local/ns/default/sa/svc1-sa\",\"x-b3-traceid\":\"cda6d87c8d342998ee1f797471592dff\",\"x-b3-spanid\":\"6dc54e848db21050\",\"x-b3-parentspanid\":\"ee1f797471592dff\",\"x-b3-sampled\":\"1\"}", 737 | "statusCode": 200 738 | } 739 | ] 740 | ``` 741 | 742 | - Bob 743 | 744 | Bob's token does not include the backend service 745 | 746 | ```golang 747 | aud = []string{"http://svc2.default.svc.cluster.local:8080/"} 748 | ``` 749 | 750 | Which means the `RequestAuthentication` will fail. Bob is only allowed to invoke `svc2` anyway 751 | 752 | 753 | ```bash 754 | export USER=bob 755 | curl -s \ 756 | --cacert certs/CA_crt.pem --resolve svc2.example.com:443:$GATEWAY_IP \ 757 | -H "Authorization: Bearer $USER" \ 758 | -w " %{http_code}\n" \ 759 | https://svc2.example.com/backend | jq '.' 760 | 761 | [ 762 | { 763 | "url": "http://be.default.svc.cluster.local:8080/backend", 764 | "body": "Audiences in Jwt are not allowed", 765 | "statusCode": 403 766 | }, 767 | { 768 | "url": "http://be.default.svc.cluster.local:8080/headerz", 769 | "body": "Audiences in Jwt are not allowed", 770 | "statusCode": 403 771 | } 772 | ] 773 | ``` 774 | 775 | - Carol 776 | 777 | Carol's token is allowed to invoke `svc1` but does not include the issuer to pass the `RequestAuthentication` policy 778 | 779 | ```golang 780 | aud = []string{"http://svc1.default.svc.cluster.local:8080/"} 781 | ``` 782 | 783 | ```bash 784 | export USER=carol 785 | curl -s \ 786 | --cacert certs/CA_crt.pem --resolve svc1.example.com:443:$GATEWAY_IP \ 787 | -H "Authorization: Bearer $USER" \ 788 | -w " %{http_code}\n" \ 789 | https://svc1.example.com/backend | jq '.' 790 | 791 | [ 792 | { 793 | "url": "http://be.default.svc.cluster.local:8080/backend", 794 | "body": "Audiences in Jwt are not allowed", 795 | "statusCode": 403 796 | }, 797 | { 798 | "url": "http://be.default.svc.cluster.local:8080/headerz", 799 | "body": "Audiences in Jwt are not allowed", 800 | "statusCode": 403 801 | } 802 | ] 803 | ``` 804 | 805 | ![images/authz_ns_flow_fe.png](images/authz_ns_flow_fe.png) 806 | 807 | 808 | If you would rather run these tests in a loop 809 | ```bash 810 | for i in {1..1000}; do curl -s \ 811 | --cacert certs/CA_crt.pem --resolve svc1.example.com:443:$GATEWAY_IP \ 812 | -H "Authorization: Bearer $USER" \ 813 | -w " %{http_code}\n" \ 814 | https://svc1.example.com/version; sleep 1; done 815 | ``` 816 | 817 | --- 818 | 819 | At this point, the system is setup to to always use mTLS, `ORIGIN` and `PEER` authentication plus `RBAC`. If you want to verify any component of `PEER`, change the policy and change the service account that is the target service authorization policy accepts and reapply the config. 820 | 821 | Change either the settings `RequestAuthentication` _or_ `AuthorizationPolicy` depending on which layer you are testing 822 | 823 | (remember to replace the value for `$ISTIO_VERSION_MINOR` ) 824 | 825 | ```yaml 826 | ## svc --> be-v1 827 | apiVersion: security.istio.io/v1 828 | kind: RequestAuthentication 829 | metadata: 830 | name: svc-be-v1-request-authn-policy 831 | namespace: default 832 | spec: 833 | selector: 834 | matchLabels: 835 | app: be 836 | version: v1 837 | jwtRules: 838 | - issuer: "$SERVICE_ACCOUNT_EMAIL" 839 | audiences: 840 | - "http://be.default.svc.cluster.local:8080/" ## or CHANGE ORIGIN <<<< "Audiences in Jwt are not allowed" 841 | jwksUri: "https://raw.githubusercontent.com/istio/istio/release-$ISTIO_VERSION_MINOR/security/tools/jwt/samples/jwks.json" 842 | # forwardOriginalToken: true 843 | outputPayloadToHeader: x-jwt-payload 844 | --- 845 | ## svc --> be-v2 846 | apiVersion: security.istio.io/v1 847 | kind: RequestAuthentication 848 | metadata: 849 | name: svc-be-v2-request-authn-policy 850 | namespace: default 851 | spec: 852 | selector: 853 | matchLabels: 854 | app: be 855 | version: v2 856 | jwtRules: 857 | - issuer: "$SERVICE_ACCOUNT_EMAIL" 858 | audiences: 859 | - "http://be.default.svc.cluster.local:8080/" ## or CHANGE ORIGIN <<<< "Audiences in Jwt are not allowed" 860 | jwksUri: "https://raw.githubusercontent.com/istio/istio/release-$ISTIO_VERSION_MINOR/security/tools/jwt/samples/jwks.json" 861 | # forwardOriginalToken: true 862 | outputPayloadToHeader: x-jwt-payload 863 | --- 864 | apiVersion: security.istio.io/v1beta1 865 | kind: AuthorizationPolicy 866 | metadata: 867 | name: svc1-be-v1-authz-policy 868 | namespace: default 869 | spec: 870 | action: ALLOW 871 | selector: 872 | matchLabels: 873 | app: be 874 | version: v1 875 | rules: 876 | - from: 877 | - source: 878 | principals: ["cluster.local/ns/default/sa/svc1-sa"] # CHANGE PEER <<<< "RBAC: access denied" 879 | to: 880 | - operation: 881 | methods: ["GET"] 882 | when: 883 | - key: request.auth.claims[iss] 884 | values: ["$SERVICE_ACCOUNT_EMAIL"] ## or CHANGE ORIGIN at Authz <<<< "RBAC: access denied" 885 | - key: request.auth.claims[aud] 886 | values: ["http://be.default.svc.cluster.local:8080/"] 887 | --- 888 | apiVersion: security.istio.io/v1 889 | kind: AuthorizationPolicy 890 | metadata: 891 | name: svc1-be-v2-authz-policy 892 | namespace: default 893 | spec: 894 | action: ALLOW 895 | selector: 896 | matchLabels: 897 | app: be 898 | version: v2 899 | rules: 900 | - from: 901 | - source: 902 | principals: ["cluster.local/ns/default/sa/svc1-sa"] # CHANGE PEER <<<< "RBAC: access denied" 903 | to: 904 | - operation: 905 | methods: ["GET"] 906 | when: 907 | - key: request.auth.claims[iss] 908 | values: ["$SERVICE_ACCOUNT_EMAIL"] ## or CHANGE ORIGIN at Authz <<<< "RBAC: access denied" 909 | - key: request.auth.claims[aud] 910 | values: ["http://be.default.svc.cluster.local:8080/"] 911 | ``` 912 | then reapply the config and access the backend as `alice` 913 | 914 | ```bash 915 | export USER=alice 916 | curl -s \ 917 | --cacert certs/CA_crt.pem --resolve svc1.example.com:443:$GATEWAY_IP \ 918 | -H "Authorization: Bearer $USER" \ 919 | -w " %{http_code}\n" \ 920 | https://svc1.example.com/backend | jq '.' 921 | 922 | 923 | [ 924 | { 925 | "url": "http://be.default.svc.cluster.local:8080/backend", 926 | "body": "RBAC: access denied", 927 | "statusCode": 403 928 | }, 929 | { 930 | "url": "http://be.default.svc.cluster.local:8080/headerz", 931 | "body": "RBAC: access denied", 932 | "statusCode": 403 933 | } 934 | ] 935 | ``` 936 | 937 | Finally, the external server is attached to the ingress gateway but you could also attach it to a sidecar for an endpoint. In this mode, the authorization decision is done not at the ingress gateway but locally on a service's sidecar. To use that mode, define the `EnvoyFilter` workloadLabel and listenerType. eg: 938 | 939 | ```yaml 940 | apiVersion: networking.istio.io/v1 941 | kind: EnvoyFilter 942 | metadata: 943 | name: svc1-authz-filter 944 | namespace: default 945 | spec: 946 | workloadSelector: 947 | labels: 948 | app: svc1 949 | configPatches: 950 | - applyTo: HTTP_FILTER 951 | match: 952 | context: SIDECAR_INBOUND 953 | listener: 954 | filterChain: 955 | filter: 956 | name: "envoy.filters.network.http_connection_manager" 957 | subFilter: 958 | name: "envoy.filters.http.router" 959 | patch: 960 | operation: INSERT_FIRST 961 | value: 962 | name: "envoy.filters.http.ext_authz" 963 | config: 964 | grpc_service: 965 | envoy_grpc: 966 | cluster_name: patched.authz.authz-ns.svc.cluster.local 967 | ``` 968 | 969 | If you do this, you will have to setup PEER policies that allow the service to connect and use the authorization server. 970 | 971 | --- 972 | 973 | ### Debugging 974 | 975 | You can debug issues using these resources 976 | 977 | - [Debugging Envoy and Istio](https://istio.io/docs/ops/diagnostic-tools/proxy-cmd/) 978 | - [Security Problems](https://istio.io/docs/ops/common-problems/security-issues/) 979 | 980 | To set the log level higher and inspect a pod's logs: 981 | 982 | ```bash 983 | istioctl manifest apply --set values.global.proxy.accessLogFile="/dev/stdout" 984 | ``` 985 | 986 | - Ingress pod 987 | 988 | ```bash 989 | INGRESS_POD_NAME=$(kubectl get po -n istio-system | grep ingressgateway\- | awk '{print$1}'); echo ${INGRESS_POD_NAME}; 990 | 991 | kubectl exec -ti $INGRESS_POD_NAME -n istio-syste -- /bin/bash 992 | istioctl proxy-config log $INGRESS_POD_NAME --level debug 993 | kubectl logs -f --tail=0 $INGRESS_POD_NAME -n istio-system 994 | istioctl dashboard envoy $INGRESS_POD_NAME.istio-system 995 | istioctl experimental authz check $INGRESS_POD_NAME.istio-system 996 | ``` 997 | 998 | ```bash 999 | $ istioctl experimental authz check $INGRESS_POD_NAME.istio-system 1000 | Checked 2/2 listeners with node IP 10.48.2.5. 1001 | LISTENER[FilterChain] CERTIFICATE mTLS (MODE) JWT (ISSUERS) AuthZ (RULES) 1002 | 0.0.0.0_80 none no (none) no (none) no (none) 1003 | 0.0.0.0_443 /etc/istio/ingressgateway-certs/tls.crt no (none) no (none) no (none) 1004 | 1005 | $ istioctl authn tls-check $INGRESS_POD_NAME.istio-system authz.authz-ns.svc.cluster.local 1006 | HOST:PORT STATUS SERVER CLIENT AUTHN POLICY DESTINATION RULE 1007 | authz.authz-ns.svc.cluster.local:50051 AUTO STRICT - /default - 1008 | ``` 1009 | 1010 | - Authz pod 1011 | 1012 | ```bash 1013 | AUTHZ_POD_NAME=$(kubectl get po -n authz-ns | grep authz\- | awk '{print$1}'); echo ${AUTHZ_POD_NAME}; 1014 | istioctl proxy-config log $AUTHZ_POD_NAME -n authz-ns --level debug 1015 | kubectl logs -f --tail=0 $AUTHZ_POD_NAME -c authz-container -n authz-ns 1016 | istioctl dashboard envoy $AUTHZ_POD_NAME.authz-ns 1017 | istioctl experimental authz check $AUTHZ_POD_NAME.authz-ns 1018 | ``` 1019 | 1020 | - SVC1 pod 1021 | 1022 | ```bash 1023 | SVC1_POD_NAME=$(kubectl get po -n default | grep svc1\- | awk '{print$1}'); echo ${SVC1_POD_NAME}; 1024 | 1025 | $ istioctl authn tls-check $SVC1_POD_NAME.default be.default.svc.cluster.local 1026 | HOST:PORT STATUS SERVER CLIENT AUTHN POLICY DESTINATION RULE 1027 | be.default.svc.cluster.local:8080 OK STRICT ISTIO_MUTUAL /default default/be-destination 1028 | ``` 1029 | 1030 | - SVC2 pod 1031 | 1032 | ```bash 1033 | SVC2_POD_NAME=$(kubectl get po -n default | grep svc2\- | awk '{print$1}'); echo ${SVC2_POD_NAME}; 1034 | 1035 | $ istioctl authn tls-check $SVC2_POD_NAME.default be.default.svc.cluster.local 1036 | HOST:PORT STATUS SERVER CLIENT AUTHN POLICY DESTINATION RULE 1037 | be.default.svc.cluster.local:8080 OK STRICT ISTIO_MUTUAL /default default/be-destination 1038 | ``` 1039 | 1040 | ### Using Google OIDC ORIGIN authentication at Ingress 1041 | 1042 | If you want to use OIDC JWT authentication at the ingress gateway and then have that token forwarded to the 1043 | external authz service, apply the `RequestAuthentication` policies on the ingress gateway as shown in the equivalent 1044 | Envoy configuration [here](https://github.com/salrashid123/envoy_iap/blob/master/envoy_google.yaml#L32). 1045 | You can generate an `id-token` using the script found under `jwt_client/` folder. 1046 | 1047 | ### Debugging 1048 | 1049 | ```bash 1050 | kubectl get pods -n istio-system -o name -l istio=ingressgateway | sed 's|pod/||' | while read -r pod; do istioctl proxy-config log "$pod" -n istio-system --level rbac:debug; done 1051 | ``` 1052 | -------------------------------------------------------------------------------- /app-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: svc1 5 | labels: 6 | app: svc1 7 | spec: 8 | ports: 9 | - port: 8080 10 | name: http 11 | selector: 12 | app: svc1 13 | --- 14 | apiVersion: v1 15 | kind: Service 16 | metadata: 17 | name: svc2 18 | labels: 19 | app: svc2 20 | spec: 21 | ports: 22 | - port: 8080 23 | name: http 24 | selector: 25 | app: svc2 26 | --- 27 | apiVersion: v1 28 | kind: Service 29 | metadata: 30 | name: be 31 | labels: 32 | app: be 33 | spec: 34 | ports: 35 | - name: http 36 | port: 8080 37 | selector: 38 | app: be 39 | --- 40 | apiVersion: v1 41 | kind: ServiceAccount 42 | metadata: 43 | name: svc1-sa 44 | --- 45 | apiVersion: v1 46 | kind: ServiceAccount 47 | metadata: 48 | name: svc2-sa 49 | --- 50 | apiVersion: v1 51 | kind: ServiceAccount 52 | metadata: 53 | name: be-sa 54 | --- 55 | apiVersion: v1 56 | kind: ConfigMap 57 | metadata: 58 | name: svc-config 59 | namespace: default 60 | data: 61 | forward_auth_header: "true" 62 | backend_namespace: default 63 | --- 64 | apiVersion: apps/v1 65 | kind: Deployment 66 | metadata: 67 | name: svc1 68 | spec: 69 | selector: 70 | matchLabels: 71 | app: svc1 72 | replicas: 1 73 | template: 74 | metadata: 75 | labels: 76 | app: svc1 77 | spec: 78 | serviceAccountName: svc1-sa 79 | containers: 80 | - name: myapp-container 81 | image: salrashid123/svc:1 82 | imagePullPolicy: IfNotPresent 83 | ports: 84 | - containerPort: 8080 85 | env: 86 | - name: FORWARD_AUTH_HEADER 87 | valueFrom: 88 | configMapKeyRef: 89 | name: svc-config 90 | key: forward_auth_header 91 | - name: BACKEND_NAMESPACE 92 | valueFrom: 93 | configMapKeyRef: 94 | name: svc-config 95 | key: backend_namespace 96 | --- 97 | apiVersion: apps/v1 98 | kind: Deployment 99 | metadata: 100 | name: svc2 101 | spec: 102 | selector: 103 | matchLabels: 104 | app: svc2 105 | replicas: 1 106 | template: 107 | metadata: 108 | labels: 109 | app: svc2 110 | spec: 111 | serviceAccountName: svc2-sa 112 | containers: 113 | - name: myapp-container 114 | image: salrashid123/svc:2 115 | imagePullPolicy: IfNotPresent 116 | ports: 117 | - containerPort: 8080 118 | env: 119 | - name: FORWARD_AUTH_HEADER 120 | valueFrom: 121 | configMapKeyRef: 122 | name: svc-config 123 | key: forward_auth_header 124 | - name: BACKEND_NAMESPACE 125 | valueFrom: 126 | configMapKeyRef: 127 | name: svc-config 128 | key: backend_namespace 129 | --- 130 | apiVersion: apps/v1 131 | kind: Deployment 132 | metadata: 133 | name: be-v1 134 | labels: 135 | type: be 136 | version: v1 137 | spec: 138 | selector: 139 | matchLabels: 140 | app: be 141 | version: v1 142 | replicas: 1 143 | template: 144 | metadata: 145 | labels: 146 | app: be 147 | version: v1 148 | spec: 149 | serviceAccountName: be-sa 150 | containers: 151 | - name: be-container 152 | image: salrashid123/besvc:1 153 | imagePullPolicy: IfNotPresent 154 | ports: 155 | - containerPort: 8080 156 | env: 157 | - name: MY_NODE_NAME 158 | valueFrom: 159 | fieldRef: 160 | fieldPath: spec.nodeName 161 | - name: MY_POD_NAME 162 | valueFrom: 163 | fieldRef: 164 | fieldPath: metadata.name 165 | --- 166 | apiVersion: apps/v1 167 | kind: Deployment 168 | metadata: 169 | name: be-v2 170 | labels: 171 | type: be 172 | version: v2 173 | spec: 174 | selector: 175 | matchLabels: 176 | app: be 177 | version: v2 178 | replicas: 1 179 | template: 180 | metadata: 181 | labels: 182 | app: be 183 | version: v2 184 | spec: 185 | serviceAccountName: be-sa 186 | containers: 187 | - name: be-container 188 | image: salrashid123/besvc:2 189 | imagePullPolicy: IfNotPresent 190 | ports: 191 | - containerPort: 8080 192 | env: 193 | - name: MY_NODE_NAME 194 | valueFrom: 195 | fieldRef: 196 | fieldPath: spec.nodeName 197 | - name: MY_POD_NAME 198 | valueFrom: 199 | fieldRef: 200 | fieldPath: metadata.name 201 | -------------------------------------------------------------------------------- /authz_server/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.22 as build 2 | 3 | ENV GO111MODULE=on 4 | 5 | WORKDIR /app 6 | 7 | COPY go.mod . 8 | COPY go.sum . 9 | 10 | RUN go mod download 11 | 12 | COPY . . 13 | 14 | RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build grpc_server.go 15 | 16 | FROM gcr.io/distroless/base 17 | COPY --from=build /app/grpc_server / 18 | 19 | EXPOSE 50051 20 | 21 | ENTRYPOINT ["/grpc_server"] 22 | -------------------------------------------------------------------------------- /authz_server/go.mod: -------------------------------------------------------------------------------- 1 | module main 2 | 3 | go 1.22.7 4 | 5 | toolchain go1.22.9 6 | 7 | require ( 8 | github.com/envoyproxy/go-control-plane v0.13.1 9 | github.com/gogo/googleapis v1.4.1 10 | github.com/golang-jwt/jwt/v5 v5.2.1 11 | github.com/golang/glog v1.2.3 12 | golang.org/x/net v0.31.0 13 | google.golang.org/genproto/googleapis/rpc v0.0.0-20241104194629-dd2ea8efbc28 14 | google.golang.org/grpc v1.68.0 15 | ) 16 | 17 | require ( 18 | github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 // indirect 19 | github.com/envoyproxy/protoc-gen-validate v1.1.0 // indirect 20 | github.com/gogo/protobuf v1.3.2 // indirect 21 | github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect 22 | golang.org/x/sys v0.27.0 // indirect 23 | golang.org/x/text v0.20.0 // indirect 24 | google.golang.org/protobuf v1.35.1 // indirect 25 | ) 26 | -------------------------------------------------------------------------------- /authz_server/go.sum: -------------------------------------------------------------------------------- 1 | github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 h1:QVw89YDxXxEe+l8gU8ETbOasdwEV+avkR75ZzsVV9WI= 2 | github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= 3 | github.com/envoyproxy/go-control-plane v0.13.1 h1:vPfJZCkob6yTMEgS+0TwfTUfbHjfy/6vOJ8hUWX/uXE= 4 | github.com/envoyproxy/go-control-plane v0.13.1/go.mod h1:X45hY0mufo6Fd0KW3rqsGvQMw58jvjymeCzBU3mWyHw= 5 | github.com/envoyproxy/protoc-gen-validate v1.1.0 h1:tntQDh69XqOCOZsDz0lVJQez/2L6Uu2PdjCQwWCJ3bM= 6 | github.com/envoyproxy/protoc-gen-validate v1.1.0/go.mod h1:sXRDRVmzEbkM7CVcM06s9shE/m23dg3wzjl0UWqJ2q4= 7 | github.com/gogo/googleapis v1.4.1 h1:1Yx4Myt7BxzvUr5ldGSbwYiZG6t9wGBZ+8/fX3Wvtq0= 8 | github.com/gogo/googleapis v1.4.1/go.mod h1:2lpHqI5OcWCtVElxXnPt+s8oJvMpySlOyM6xDCrzib4= 9 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 10 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 11 | github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= 12 | github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= 13 | github.com/golang/glog v1.2.3 h1:oDTdz9f5VGVVNGu/Q7UXKWYsD0873HXLHdJUNBsSEKM= 14 | github.com/golang/glog v1.2.3/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= 15 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 16 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 17 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 18 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 19 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 20 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 21 | github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= 22 | github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= 23 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 24 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 25 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 26 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 27 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 28 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 29 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 30 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 31 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 32 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 33 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 34 | golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= 35 | golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= 36 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 37 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 38 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 39 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 40 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 41 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 42 | golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= 43 | golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 44 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 45 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 46 | golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= 47 | golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= 48 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 49 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 50 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 51 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 52 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 53 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 54 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 55 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 56 | google.golang.org/genproto/googleapis/rpc v0.0.0-20241104194629-dd2ea8efbc28 h1:XVhgTWWV3kGQlwJHR3upFWZeTsei6Oks1apkZSeonIE= 57 | google.golang.org/genproto/googleapis/rpc v0.0.0-20241104194629-dd2ea8efbc28/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= 58 | google.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0= 59 | google.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA= 60 | google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= 61 | google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 62 | -------------------------------------------------------------------------------- /authz_server/grpc_server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | /* 4 | This is close variation of jbarratt@ repo here 5 | https://github.com/jbarratt/envoy_ratelimit_example/blob/master/extauth/main.go 6 | 7 | */ 8 | import ( 9 | "flag" 10 | "fmt" 11 | "log" 12 | "net" 13 | "os" 14 | "strings" 15 | "time" 16 | 17 | "golang.org/x/net/context" 18 | "google.golang.org/grpc" 19 | 20 | "google.golang.org/grpc/codes" 21 | "google.golang.org/grpc/health" 22 | healthpb "google.golang.org/grpc/health/grpc_health_v1" 23 | "google.golang.org/grpc/status" 24 | 25 | rpcstatus "google.golang.org/genproto/googleapis/rpc/status" 26 | 27 | core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" 28 | 29 | auth "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3" 30 | 31 | envoy_type "github.com/envoyproxy/go-control-plane/envoy/type/v3" 32 | 33 | "github.com/gogo/googleapis/google/rpc" 34 | 35 | "crypto/rsa" 36 | "crypto/x509" 37 | "encoding/pem" 38 | "io/ioutil" 39 | 40 | jwt "github.com/golang-jwt/jwt/v5" 41 | "github.com/golang/glog" 42 | ) 43 | 44 | var ( 45 | grpcport = flag.String("grpcport", ":50051", "grpcport") 46 | conn *grpc.ClientConn 47 | hs *health.Server 48 | AUTHZ_ALLOWED_USERS = os.Getenv("AUTHZ_ALLOWED_USERS") 49 | 50 | AUTHZ_ISSUER = os.Getenv("AUTHZ_ISSUER") 51 | AUTHZ_SERVER_KEY_ID = os.Getenv("AUTHZ_SERVER_KEY_ID") 52 | privateKey *rsa.PrivateKey 53 | ) 54 | 55 | func stringInSlice(a string, list []string) bool { 56 | for _, b := range list { 57 | if b == a { 58 | return true 59 | } 60 | } 61 | return false 62 | } 63 | 64 | const ( 65 | address string = ":50051" 66 | keyfile string = "/data/certs/key.pem" 67 | ) 68 | 69 | type MyCustomClaims struct { 70 | Uid string `json:"uid"` 71 | Aud []string `json:"aud"` // https://github.com/dgrijalva/jwt-go/pull/308 72 | jwt.RegisteredClaims 73 | } 74 | 75 | type healthServer struct{} 76 | 77 | func (s *healthServer) Check(ctx context.Context, in *healthpb.HealthCheckRequest) (*healthpb.HealthCheckResponse, error) { 78 | log.Printf("Handling grpc Check request") 79 | // yeah, right, open 24x7, like 7-11 80 | return &healthpb.HealthCheckResponse{Status: healthpb.HealthCheckResponse_SERVING}, nil 81 | } 82 | 83 | func (s *healthServer) Watch(in *healthpb.HealthCheckRequest, srv healthpb.Health_WatchServer) error { 84 | return status.Error(codes.Unimplemented, "Watch is not implemented") 85 | } 86 | 87 | type AuthorizationServer struct{} 88 | 89 | func returnUnAuthenticated(message string) *auth.CheckResponse { 90 | return &auth.CheckResponse{ 91 | Status: &rpcstatus.Status{ 92 | Code: int32(rpc.UNAUTHENTICATED), 93 | }, 94 | HttpResponse: &auth.CheckResponse_DeniedResponse{ 95 | DeniedResponse: &auth.DeniedHttpResponse{ 96 | Status: &envoy_type.HttpStatus{ 97 | Code: envoy_type.StatusCode_Forbidden, 98 | }, 99 | Body: message, 100 | }, 101 | }, 102 | } 103 | } 104 | 105 | func returnPermissionDenied(message string) *auth.CheckResponse { 106 | return &auth.CheckResponse{ 107 | Status: &rpcstatus.Status{ 108 | Code: int32(rpc.PERMISSION_DENIED), 109 | }, 110 | HttpResponse: &auth.CheckResponse_DeniedResponse{ 111 | DeniedResponse: &auth.DeniedHttpResponse{ 112 | Status: &envoy_type.HttpStatus{ 113 | Code: envoy_type.StatusCode_Unauthorized, 114 | }, 115 | Body: message, 116 | }, 117 | }, 118 | } 119 | } 120 | 121 | func (a *AuthorizationServer) Check(ctx context.Context, req *auth.CheckRequest) (*auth.CheckResponse, error) { 122 | log.Println(">>> Authorization called check()") 123 | 124 | authHeader, ok := req.Attributes.Request.Http.Headers["authorization"] 125 | if !ok { 126 | return returnUnAuthenticated("Unable to find Authorization Header"), nil 127 | } 128 | var splitToken []string 129 | log.Printf("Authorization Header %s", authHeader) 130 | 131 | if ok { 132 | splitToken = strings.Split(authHeader, "Bearer ") 133 | } else { 134 | log.Println("Unable to parse Header") 135 | return returnUnAuthenticated("Unable to parse Authorization Header"), nil 136 | } 137 | if len(splitToken) == 2 { 138 | token := splitToken[1] 139 | 140 | if stringInSlice(token, strings.Split(AUTHZ_ALLOWED_USERS, ",")) { 141 | 142 | var aud []string 143 | if token == "alice" { 144 | aud = []string{"http://svc1.default.svc.cluster.local:8080/", "http://be.default.svc.cluster.local:8080/"} 145 | } else if token == "bob" { 146 | aud = []string{"http://svc2.default.svc.cluster.local:8080/"} 147 | } else if token == "carol" { 148 | aud = []string{"http://svc1.default.svc.cluster.local:8080/"} 149 | } else { 150 | aud = []string{} 151 | } 152 | claims := MyCustomClaims{ 153 | token, 154 | aud, 155 | jwt.RegisteredClaims{ 156 | Issuer: AUTHZ_ISSUER, 157 | Subject: AUTHZ_ISSUER, 158 | //Audience: aud, 159 | IssuedAt: &jwt.NumericDate{time.Now()}, 160 | ExpiresAt: &jwt.NumericDate{time.Now().Add(time.Minute * 1)}, 161 | }, 162 | } 163 | 164 | log.Printf("Using Claim %v", claims) 165 | token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) 166 | token.Header["kid"] = AUTHZ_SERVER_KEY_ID 167 | ss, err := token.SignedString(privateKey) 168 | if err != nil { 169 | return returnUnAuthenticated("Unable to generate JWT"), nil 170 | } 171 | 172 | log.Printf("Issuing outbound Header %s", ss) 173 | 174 | return &auth.CheckResponse{ 175 | Status: &rpcstatus.Status{ 176 | Code: int32(rpc.OK), 177 | }, 178 | HttpResponse: &auth.CheckResponse_OkResponse{ 179 | OkResponse: &auth.OkHttpResponse{ 180 | Headers: []*core.HeaderValueOption{ 181 | { 182 | Header: &core.HeaderValue{ 183 | Key: "Authorization", 184 | Value: "Bearer " + ss, 185 | }, 186 | }, 187 | }, 188 | }, 189 | }, 190 | }, nil 191 | } else { 192 | log.Printf("Authorization Header missing") 193 | return returnPermissionDenied("Permission Denied"), nil 194 | 195 | } 196 | 197 | } 198 | return returnUnAuthenticated("Authorization header not provided"), nil 199 | } 200 | 201 | func main() { 202 | 203 | flag.Parse() 204 | 205 | if *grpcport == "" { 206 | fmt.Fprintln(os.Stderr, "missing -grpcport flag (:50051)") 207 | flag.Usage() 208 | os.Exit(2) 209 | } 210 | 211 | lis, err := net.Listen("tcp", *grpcport) 212 | if err != nil { 213 | log.Fatalf("failed to listen: %v", err) 214 | } 215 | 216 | opts := []grpc.ServerOption{grpc.MaxConcurrentStreams(10)} 217 | opts = append(opts) 218 | 219 | s := grpc.NewServer(opts...) 220 | 221 | auth.RegisterAuthorizationServer(s, &AuthorizationServer{}) 222 | healthpb.RegisterHealthServer(s, &healthServer{}) 223 | 224 | data, err := ioutil.ReadFile(keyfile) 225 | if err != nil { 226 | glog.Fatal(err) 227 | } 228 | 229 | block, _ := pem.Decode(data) 230 | 231 | privateKey, err = x509.ParsePKCS1PrivateKey(block.Bytes) 232 | if err != nil { 233 | glog.Fatal(err) 234 | } 235 | 236 | log.Printf("Starting gRPC Server at %s", *grpcport) 237 | s.Serve(lis) 238 | 239 | } 240 | -------------------------------------------------------------------------------- /backend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:latest 2 | 3 | COPY . /app 4 | RUN cd /app && npm install --silent 5 | EXPOSE 8080 6 | ARG VER=0 7 | ENV VER=$VER 8 | CMD ["node", "/app/app.js"] 9 | -------------------------------------------------------------------------------- /backend/app.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | 3 | const app = express(); 4 | var rp = require('request-promise'); 5 | const dns = require('dns'); 6 | const morgan = require('morgan'); 7 | 8 | const port = 8080; 9 | 10 | app.use( 11 | morgan('combined') 12 | ); 13 | 14 | var winston = require('winston'); 15 | var logger = winston.createLogger({ 16 | transports: [ 17 | new (winston.transports.Console)({ level: 'info' }) 18 | ] 19 | }); 20 | 21 | app.get('/', (request, response) => { 22 | logger.info('Called /'); 23 | response.send('Hello from Express!'); 24 | }) 25 | 26 | app.get('/_ah/health', (request, response) => { 27 | response.send('ok'); 28 | }) 29 | 30 | app.get('/varz', (request, response) => { 31 | response.send(process.env); 32 | }) 33 | 34 | app.get('/version', (request, response) => { 35 | response.send(process.env.VER); 36 | }) 37 | 38 | app.get('/backend', (request, response) => { 39 | var auth_header = request.headers['authorization']; 40 | logger.info("Got Authorization Header: [" + auth_header + "]"); 41 | response.send('pod: [' + process.env.MY_POD_NAME + '] node: [' + process.env.MY_NODE_NAME + ']'); 42 | }) 43 | 44 | app.get('/headerz', (request, response) => { 45 | logger.info('/headerz'); 46 | response.send(request.headers); 47 | }) 48 | 49 | const server = app.listen(port, () => logger.info('Running…')); 50 | 51 | 52 | setInterval(() => server.getConnections( 53 | (err, connections) => console.log(`${connections} connections currently open`) 54 | ), 60000); 55 | 56 | process.on('SIGTERM', shutDown); 57 | process.on('SIGINT', shutDown); 58 | 59 | let connections = []; 60 | 61 | server.on('connection', connection => { 62 | connections.push(connection); 63 | connection.on('close', () => connections = connections.filter(curr => curr !== connection)); 64 | }); 65 | 66 | function shutDown() { 67 | console.log('Received kill signal, shutting down gracefully'); 68 | server.close(() => { 69 | logger.info('Closed out remaining connections'); 70 | process.exit(0); 71 | }); 72 | 73 | setTimeout(() => { 74 | logger.error('Could not close connections in time, forcefully shutting down'); 75 | process.exit(1); 76 | }, 10000); 77 | 78 | connections.forEach(curr => curr.end()); 79 | setTimeout(() => connections.forEach(curr => curr.destroy()), 5000); 80 | } 81 | -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "myapp", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "start": "node app.js" 6 | }, 7 | "dependencies": { 8 | "@google-cloud/trace-agent": "2.3.3", 9 | "debug": "~2.6.9", 10 | "dns": "0.2.2", 11 | "express": "^4.17.1", 12 | "log4js": "^0.6.27", 13 | "morgan": "^1.9.1", 14 | "request-promise": "4.2.2", 15 | "winston": "*" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /certs/CA_crt.pem: -------------------------------------------------------------------------------- 1 | Certificate: 2 | Data: 3 | Version: 3 (0x2) 4 | Serial Number: 1 (0x1) 5 | Signature Algorithm: sha256WithRSAEncryption 6 | Issuer: C=US, O=Google, OU=Enterprise, CN=Single Root CA 7 | Validity 8 | Not Before: Dec 27 17:38:02 2023 GMT 9 | Not After : Dec 26 17:38:02 2033 GMT 10 | Subject: C=US, O=Google, OU=Enterprise, CN=Single Root CA 11 | Subject Public Key Info: 12 | Public Key Algorithm: rsaEncryption 13 | Public-Key: (2048 bit) 14 | Modulus: 15 | 00:bd:4e:4b:a5:b0:44:c4:8a:e0:82:cf:f2:e2:30: 16 | 02:08:9b:c3:31:82:29:7c:49:91:24:a9:eb:f3:6e: 17 | 2e:f8:97:51:6d:b2:54:be:8e:e9:18:18:e4:24:6f: 18 | 4e:9e:7c:82:c3:d1:1b:0a:7c:f1:6f:90:37:3a:5c: 19 | 26:ff:4a:2b:72:53:dc:7a:22:8f:e5:ab:14:13:e9: 20 | 44:be:03:60:3a:d0:dd:e5:95:61:40:84:5b:e7:19: 21 | 01:5f:c6:87:5d:47:fa:e2:0a:6e:bd:9d:7b:b1:88: 22 | ac:8f:a3:8f:97:b7:6d:57:03:a3:6c:bf:7b:25:1e: 23 | f0:ba:09:c2:47:7e:0a:eb:b8:c3:9f:e7:87:c9:a1: 24 | 4b:0f:70:ca:75:0b:ee:b3:8e:a3:9a:92:f6:18:65: 25 | 5a:e0:37:8c:ef:48:bb:cd:45:35:85:11:19:dc:d8: 26 | f1:6f:8d:a5:d7:ef:48:15:9f:1a:47:fa:16:dd:77: 27 | 29:69:06:6d:1e:d9:1d:4f:86:61:df:6b:b8:91:f1: 28 | 43:b6:80:e4:65:20:66:90:aa:d1:4d:a6:3d:39:7c: 29 | a2:19:8e:16:a7:61:a2:f2:18:4b:13:25:ee:1b:72: 30 | 38:b9:8f:3c:50:ad:cd:88:fa:dc:08:a5:67:8e:0f: 31 | 8a:c8:66:8d:13:50:24:c3:1f:a8:c7:a9:56:7f:c3: 32 | 15:3f 33 | Exponent: 65537 (0x10001) 34 | X509v3 extensions: 35 | X509v3 Key Usage: critical 36 | Certificate Sign, CRL Sign 37 | X509v3 Basic Constraints: critical 38 | CA:TRUE 39 | X509v3 Subject Key Identifier: 40 | EC:F0:EA:53:53:3F:9F:23:DC:C1:0E:31:10:37:07:DE:DE:E7:6E:F3 41 | X509v3 Authority Key Identifier: 42 | EC:F0:EA:53:53:3F:9F:23:DC:C1:0E:31:10:37:07:DE:DE:E7:6E:F3 43 | Signature Algorithm: sha256WithRSAEncryption 44 | Signature Value: 45 | 77:aa:56:03:04:39:86:fc:c8:69:8b:c3:02:ae:87:43:e9:af: 46 | 66:60:fd:f0:df:98:3e:f3:f1:ea:bb:8e:1a:40:ce:18:d2:b4: 47 | 28:93:e4:4d:1c:15:14:89:23:85:10:7f:cf:f6:37:b4:64:c8: 48 | 8e:57:67:41:2e:08:0f:19:87:e6:7a:8a:4a:e6:93:92:96:53: 49 | 27:19:21:24:38:c7:2f:43:67:be:a4:85:3c:e4:b0:10:d7:14: 50 | 76:48:e8:81:e0:ba:7a:26:c4:f3:1d:ba:61:bd:cd:96:4e:9d: 51 | f6:b6:e5:ad:6a:79:c0:89:01:a9:1e:9b:54:95:97:6e:fb:99: 52 | 17:84:7f:b6:a0:05:a1:0b:41:a6:c8:d3:a4:08:f6:d6:ff:6f: 53 | 1e:94:87:d0:c3:4b:da:8a:4a:aa:42:a8:60:2c:55:25:b1:62: 54 | bc:a3:54:0e:de:4a:36:07:78:ed:2a:ed:d0:e2:d8:45:45:f3: 55 | 4e:4f:1c:3f:a5:5d:59:bf:5e:66:f4:f6:77:1b:87:90:c0:78: 56 | 94:aa:a8:1b:83:6d:0f:f9:83:32:5b:18:78:1b:99:46:61:64: 57 | 46:61:80:bd:d8:3d:6a:e5:6f:cf:37:c6:bf:32:c9:a0:6e:f4: 58 | 95:4e:de:07:35:af:2e:d5:1f:9d:6c:fb:b4:59:db:90:bb:b9: 59 | 44:43:40:92 60 | -----BEGIN CERTIFICATE----- 61 | MIIDdjCCAl6gAwIBAgIBATANBgkqhkiG9w0BAQsFADBMMQswCQYDVQQGEwJVUzEP 62 | MA0GA1UECgwGR29vZ2xlMRMwEQYDVQQLDApFbnRlcnByaXNlMRcwFQYDVQQDDA5T 63 | aW5nbGUgUm9vdCBDQTAeFw0yMzEyMjcxNzM4MDJaFw0zMzEyMjYxNzM4MDJaMEwx 64 | CzAJBgNVBAYTAlVTMQ8wDQYDVQQKDAZHb29nbGUxEzARBgNVBAsMCkVudGVycHJp 65 | c2UxFzAVBgNVBAMMDlNpbmdsZSBSb290IENBMIIBIjANBgkqhkiG9w0BAQEFAAOC 66 | AQ8AMIIBCgKCAQEAvU5LpbBExIrggs/y4jACCJvDMYIpfEmRJKnr824u+JdRbbJU 67 | vo7pGBjkJG9OnnyCw9EbCnzxb5A3Olwm/0orclPceiKP5asUE+lEvgNgOtDd5ZVh 68 | QIRb5xkBX8aHXUf64gpuvZ17sYisj6OPl7dtVwOjbL97JR7wugnCR34K67jDn+eH 69 | yaFLD3DKdQvus46jmpL2GGVa4DeM70i7zUU1hREZ3Njxb42l1+9IFZ8aR/oW3Xcp 70 | aQZtHtkdT4Zh32u4kfFDtoDkZSBmkKrRTaY9OXyiGY4Wp2Gi8hhLEyXuG3I4uY88 71 | UK3NiPrcCKVnjg+KyGaNE1Akwx+ox6lWf8MVPwIDAQABo2MwYTAOBgNVHQ8BAf8E 72 | BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU7PDqU1M/nyPcwQ4xEDcH 73 | 3t7nbvMwHwYDVR0jBBgwFoAU7PDqU1M/nyPcwQ4xEDcH3t7nbvMwDQYJKoZIhvcN 74 | AQELBQADggEBAHeqVgMEOYb8yGmLwwKuh0Ppr2Zg/fDfmD7z8eq7jhpAzhjStCiT 75 | 5E0cFRSJI4UQf8/2N7RkyI5XZ0EuCA8Zh+Z6ikrmk5KWUycZISQ4xy9DZ76khTzk 76 | sBDXFHZI6IHgunomxPMdumG9zZZOnfa25a1qecCJAakem1SVl277mReEf7agBaEL 77 | QabI06QI9tb/bx6Uh9DDS9qKSqpCqGAsVSWxYryjVA7eSjYHeO0q7dDi2EVF805P 78 | HD+lXVm/Xmb09ncbh5DAeJSqqBuDbQ/5gzJbGHgbmUZhZEZhgL3YPWrlb883xr8y 79 | yaBu9JVO3gc1ry7VH51s+7RZ25C7uURDQJI= 80 | -----END CERTIFICATE----- 81 | -------------------------------------------------------------------------------- /certs/svc_crt.pem: -------------------------------------------------------------------------------- 1 | Certificate: 2 | Data: 3 | Version: 3 (0x2) 4 | Serial Number: 12 (0xc) 5 | Signature Algorithm: sha256WithRSAEncryption 6 | Issuer: C=US, O=Google, OU=Enterprise, CN=Single Root CA 7 | Validity 8 | Not Before: Apr 2 12:46:14 2024 GMT 9 | Not After : Apr 2 12:46:14 2034 GMT 10 | Subject: C=US, O=Google, OU=Enterprise, CN=svc.domain.com 11 | Subject Public Key Info: 12 | Public Key Algorithm: rsaEncryption 13 | Public-Key: (2048 bit) 14 | Modulus: 15 | 00:d3:35:6c:85:a3:40:a5:00:4b:fc:30:75:70:68: 16 | 3e:f1:0e:44:17:81:c7:ea:cb:b4:d0:51:7f:d3:71: 17 | 0b:24:bb:19:4f:82:c9:8a:46:f7:23:c5:81:e5:b2: 18 | a6:90:82:08:ed:d6:a4:86:de:85:3d:13:34:76:91: 19 | d1:e7:f0:11:af:8a:6c:70:11:55:c4:e0:3a:8c:60: 20 | 57:34:21:7b:f6:99:d5:86:e0:1e:a5:40:53:b8:b5: 21 | c4:81:a6:6f:76:73:35:e1:0f:3c:32:ae:c6:07:12: 22 | 75:d8:76:8c:d9:31:bb:81:dd:89:06:00:9b:bf:51: 23 | b8:a2:96:50:d9:13:3d:e0:61:da:9f:a6:15:8b:8d: 24 | db:7f:67:00:92:d1:99:23:0c:17:b4:82:35:7c:ba: 25 | 4e:59:4d:4a:83:1e:94:1f:0b:57:9f:3d:a5:2e:30: 26 | f0:ec:0d:20:70:61:4e:e6:84:db:0f:9c:c5:18:87: 27 | fd:2d:3d:06:c1:d5:cf:63:b8:57:fc:e5:54:07:4a: 28 | 0c:84:5a:fe:9e:56:28:66:ed:07:e4:21:72:03:bb: 29 | ea:05:04:32:04:d6:33:94:e2:bb:6b:b7:73:73:ea: 30 | fc:04:31:9b:1d:54:ba:a7:a2:1d:ca:cf:29:a8:c4: 31 | b7:de:3a:20:9b:01:d0:06:90:9e:88:5b:ac:ad:50: 32 | af:9f 33 | Exponent: 65537 (0x10001) 34 | X509v3 extensions: 35 | X509v3 Key Usage: critical 36 | Digital Signature 37 | X509v3 Basic Constraints: 38 | CA:FALSE 39 | X509v3 Extended Key Usage: 40 | TLS Web Server Authentication 41 | X509v3 Subject Key Identifier: 42 | 1F:27:1B:90:64:9C:DB:0C:30:94:A8:3D:BE:67:03:3E:8D:1E:3A:9A 43 | X509v3 Authority Key Identifier: 44 | EC:F0:EA:53:53:3F:9F:23:DC:C1:0E:31:10:37:07:DE:DE:E7:6E:F3 45 | Authority Information Access: 46 | CA Issuers - URI:http://pki.esodemoapp2.com/ca/root-ca.cer 47 | X509v3 CRL Distribution Points: 48 | Full Name: 49 | URI:http://pki.esodemoapp2.com/ca/root-ca.crl 50 | X509v3 Subject Alternative Name: 51 | DNS:svc1.example.com, DNS:svc2.example.com 52 | Signature Algorithm: sha256WithRSAEncryption 53 | Signature Value: 54 | b0:10:7a:90:85:e4:d5:46:e2:3a:f2:22:05:55:d6:64:2f:11: 55 | 82:9f:4f:fc:84:2e:32:39:3d:bc:ff:e8:04:65:c2:24:98:8e: 56 | 7e:8b:f2:39:b5:4a:4c:ce:be:45:7e:46:18:05:59:95:c2:0f: 57 | 52:19:db:4f:59:6a:f5:c2:2b:56:2b:dd:29:61:e9:ff:cc:8c: 58 | f3:60:2f:e7:2e:4f:79:b7:cf:1d:2b:33:44:76:60:48:f2:2c: 59 | fb:f8:27:b2:60:65:45:08:70:18:a7:e2:ef:97:dd:0c:98:9d: 60 | 73:f9:e2:c9:3a:be:15:97:f0:e2:92:2d:fd:55:98:72:8f:53: 61 | 7d:11:61:dd:38:c1:9b:d0:63:57:cb:f6:d7:00:a5:00:bd:dc: 62 | f8:44:69:e1:2b:36:20:84:63:51:38:e9:b9:12:9e:b8:d0:61: 63 | ed:f7:eb:b6:8a:54:5a:64:5b:c8:ce:55:38:8a:20:52:b5:66: 64 | fd:47:c6:d0:ea:c3:54:fc:95:a7:07:a9:d6:e2:74:53:d3:fb: 65 | b4:07:70:5d:54:c9:56:fa:2a:93:fa:88:a8:d8:1c:d5:06:5b: 66 | 44:50:9c:71:bb:40:88:2b:25:88:51:5b:75:fe:0e:96:dd:98: 67 | d5:7a:d6:29:12:66:0c:68:28:4a:02:4d:d7:f3:12:37:a9:3a: 68 | 79:41:ea:68 69 | -----BEGIN CERTIFICATE----- 70 | MIIEOzCCAyOgAwIBAgIBDDANBgkqhkiG9w0BAQsFADBMMQswCQYDVQQGEwJVUzEP 71 | MA0GA1UECgwGR29vZ2xlMRMwEQYDVQQLDApFbnRlcnByaXNlMRcwFQYDVQQDDA5T 72 | aW5nbGUgUm9vdCBDQTAeFw0yNDA0MDIxMjQ2MTRaFw0zNDA0MDIxMjQ2MTRaMEwx 73 | CzAJBgNVBAYTAlVTMQ8wDQYDVQQKDAZHb29nbGUxEzARBgNVBAsMCkVudGVycHJp 74 | c2UxFzAVBgNVBAMMDnN2Yy5kb21haW4uY29tMIIBIjANBgkqhkiG9w0BAQEFAAOC 75 | AQ8AMIIBCgKCAQEA0zVshaNApQBL/DB1cGg+8Q5EF4HH6su00FF/03ELJLsZT4LJ 76 | ikb3I8WB5bKmkIII7dakht6FPRM0dpHR5/ARr4pscBFVxOA6jGBXNCF79pnVhuAe 77 | pUBTuLXEgaZvdnM14Q88Mq7GBxJ12HaM2TG7gd2JBgCbv1G4opZQ2RM94GHan6YV 78 | i43bf2cAktGZIwwXtII1fLpOWU1Kgx6UHwtXnz2lLjDw7A0gcGFO5oTbD5zFGIf9 79 | LT0GwdXPY7hX/OVUB0oMhFr+nlYoZu0H5CFyA7vqBQQyBNYzlOK7a7dzc+r8BDGb 80 | HVS6p6Idys8pqMS33jogmwHQBpCeiFusrVCvnwIDAQABo4IBJjCCASIwDgYDVR0P 81 | AQH/BAQDAgeAMAkGA1UdEwQCMAAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwHQYDVR0O 82 | BBYEFB8nG5BknNsMMJSoPb5nAz6NHjqaMB8GA1UdIwQYMBaAFOzw6lNTP58j3MEO 83 | MRA3B97e527zMEUGCCsGAQUFBwEBBDkwNzA1BggrBgEFBQcwAoYpaHR0cDovL3Br 84 | aS5lc29kZW1vYXBwMi5jb20vY2Evcm9vdC1jYS5jZXIwOgYDVR0fBDMwMTAvoC2g 85 | K4YpaHR0cDovL3BraS5lc29kZW1vYXBwMi5jb20vY2Evcm9vdC1jYS5jcmwwLQYD 86 | VR0RBCYwJIIQc3ZjMS5leGFtcGxlLmNvbYIQc3ZjMi5leGFtcGxlLmNvbTANBgkq 87 | hkiG9w0BAQsFAAOCAQEAsBB6kIXk1UbiOvIiBVXWZC8Rgp9P/IQuMjk9vP/oBGXC 88 | JJiOfovyObVKTM6+RX5GGAVZlcIPUhnbT1lq9cIrVivdKWHp/8yM82Av5y5PebfP 89 | HSszRHZgSPIs+/gnsmBlRQhwGKfi75fdDJidc/niyTq+FZfw4pIt/VWYco9TfRFh 90 | 3TjBm9BjV8v21wClAL3c+ERp4Ss2IIRjUTjpuRKeuNBh7ffrtopUWmRbyM5VOIog 91 | UrVm/UfG0OrDVPyVpwep1uJ0U9P7tAdwXVTJVvoqk/qIqNgc1QZbRFCccbtAiCsl 92 | iFFbdf4Olt2Y1XrWKRJmDGgoSgJN1/MSN6k6eUHqaA== 93 | -----END CERTIFICATE----- 94 | -------------------------------------------------------------------------------- /certs/svc_key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDTNWyFo0ClAEv8 3 | MHVwaD7xDkQXgcfqy7TQUX/TcQskuxlPgsmKRvcjxYHlsqaQggjt1qSG3oU9EzR2 4 | kdHn8BGvimxwEVXE4DqMYFc0IXv2mdWG4B6lQFO4tcSBpm92czXhDzwyrsYHEnXY 5 | dozZMbuB3YkGAJu/UbiillDZEz3gYdqfphWLjdt/ZwCS0ZkjDBe0gjV8uk5ZTUqD 6 | HpQfC1efPaUuMPDsDSBwYU7mhNsPnMUYh/0tPQbB1c9juFf85VQHSgyEWv6eVihm 7 | 7QfkIXIDu+oFBDIE1jOU4rtrt3Nz6vwEMZsdVLqnoh3KzymoxLfeOiCbAdAGkJ6I 8 | W6ytUK+fAgMBAAECggEATLyVUDyPKSPhd6AXmx6U97oKLUw+2WTnreRef/ELbm33 9 | 9TZ8iRvdgQaqek0dTVWhbuBUaJgCar1Gi2nRjOZhVpkBavoxYlVhkE0UgeFEi8U6 10 | mkVlfP7RLEQGQGC7EJstUTba1UNAuaMQY/Q2mlcCXF2kAiVIcQt8/L2GZEgkbBgY 11 | V8iLu92u+CXRfoZGTjdQdp07KjUEcglX/u1BUDJoB2f5Sp4Hk1WgeQH3MWA/ruXy 12 | Vpx1cc5oA5hXJjpp++cBuhydoKIYyKAyBksvptbzXFFb8BWbQcJuWQ8tximoL/LM 13 | i2BSYKbo+Q5qHIr5X12yXoNzUoRAmpqLwuybz0g2gQKBgQDy/6fLBa+2LDENcWLS 14 | qAxXd5coa5oA1fvyG61TsxqIDPnu+I9JJGZ35DS+dRKtd7mJSYlHuNehrMdoHw7W 15 | 5tuzMuWlVLMB9R8KcmGTyNG85KoGE8faIK6JxOx9RW9lpuF/qpXzG/ygi/PF7k6/ 16 | 1YWXgmPZPDi+St7I8dZGUeeIQQKBgQDeglejzu3otfmKtb/14Liy5XWYWICYcDyF 17 | B6q0Re1Nr76fzuClN5d6xbXjjBrE5S7VnzUcJgf9PC/zRtat1yn2I+UGKde6qVr0 18 | eKplfs74COdVeL1bDFYFnNrqKA0A+pRxAWfQ5JILghgUrTzimhz+Z11I+PmRNpow 19 | LzUiG9k/3wKBgQCq5JUSuNsMGSQeOiGv2LNlSBusN5BFCjh32nMZLBp6l/Wl1HSg 20 | kdLmej2FvDv4dvyqymabZClx8FsEpOMAy7ay3haXIWwK8mJ0LGDnWBH48C+KupqQ 21 | ku9swGs45n4jVSu9ZzCxmnseRY6ZIrGLRBUBqpeiDTIy0eDAHSP+rFdSgQKBgBuU 22 | Nj6keFJ+s6ZgrqFQMDRkQnYWiiHaz3WBpMPhYu9dGBiAsNFpmYnrFYdVpz++VFa/ 23 | +/o4MIdPvXW+0v8LCzVp74obB5UoRpDEoAIifI67s8ihRX8SgVsCvROG0U3MwTdc 24 | gDyhCAULI+b0Lfa8OVnDAAn6ahx4kxXj1AcdQokrAoGBAKTLtlK4IgSoq0+5ODaZ 25 | q+GGRcA5m6acdSSwe6Wn00i+qH+x80Zemhiwvrrr2CGmMVOtUoBgzPy0Rf8exbXQ 26 | 4qNU4VWmFgy2defiSGyNSkc3NPv/PyOCRwS3cjRUjlRWb2ic+Gu6wL/Bp6vy8rmf 27 | Oz+2bPRtfuEf9VM9r5UixiYi 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /ext_authz.yaml.tmpl: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: authz-ns 5 | labels: 6 | istio-injection: enabled 7 | --- 8 | apiVersion: apps/v1 9 | kind: Deployment 10 | metadata: 11 | name: authz 12 | namespace: authz-ns 13 | spec: 14 | selector: 15 | matchLabels: 16 | app: authz 17 | replicas: 1 18 | template: 19 | metadata: 20 | labels: 21 | app: authz 22 | spec: 23 | serviceAccountName: authz-sa 24 | containers: 25 | - name: authz-container 26 | image: salrashid123/ext-authz-server 27 | volumeMounts: 28 | - name: keyfile 29 | mountPath: "/data/certs" 30 | readOnly: true 31 | imagePullPolicy: IfNotPresent 32 | ports: 33 | - containerPort: 50051 34 | env: 35 | - name: AUTHZ_ALLOWED_USERS 36 | valueFrom: 37 | configMapKeyRef: 38 | name: authz-config 39 | key: allowedusers 40 | - name: AUTHZ_SERVER_KEY_ID 41 | valueFrom: 42 | configMapKeyRef: 43 | name: authz-config 44 | key: authzserverkeyid 45 | - name: AUTHZ_ISSUER 46 | valueFrom: 47 | configMapKeyRef: 48 | name: authz-config 49 | key: authzissuer 50 | volumes: 51 | - name: keyfile 52 | secret: 53 | secretName: svc-secret 54 | --- 55 | apiVersion: v1 56 | data: 57 | key.pem: $SVC_ACCOUNT_KEY 58 | kind: Secret 59 | metadata: 60 | name: svc-secret 61 | namespace: authz-ns 62 | type: Opaque 63 | --- 64 | apiVersion: v1 65 | kind: ConfigMap 66 | metadata: 67 | name: authz-config 68 | namespace: authz-ns 69 | data: 70 | allowedusers: "alice,bob,carol" 71 | authzserverkeyid: "$KEY_ID" 72 | authzissuer: "$SERVICE_ACCOUNT_EMAIL" 73 | --- 74 | apiVersion: v1 75 | kind: Service 76 | metadata: 77 | name: authz 78 | namespace: authz-ns 79 | labels: 80 | app: authz 81 | spec: 82 | ports: 83 | - port: 50051 84 | targetPort: 50051 85 | name: grpc 86 | selector: 87 | app: authz 88 | --- 89 | apiVersion: v1 90 | kind: ServiceAccount 91 | metadata: 92 | name: authz-sa 93 | namespace: authz-ns 94 | --- 95 | apiVersion: networking.istio.io/v1 96 | kind: VirtualService 97 | metadata: 98 | name: authz-virtualservice 99 | namespace: authz-ns 100 | spec: 101 | hosts: 102 | - authz 103 | gateways: 104 | - mesh 105 | http: 106 | - route: 107 | - destination: 108 | host: authz 109 | match: 110 | - sourceLabels: 111 | istio: ingressgateway 112 | --- 113 | apiVersion: networking.istio.io/v1 114 | kind: DestinationRule 115 | metadata: 116 | name: authz-destination 117 | namespace: authz-ns 118 | spec: 119 | host: "authz.authz-ns.svc.cluster.local" 120 | trafficPolicy: 121 | tls: 122 | mode: ISTIO_MUTUAL 123 | --- 124 | ## ingress --> authz 125 | apiVersion: security.istio.io/v1 126 | kind: PeerAuthentication 127 | metadata: 128 | name: ing-authzserver-peer-authn-policy 129 | namespace: authz-ns 130 | spec: 131 | selector: 132 | matchLabels: 133 | app: authz 134 | mtls: 135 | mode: STRICT 136 | --- 137 | apiVersion: security.istio.io/v1 138 | kind: AuthorizationPolicy 139 | metadata: 140 | name: ing-authzserver-authz-policy 141 | namespace: authz-ns 142 | spec: 143 | action: ALLOW 144 | selector: 145 | matchLabels: 146 | app: authz 147 | rules: 148 | - from: 149 | - source: 150 | principals: ["cluster.local/ns/istio-system/sa/istio-ingressgateway-service-account"] 151 | --- 152 | apiVersion: security.istio.io/v1 153 | kind: AuthorizationPolicy 154 | metadata: 155 | name: ext-authz 156 | namespace: istio-system 157 | spec: 158 | selector: 159 | matchLabels: 160 | istio: ingressgateway 161 | action: CUSTOM 162 | provider: 163 | name: "my-ext-authz-grpc" 164 | rules: 165 | - to: 166 | - operation: 167 | paths: ["/*"] 168 | --- 169 | ## default deny all 170 | apiVersion: security.istio.io/v1 171 | kind: AuthorizationPolicy 172 | metadata: 173 | name: deny-all-authz-ns 174 | namespace: authz-ns 175 | spec: 176 | {} 177 | --- -------------------------------------------------------------------------------- /ext_authz_rules.yaml.tmpl: -------------------------------------------------------------------------------- 1 | ## ingress --> svc1 2 | apiVersion: security.istio.io/v1 3 | kind: RequestAuthentication 4 | metadata: 5 | name: ing-svc1-request-authn-policy 6 | namespace: default 7 | spec: 8 | selector: 9 | matchLabels: 10 | app: svc1 11 | jwtRules: 12 | - issuer: "$SERVICE_ACCOUNT_EMAIL" 13 | audiences: 14 | - "http://svc1.default.svc.cluster.local:8080/" 15 | jwksUri: "https://raw.githubusercontent.com/istio/istio/release-$ISTIO_VERSION_MINOR/security/tools/jwt/samples/jwks.json" 16 | forwardOriginalToken: true 17 | --- 18 | apiVersion: security.istio.io/v1 19 | kind: AuthorizationPolicy 20 | metadata: 21 | name: ing-svc1-authz-policy 22 | namespace: default 23 | spec: 24 | action: ALLOW 25 | selector: 26 | matchLabels: 27 | app: svc1 28 | rules: 29 | - from: 30 | - source: 31 | principals: ["cluster.local/ns/istio-system/sa/istio-ingressgateway-service-account"] 32 | to: 33 | - operation: 34 | methods: ["GET"] 35 | when: 36 | - key: request.auth.claims[iss] 37 | values: ["$SERVICE_ACCOUNT_EMAIL"] 38 | - key: request.auth.claims[aud] 39 | values: ["http://svc1.default.svc.cluster.local:8080/"] 40 | --- 41 | ## ingress --> svc2 42 | apiVersion: security.istio.io/v1 43 | kind: RequestAuthentication 44 | metadata: 45 | name: ing-svc2-request-authn-policy 46 | namespace: default 47 | spec: 48 | selector: 49 | matchLabels: 50 | app: svc2 51 | jwtRules: 52 | - issuer: "$SERVICE_ACCOUNT_EMAIL" 53 | audiences: 54 | - "http://svc2.default.svc.cluster.local:8080/" 55 | jwksUri: "https://raw.githubusercontent.com/istio/istio/release-$ISTIO_VERSION_MINOR/security/tools/jwt/samples/jwks.json" 56 | forwardOriginalToken: true 57 | --- 58 | apiVersion: security.istio.io/v1 59 | kind: AuthorizationPolicy 60 | metadata: 61 | name: ing-svc2-authz-policy 62 | namespace: default 63 | spec: 64 | action: ALLOW 65 | selector: 66 | matchLabels: 67 | app: svc2 68 | rules: 69 | - from: 70 | - source: 71 | principals: ["cluster.local/ns/istio-system/sa/istio-ingressgateway-service-account"] 72 | to: 73 | - operation: 74 | methods: ["GET"] 75 | when: 76 | - key: request.auth.claims[iss] 77 | values: ["$SERVICE_ACCOUNT_EMAIL"] 78 | - key: request.auth.claims[aud] 79 | values: ["http://svc2.default.svc.cluster.local:8080/"] 80 | --- 81 | ## default deny all 82 | apiVersion: security.istio.io/v1 83 | kind: AuthorizationPolicy 84 | metadata: 85 | name: deny-all-default 86 | namespace: default 87 | spec: 88 | {} 89 | --- 90 | ## svc --> be-v1 91 | apiVersion: security.istio.io/v1 92 | kind: RequestAuthentication 93 | metadata: 94 | name: svc-be-v1-request-authn-policy 95 | namespace: default 96 | spec: 97 | selector: 98 | matchLabels: 99 | app: be 100 | version: v1 101 | jwtRules: 102 | - issuer: "$SERVICE_ACCOUNT_EMAIL" 103 | audiences: 104 | - "http://be.default.svc.cluster.local:8080/" 105 | jwksUri: "https://raw.githubusercontent.com/istio/istio/release-$ISTIO_VERSION_MINOR/security/tools/jwt/samples/jwks.json" 106 | # forwardOriginalToken: true 107 | outputPayloadToHeader: x-jwt-payload 108 | --- 109 | apiVersion: security.istio.io/v1 110 | kind: AuthorizationPolicy 111 | metadata: 112 | name: svc1-be-v1-authz-policy 113 | namespace: default 114 | spec: 115 | action: ALLOW 116 | selector: 117 | matchLabels: 118 | app: be 119 | version: v1 120 | rules: 121 | - from: 122 | - source: 123 | principals: ["cluster.local/ns/default/sa/svc1-sa"] 124 | to: 125 | - operation: 126 | methods: ["GET"] 127 | when: 128 | - key: request.auth.claims[iss] 129 | values: ["$SERVICE_ACCOUNT_EMAIL"] 130 | - key: request.auth.claims[aud] 131 | values: ["http://be.default.svc.cluster.local:8080/"] 132 | --- 133 | ## svc --> be-v2 134 | apiVersion: security.istio.io/v1 135 | kind: RequestAuthentication 136 | metadata: 137 | name: svc-be-v2-request-authn-policy 138 | namespace: default 139 | spec: 140 | selector: 141 | matchLabels: 142 | app: be 143 | version: v2 144 | jwtRules: 145 | - issuer: "$SERVICE_ACCOUNT_EMAIL" 146 | audiences: 147 | - "http://be.default.svc.cluster.local:8080/" 148 | jwksUri: "https://raw.githubusercontent.com/istio/istio/release-$ISTIO_VERSION_MINOR/security/tools/jwt/samples/jwks.json" 149 | # forwardOriginalToken: true 150 | outputPayloadToHeader: x-jwt-payload 151 | --- 152 | apiVersion: security.istio.io/v1 153 | kind: AuthorizationPolicy 154 | metadata: 155 | name: svc1-be-v2-authz-policy 156 | namespace: default 157 | spec: 158 | action: ALLOW 159 | selector: 160 | matchLabels: 161 | app: be 162 | version: v2 163 | rules: 164 | - from: 165 | - source: 166 | principals: ["cluster.local/ns/default/sa/svc1-sa"] 167 | to: 168 | - operation: 169 | methods: ["GET"] 170 | when: 171 | - key: request.auth.claims[iss] 172 | values: ["$SERVICE_ACCOUNT_EMAIL"] 173 | - key: request.auth.claims[aud] 174 | values: ["http://be.default.svc.cluster.local:8080/"] -------------------------------------------------------------------------------- /frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:latest 2 | 3 | COPY . /app 4 | RUN cd /app && npm install --silent 5 | EXPOSE 8080 6 | ARG VER=0 7 | ENV VER=$VER 8 | CMD ["node", "/app/app.js"] 9 | -------------------------------------------------------------------------------- /frontend/app.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | 3 | const app = express(); 4 | var rp = require('request-promise'); 5 | const dns = require('dns'); 6 | const morgan = require('morgan'); 7 | 8 | const port = 8080; 9 | 10 | const FORWARD_AUTH_HEADER = process.env.FORWARD_AUTH_HEADER || 'false'; 11 | const BACKEND_NAMESPACE = process.env.BACKEND_NAMESPACE || 'default'; 12 | 13 | app.use( 14 | morgan('combined') 15 | ); 16 | 17 | var winston = require('winston'); 18 | var logger = winston.createLogger({ 19 | transports: [ 20 | new (winston.transports.Console)({ level: 'info' }) 21 | ] 22 | }); 23 | 24 | app.get('/', (request, response) => { 25 | logger.info('Called /'); 26 | response.send('Hello from Express!'); 27 | }) 28 | 29 | app.get('/_ah/health', (request, response) => { 30 | response.send('ok'); 31 | }) 32 | 33 | app.get('/varz', (request, response) => { 34 | response.send(process.env); 35 | }) 36 | 37 | app.get('/version', (request, response) => { 38 | response.send(process.env.VER); 39 | }) 40 | 41 | 42 | app.get('/backend', (request, response) => { 43 | 44 | 45 | dns.resolveSrv("_http._tcp.be."+ BACKEND_NAMESPACE + ".svc.cluster.local", function onLookup(err, addresses, family) { 46 | 47 | if (err) { 48 | response.send(err); 49 | } else if (addresses.length >= 1) { 50 | logger.info('addresses: ' + JSON.stringify(addresses)); 51 | var host = addresses[0].name; 52 | var port = addresses[0].port; 53 | logger.info(host + " --> " + port); 54 | 55 | var resp_promises = [] 56 | var urls = [ 57 | 'http://' + host + ':' + port + '/backend', 58 | 'http://' + host + ':' + port + '/headerz', 59 | ] 60 | 61 | out_headers = {}; 62 | if (FORWARD_AUTH_HEADER == 'true') { 63 | var auth_header = request.headers['authorization']; 64 | logger.info("Got Authorization Header: [" + auth_header + "]"); 65 | out_headers = { 66 | 'authorization': auth_header, 67 | }; 68 | } 69 | 70 | urls.forEach(element => { 71 | resp_promises.push( getURL(element,out_headers) ) 72 | }); 73 | 74 | Promise.all(resp_promises).then(function(value) { 75 | response.send(value); 76 | }, function(value) { 77 | response.send(value); 78 | }); 79 | 80 | } else{ 81 | response.send('No Backend Services found'); 82 | } 83 | }); 84 | }) 85 | 86 | function getURL(u, headers) { 87 | logger.info("Sending outbound Headers --> " + JSON.stringify(headers, null, 2)); 88 | var options = { 89 | method: 'GET', 90 | uri: u, 91 | resolveWithFullResponse: true, 92 | simple: false, 93 | headers: headers 94 | }; 95 | return rp(options) 96 | .then(function (resp) { 97 | return Promise.resolve( 98 | { 'url' : u, 'body': resp.body, 'statusCode': resp.statusCode } 99 | ); 100 | }) 101 | .catch(function (err) { 102 | return Promise.resolve({ 'url' : u, 'statusCode': err } ); 103 | }); 104 | } 105 | 106 | app.get('/headerz', (request, response) => { 107 | logger.info('/headerz'); 108 | response.send(request.headers); 109 | }) 110 | 111 | const server = app.listen(port, () => logger.info('Running…')); 112 | 113 | 114 | setInterval(() => server.getConnections( 115 | (err, connections) => console.log(`${connections} connections currently open`) 116 | ), 60000); 117 | 118 | process.on('SIGTERM', shutDown); 119 | process.on('SIGINT', shutDown); 120 | 121 | let connections = []; 122 | 123 | server.on('connection', connection => { 124 | connections.push(connection); 125 | connection.on('close', () => connections = connections.filter(curr => curr !== connection)); 126 | }); 127 | 128 | function shutDown() { 129 | console.log('Received kill signal, shutting down gracefully'); 130 | server.close(() => { 131 | logger.info('Closed out remaining connections'); 132 | process.exit(0); 133 | }); 134 | 135 | setTimeout(() => { 136 | logger.error('Could not close connections in time, forcefully shutting down'); 137 | process.exit(1); 138 | }, 10000); 139 | 140 | connections.forEach(curr => curr.end()); 141 | setTimeout(() => connections.forEach(curr => curr.destroy()), 5000); 142 | } 143 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "myapp", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "start": "node app.js" 6 | }, 7 | "dependencies": { 8 | "@google-cloud/trace-agent": "2.3.3", 9 | "debug": "~2.6.9", 10 | "dns": "0.2.2", 11 | "express": "^4.17.1", 12 | "log4js": "^0.6.27", 13 | "morgan": "^1.9.1", 14 | "request-promise": "4.2.2", 15 | "winston": "*" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /images/authz_ns_flow_fe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salrashid123/istio_external_authorization_server/90a8ffeca47424a6856befec7c41ad01936d8ec3/images/authz_ns_flow_fe.png -------------------------------------------------------------------------------- /images/authz_ns_flow_full.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salrashid123/istio_external_authorization_server/90a8ffeca47424a6856befec7c41ad01936d8ec3/images/authz_ns_flow_full.png -------------------------------------------------------------------------------- /images/config_img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salrashid123/istio_external_authorization_server/90a8ffeca47424a6856befec7c41ad01936d8ec3/images/config_img.png -------------------------------------------------------------------------------- /images/default-traffic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salrashid123/istio_external_authorization_server/90a8ffeca47424a6856befec7c41ad01936d8ec3/images/default-traffic.png -------------------------------------------------------------------------------- /istio-app-config.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.istio.io/v1 2 | kind: VirtualService 3 | metadata: 4 | name: svc1-virtualservice 5 | spec: 6 | hosts: 7 | - "svc1.example.com" 8 | gateways: 9 | - my-gateway 10 | http: 11 | - route: 12 | - destination: 13 | host: svc1 14 | --- 15 | apiVersion: networking.istio.io/v1 16 | kind: DestinationRule 17 | metadata: 18 | name: svc1-destination 19 | spec: 20 | host: svc1 21 | trafficPolicy: 22 | tls: 23 | mode: ISTIO_MUTUAL 24 | loadBalancer: 25 | simple: ROUND_ROBIN 26 | --- 27 | apiVersion: networking.istio.io/v1 28 | kind: VirtualService 29 | metadata: 30 | name: svc2-virtualservice 31 | spec: 32 | hosts: 33 | - "svc2.example.com" 34 | gateways: 35 | - my-gateway 36 | http: 37 | - route: 38 | - destination: 39 | host: svc2 40 | --- 41 | apiVersion: networking.istio.io/v1 42 | kind: DestinationRule 43 | metadata: 44 | name: svc2-destination 45 | spec: 46 | host: svc2 47 | trafficPolicy: 48 | tls: 49 | mode: ISTIO_MUTUAL 50 | loadBalancer: 51 | simple: ROUND_ROBIN 52 | --- 53 | apiVersion: networking.istio.io/v1 54 | kind: VirtualService 55 | metadata: 56 | name: be-virtualservice 57 | spec: 58 | gateways: 59 | - mesh 60 | hosts: 61 | - be 62 | http: 63 | - match: 64 | - sourceLabels: 65 | app: svc1 66 | - sourceLabels: 67 | app: svc2 68 | route: 69 | - destination: 70 | host: be 71 | --- 72 | apiVersion: networking.istio.io/v1 73 | kind: DestinationRule 74 | metadata: 75 | name: be-destination 76 | spec: 77 | host: be 78 | trafficPolicy: 79 | tls: 80 | mode: ISTIO_MUTUAL 81 | loadBalancer: 82 | simple: ROUND_ROBIN 83 | subsets: 84 | - name: v1 85 | labels: 86 | version: v1 87 | - name: v2 88 | labels: 89 | version: v2 -------------------------------------------------------------------------------- /istio-ingress-gateway.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.istio.io/v1 2 | kind: Gateway 3 | metadata: 4 | name: my-gateway 5 | spec: 6 | selector: 7 | istio: ingressgateway 8 | servers: 9 | - port: 10 | number: 80 11 | name: http 12 | protocol: HTTP 13 | hosts: 14 | - "*" 15 | - port: 16 | number: 443 17 | name: https 18 | protocol: HTTPS 19 | hosts: 20 | - "*" 21 | tls: 22 | mode: SIMPLE 23 | serverCertificate: /etc/istio/ingressgateway-certs/tls.crt 24 | privateKey: /etc/istio/ingressgateway-certs/tls.key -------------------------------------------------------------------------------- /istio-lb-certs.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | data: 3 | tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUVPekNDQXlPZ0F3SUJBZ0lCRERBTkJna3Foa2lHOXcwQkFRc0ZBREJNTVFzd0NRWURWUVFHRXdKVlV6RVAKTUEwR0ExVUVDZ3dHUjI5dloyeGxNUk13RVFZRFZRUUxEQXBGYm5SbGNuQnlhWE5sTVJjd0ZRWURWUVFEREE1VAphVzVuYkdVZ1VtOXZkQ0JEUVRBZUZ3MHlOREEwTURJeE1qUTJNVFJhRncwek5EQTBNREl4TWpRMk1UUmFNRXd4CkN6QUpCZ05WQkFZVEFsVlRNUTh3RFFZRFZRUUtEQVpIYjI5bmJHVXhFekFSQmdOVkJBc01Da1Z1ZEdWeWNISnAKYzJVeEZ6QVZCZ05WQkFNTURuTjJZeTVrYjIxaGFXNHVZMjl0TUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQwpBUThBTUlJQkNnS0NBUUVBMHpWc2hhTkFwUUJML0RCMWNHZys4UTVFRjRISDZzdTAwRkYvMDNFTEpMc1pUNExKCmlrYjNJOFdCNWJLbWtJSUk3ZGFraHQ2RlBSTTBkcEhSNS9BUnI0cHNjQkZWeE9BNmpHQlhOQ0Y3OXBuVmh1QWUKcFVCVHVMWEVnYVp2ZG5NMTRRODhNcTdHQnhKMTJIYU0yVEc3Z2QySkJnQ2J2MUc0b3BaUTJSTTk0R0hhbjZZVgppNDNiZjJjQWt0R1pJd3dYdElJMWZMcE9XVTFLZ3g2VUh3dFhuejJsTGpEdzdBMGdjR0ZPNW9UYkQ1ekZHSWY5CkxUMEd3ZFhQWTdoWC9PVlVCMG9NaEZyK25sWW9adTBINUNGeUE3dnFCUVF5Qk5ZemxPSzdhN2R6YytyOEJER2IKSFZTNnA2SWR5czhwcU1TMzNqb2dtd0hRQnBDZWlGdXNyVkN2bndJREFRQUJvNElCSmpDQ0FTSXdEZ1lEVlIwUApBUUgvQkFRREFnZUFNQWtHQTFVZEV3UUNNQUF3RXdZRFZSMGxCQXd3Q2dZSUt3WUJCUVVIQXdFd0hRWURWUjBPCkJCWUVGQjhuRzVCa25Oc01NSlNvUGI1bkF6Nk5IanFhTUI4R0ExVWRJd1FZTUJhQUZPenc2bE5UUDU4ajNNRU8KTVJBM0I5N2U1Mjd6TUVVR0NDc0dBUVVGQndFQkJEa3dOekExQmdnckJnRUZCUWN3QW9ZcGFIUjBjRG92TDNCcgphUzVsYzI5a1pXMXZZWEJ3TWk1amIyMHZZMkV2Y205dmRDMWpZUzVqWlhJd09nWURWUjBmQkRNd01UQXZvQzJnCks0WXBhSFIwY0RvdkwzQnJhUzVsYzI5a1pXMXZZWEJ3TWk1amIyMHZZMkV2Y205dmRDMWpZUzVqY213d0xRWUQKVlIwUkJDWXdKSUlRYzNaak1TNWxlR0Z0Y0d4bExtTnZiWUlRYzNaak1pNWxlR0Z0Y0d4bExtTnZiVEFOQmdrcQpoa2lHOXcwQkFRc0ZBQU9DQVFFQXNCQjZrSVhrMVViaU92SWlCVlhXWkM4UmdwOVAvSVF1TWprOXZQL29CR1hDCkpKaU9mb3Z5T2JWS1RNNitSWDVHR0FWWmxjSVBVaG5iVDFscTljSXJWaXZkS1dIcC84eU04MkF2NXk1UGViZlAKSFNzelJIWmdTUElzKy9nbnNtQmxSUWh3R0tmaTc1ZmRESmlkYy9uaXlUcStGWmZ3NHBJdC9WV1ljbzlUZlJGaAozVGpCbTlCalY4djIxd0NsQUwzYytFUnA0U3MySUlSalVUanB1UktldU5CaDdmZnJ0b3BVV21SYnlNNVZPSW9nClVyVm0vVWZHME9yRFZQeVZwd2VwMXVKMFU5UDd0QWR3WFZUSlZ2b3FrL3FJcU5nYzFRWmJSRkNjY2J0QWlDc2wKaUZGYmRmNE9sdDJZMVhyV0tSSm1ER2dvU2dKTjEvTVNONms2ZVVIcWFBPT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQ== 4 | tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2Z0lCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktnd2dnU2tBZ0VBQW9JQkFRRFROV3lGbzBDbEFFdjgKTUhWd2FEN3hEa1FYZ2NmcXk3VFFVWC9UY1Fza3V4bFBnc21LUnZjanhZSGxzcWFRZ2dqdDFxU0czb1U5RXpSMgprZEhuOEJHdmlteHdFVlhFNERxTVlGYzBJWHYybWRXRzRCNmxRRk80dGNTQnBtOTJjelhoRHp3eXJzWUhFblhZCmRvelpNYnVCM1lrR0FKdS9VYmlpbGxEWkV6M2dZZHFmcGhXTGpkdC9ad0NTMFprakRCZTBnalY4dWs1WlRVcUQKSHBRZkMxZWZQYVV1TVBEc0RTQndZVTdtaE5zUG5NVVloLzB0UFFiQjFjOWp1RmY4NVZRSFNneUVXdjZlVmlobQo3UWZrSVhJRHUrb0ZCRElFMWpPVTRydHJ0M056NnZ3RU1ac2RWTHFub2gzS3p5bW94TGZlT2lDYkFkQUdrSjZJClc2eXRVSytmQWdNQkFBRUNnZ0VBVEx5VlVEeVBLU1BoZDZBWG14NlU5N29LTFV3KzJXVG5yZVJlZi9FTGJtMzMKOVRaOGlSdmRnUWFxZWswZFRWV2hidUJVYUpnQ2FyMUdpMm5Sak9aaFZwa0Jhdm94WWxWaGtFMFVnZUZFaThVNgpta1ZsZlA3UkxFUUdRR0M3RUpzdFVUYmExVU5BdWFNUVkvUTJtbGNDWEYya0FpVkljUXQ4L0wyR1pFZ2tiQmdZClY4aUx1OTJ1K0NYUmZvWkdUamRRZHAwN0tqVUVjZ2xYL3UxQlVESm9CMmY1U3A0SGsxV2dlUUgzTVdBL3J1WHkKVnB4MWNjNW9BNWhYSmpwcCsrY0J1aHlkb0tJWXlLQXlCa3N2cHRielhGRmI4QldiUWNKdVdROHR4aW1vTC9MTQppMkJTWUtibytRNXFISXI1WDEyeVhvTnpVb1JBbXBxTHd1eWJ6MGcyZ1FLQmdRRHkvNmZMQmErMkxERU5jV0xTCnFBeFhkNWNvYTVvQTFmdnlHNjFUc3hxSURQbnUrSTlKSkdaMzVEUytkUkt0ZDdtSlNZbEh1TmVock1kb0h3N1cKNXR1ek11V2xWTE1COVI4S2NtR1R5Tkc4NUtvR0U4ZmFJSzZKeE94OVJXOWxwdUYvcXBYekcveWdpL1BGN2s2LwoxWVdYZ21QWlBEaStTdDdJOGRaR1VlZUlRUUtCZ1FEZWdsZWp6dTNvdGZtS3RiLzE0TGl5NVhXWVdJQ1ljRHlGCkI2cTBSZTFOcjc2Znp1Q2xONWQ2eGJYampCckU1UzdWbnpVY0pnZjlQQy96UnRhdDF5bjJJK1VHS2RlNnFWcjAKZUtwbGZzNzRDT2RWZUwxYkRGWUZuTnJxS0EwQStwUnhBV2ZRNUpJTGdoZ1VyVHppbWh6K1oxMUkrUG1STnBvdwpMelVpRzlrLzN3S0JnUUNxNUpVU3VOc01HU1FlT2lHdjJMTmxTQnVzTjVCRkNqaDMybk1aTEJwNmwvV2wxSFNnCmtkTG1lajJGdkR2NGR2eXF5bWFiWkNseDhGc0VwT01BeTdheTNoYVhJV3dLOG1KMExHRG5XQkg0OEMrS3VwcVEKa3U5c3dHczQ1bjRqVlN1OVp6Q3htbnNlUlk2WklyR0xSQlVCcXBlaURUSXkwZURBSFNQK3JGZFNnUUtCZ0J1VQpOajZrZUZKK3M2WmdycUZRTURSa1FuWVdpaUhhejNXQnBNUGhZdTlkR0JpQXNORnBtWW5yRllkVnB6KytWRmEvCisvbzRNSWRQdlhXKzB2OExDelZwNzRvYkI1VW9ScERFb0FJaWZJNjdzOGloUlg4U2dWc0N2Uk9HMFUzTXdUZGMKZ0R5aENBVUxJK2IwTGZhOE9WbkRBQW42YWh4NGt4WGoxQWNkUW9rckFvR0JBS1RMdGxLNElnU29xMCs1T0RhWgpxK0dHUmNBNW02YWNkU1N3ZTZXbjAwaStxSCt4ODBaZW1oaXd2cnJyMkNHbU1WT3RVb0JnelB5MFJmOGV4YlhRCjRxTlU0VldtRmd5MmRlZmlTR3lOU2tjM05Qdi9QeU9DUndTM2NqUlVqbFJXYjJpYytHdTZ3TC9CcDZ2eThybWYKT3orMmJQUnRmdUVmOVZNOXI1VWl4aVlpCi0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0= 5 | kind: Secret 6 | metadata: 7 | name: istio-ingressgateway-certs 8 | namespace: istio-system 9 | type: kubernetes.io/tls 10 | --- -------------------------------------------------------------------------------- /jwt_client/generateToken.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "io/ioutil" 7 | "log" 8 | "net/http" 9 | "net/url" 10 | "os" 11 | "strings" 12 | 13 | "crypto/rsa" 14 | "crypto/x509" 15 | "encoding/json" 16 | "encoding/pem" 17 | "time" 18 | 19 | "golang.org/x/net/context" 20 | 21 | "golang.org/x/oauth2/google" 22 | 23 | "github.com/coreos/go-oidc" 24 | "github.com/golang/glog" 25 | "golang.org/x/oauth2/jws" 26 | ) 27 | 28 | /* 29 | Sample test client that generates a Google OIDC token or a self-signed JWT 30 | 31 | To use, download a google serviceAccount JSON file 32 | 33 | for OIDC: 34 | go run generateToken.go --mode=oidc --key=/path/to/svc_account.json --aud=http://foo.bar --jwkUrl=https://www.googleapis.com/oauth2/v3/certs --issuer=https://accounts.google.com --v=10 -alsologtostderr 35 | 36 | for JWT: 37 | SVC_ACCOUNT_EMAIL=`cat /home/srashid/gcp_misc/certs/mineral-minutia-820-83b3ce7dcddb.json | jq -r '.client_email'` 38 | export $SVC_ACCOUNT_EMAIL 39 | go run generateToken.go --mode=jwt --key=/path/to/svc_account.json --aud=http://foo.bar --jwkUrl=https://www.googleapis.com/service_accounts/v1/jwk/$SVC_ACCOUNT_EMAIL --issuer=$SVC_ACCOUNT_EMAIL --v=10 -alsologtostderr 40 | 41 | */ 42 | 43 | const ( 44 | metadataIdentityDocURL = "http://metadata/computeMetadata/v1/instance/service-accounts/default/identity" 45 | ) 46 | 47 | var ( 48 | cfg = &genConfig{} 49 | ) 50 | 51 | type genConfig struct { 52 | flmode string 53 | flkey string 54 | flaud string 55 | flissuer string 56 | fljwkUrl string 57 | } 58 | 59 | func getIDTokenFromServiceAccount(ctx context.Context, svcAccountkey string, audience string) (string, error) { 60 | data, err := ioutil.ReadFile(svcAccountkey) 61 | if err != nil { 62 | return "", err 63 | } 64 | 65 | conf, err := google.JWTConfigFromJSON(data, "") 66 | if err != nil { 67 | return "", err 68 | } 69 | 70 | header := &jws.Header{ 71 | Algorithm: "RS256", 72 | Typ: "JWT", 73 | KeyID: conf.PrivateKeyID, 74 | } 75 | 76 | privateClaims := map[string]interface{}{"target_audience": audience} 77 | iat := time.Now() 78 | exp := iat.Add(time.Hour) 79 | 80 | payload := &jws.ClaimSet{ 81 | Iss: conf.Email, 82 | Iat: iat.Unix(), 83 | Exp: exp.Unix(), 84 | Aud: "https://www.googleapis.com/oauth2/v4/token", 85 | PrivateClaims: privateClaims, 86 | } 87 | 88 | key := conf.PrivateKey 89 | block, _ := pem.Decode(key) 90 | if block != nil { 91 | key = block.Bytes 92 | } 93 | parsedKey, err := x509.ParsePKCS8PrivateKey(key) 94 | if err != nil { 95 | parsedKey, err = x509.ParsePKCS1PrivateKey(key) 96 | if err != nil { 97 | return "", err 98 | } 99 | } 100 | parsed, ok := parsedKey.(*rsa.PrivateKey) 101 | if !ok { 102 | log.Fatal("private key is invalid") 103 | } 104 | 105 | token, err := jws.Encode(header, payload, parsed) 106 | if err != nil { 107 | return "", err 108 | } 109 | 110 | d := url.Values{} 111 | d.Set("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer") 112 | d.Add("assertion", token) 113 | 114 | client := &http.Client{} 115 | req, err := http.NewRequest("POST", "https://www.googleapis.com/oauth2/v4/token", strings.NewReader(d.Encode())) 116 | if err != nil { 117 | return "", err 118 | } 119 | req.Header.Add("Content-Type", "application/x-www-form-urlencoded") 120 | 121 | resp, err := client.Do(req) 122 | if err != nil { 123 | return "", err 124 | } 125 | defer resp.Body.Close() 126 | 127 | body, err := ioutil.ReadAll(resp.Body) 128 | if err != nil { 129 | return "", err 130 | } 131 | 132 | var y map[string]interface{} 133 | err = json.Unmarshal([]byte(body), &y) 134 | if err != nil { 135 | return "", err 136 | } 137 | return y["id_token"].(string), nil 138 | } 139 | 140 | func getIDTokenFromComputeEngine(ctx context.Context, audience string) (string, error) { 141 | client := &http.Client{} 142 | req, err := http.NewRequest("GET", metadataIdentityDocURL+"?audience="+audience, nil) 143 | req.Header.Add("Metadata-Flavor", "Google") 144 | resp, err := client.Do(req) 145 | if err != nil { 146 | return "", err 147 | } 148 | defer resp.Body.Close() 149 | 150 | bodyBytes, err := ioutil.ReadAll(resp.Body) 151 | if err != nil { 152 | return "", err 153 | } 154 | 155 | bodyString := string(bodyBytes) 156 | return bodyString, nil 157 | } 158 | 159 | func verifyGoogleIDToken(ctx context.Context, jwkUrl string, aud string, issuer string, token string) (bool, error) { 160 | 161 | keySet := oidc.NewRemoteKeySet(ctx, jwkUrl) 162 | 163 | // https://github.com/coreos/go-oidc/blob/master/verify.go#L36 164 | var config = &oidc.Config{ 165 | SkipClientIDCheck: false, 166 | ClientID: aud, 167 | } 168 | verifier := oidc.NewVerifier(issuer, keySet, config) 169 | idt, err := verifier.Verify(ctx, token) 170 | if err != nil { 171 | return false, err 172 | } 173 | glog.V(2).Infof("Verified id_token with Issuer %v: ", idt.Issuer) 174 | return true, nil 175 | } 176 | 177 | func getJWTTokenFromServiceAccount(ctx context.Context, svcAccountkey string, audience string) (string, error) { 178 | data, err := ioutil.ReadFile(svcAccountkey) 179 | if err != nil { 180 | return "", err 181 | } 182 | 183 | conf, err := google.JWTConfigFromJSON(data, "") 184 | if err != nil { 185 | return "", err 186 | } 187 | 188 | header := &jws.Header{ 189 | Algorithm: "RS256", 190 | Typ: "JWT", 191 | KeyID: conf.PrivateKeyID, 192 | } 193 | 194 | privateClaims := map[string]interface{}{"some_claim": "some_value"} 195 | iat := time.Now() 196 | exp := iat.Add(time.Hour) 197 | 198 | payload := &jws.ClaimSet{ 199 | Iss: conf.Email, 200 | Iat: iat.Unix(), 201 | Exp: exp.Unix(), 202 | Aud: audience, 203 | PrivateClaims: privateClaims, 204 | } 205 | 206 | key := conf.PrivateKey 207 | block, _ := pem.Decode(key) 208 | if block != nil { 209 | key = block.Bytes 210 | } 211 | parsedKey, err := x509.ParsePKCS8PrivateKey(key) 212 | if err != nil { 213 | parsedKey, err = x509.ParsePKCS1PrivateKey(key) 214 | if err != nil { 215 | return "", err 216 | } 217 | } 218 | parsed, ok := parsedKey.(*rsa.PrivateKey) 219 | if !ok { 220 | log.Fatal("private key is invalid") 221 | } 222 | 223 | token, err := jws.Encode(header, payload, parsed) 224 | if err != nil { 225 | return "", err 226 | } 227 | return token, nil 228 | } 229 | 230 | func init() { 231 | flag.StringVar(&cfg.flmode, "mode", "oidc", "(required) mode (oidc|jwt) ") 232 | flag.StringVar(&cfg.flkey, "key", "", "(required) privateKey") 233 | flag.StringVar(&cfg.flaud, "aud", "", "(required) audience") 234 | flag.StringVar(&cfg.flissuer, "issuer", "https://accounts.google.com", "issuer") 235 | flag.StringVar(&cfg.fljwkUrl, "jwkUrl", "https://www.googleapis.com/oauth2/v3/certs", "JWK url to verify") 236 | 237 | flag.Parse() 238 | 239 | argError := func(s string, v ...interface{}) { 240 | glog.V(2).Infof("Invalid Argument error: "+s, v...) 241 | os.Exit(-1) 242 | } 243 | 244 | if cfg.flmode != "oidc" && cfg.flmode != "jwt" { 245 | argError("-mode must be either oidc or jwt") 246 | } 247 | 248 | if cfg.flkey == "" { 249 | argError("-key not specified") 250 | } 251 | 252 | if cfg.flaud == "" { 253 | argError("-aud not specified") 254 | } 255 | 256 | } 257 | 258 | func main() { 259 | 260 | ctx := context.Background() 261 | 262 | if cfg.flmode == "oidc" { 263 | // For Service Account 264 | idToken, err := getIDTokenFromServiceAccount(ctx, cfg.flkey, cfg.flaud) 265 | 266 | if err != nil { 267 | glog.Fatalf("%v", err) 268 | } 269 | 270 | fmt.Printf("%s\n", idToken) 271 | 272 | verified, err := verifyGoogleIDToken(ctx, cfg.fljwkUrl, cfg.flaud, cfg.flissuer, idToken) 273 | if err != nil { 274 | log.Fatalf("%v", err) 275 | } 276 | glog.V(2).Infof("Verify %v", verified) 277 | } else if cfg.flmode == "jwt" { 278 | 279 | idToken, err := getJWTTokenFromServiceAccount(ctx, cfg.flkey, cfg.flaud) 280 | 281 | if err != nil { 282 | glog.Fatalf("%v", err) 283 | } 284 | 285 | fmt.Printf("%s\n", idToken) 286 | 287 | verified, err := verifyGoogleIDToken(ctx, cfg.fljwkUrl, cfg.flaud, cfg.flissuer, idToken) 288 | if err != nil { 289 | log.Fatalf("%v", err) 290 | } 291 | glog.V(2).Infof("Verify %v", verified) 292 | } 293 | 294 | } 295 | -------------------------------------------------------------------------------- /jwt_client/go.mod: -------------------------------------------------------------------------------- 1 | module main 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/coreos/go-oidc v2.2.1+incompatible 7 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b 8 | github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect 9 | golang.org/x/net v0.0.0-20200301022130-244492dfa37a 10 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d 11 | gopkg.in/square/go-jose.v2 v2.4.1 // indirect 12 | ) 13 | -------------------------------------------------------------------------------- /jwt_client/go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.34.0 h1:eOI3/cP2VTU6uZLDYAoic+eyzzB9YyGmJ7eIjl8rOPg= 2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | github.com/coreos/go-oidc v2.2.1+incompatible h1:mh48q/BqXqgjVHpy2ZY7WnWAbenxRjsz9N1i1YxjHAk= 4 | github.com/coreos/go-oidc v2.2.1+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= 5 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= 6 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 7 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 8 | github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 h1:J9b7z+QKAmPf4YLrFg6oQUotqHQeUNWwkvo7jZp1GLU= 9 | github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA= 10 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= 11 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 12 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 13 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 14 | golang.org/x/net v0.0.0-20200301022130-244492dfa37a h1:GuSPYbZzB5/dcLNCwLQLsg3obCJtX9IJhpXkvY7kzk0= 15 | golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 16 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw= 17 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 18 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 19 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 20 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 21 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 22 | gopkg.in/square/go-jose.v2 v2.4.1 h1:H0TmLt7/KmzlrDOpa1F+zr0Tk90PbJYBfsVUmRLrf9Y= 23 | gopkg.in/square/go-jose.v2 v2.4.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= 24 | --------------------------------------------------------------------------------