diff --git a/bookinfo-example/.gitignore b/bookinfo-example/.gitignore new file mode 100644 index 00000000..9622ab76 --- /dev/null +++ b/bookinfo-example/.gitignore @@ -0,0 +1,2 @@ +key.pem +cert.pem diff --git a/bookinfo-example/README.md b/bookinfo-example/README.md index 7eece42d..0bb45f34 100644 --- a/bookinfo-example/README.md +++ b/bookinfo-example/README.md @@ -4,42 +4,85 @@ This doc shows how to integrate Authservice into an Istio system deployed on Kub This demo uses the [Istio Bookinfo sample application](https://istio.io/docs/examples/bookinfo/). -This demo takes advantage of an Istio feature set that gives the ability to inject http filters on -Sidecars. This feature set was released in Istio 1.3.0. +This demo takes relies on Istio [external authorization provider](https://istio.io/latest/docs/tasks/security/authorization/authz-custom/), released since 1.9. -Things needed before starting: - -- A Kubernetes cluster that is compatible with Istio 1.3 or newer -- An OIDC provider configured to support Authorization Code grant type. The urls and credentials for this -provider will be needed to configure Authservice. - ### Pre-requisites: -1. Download Istio 1.9 or greater. For example: +1. Prepare your OIDC provider configuration. In our example, we use Google as identity provider. +Follow [instructions](https://developers.google.com/identity/protocols/oauth2/openid-connect) to +create one. - [`scripts/download-istio-1.4.sh`](scripts/download-istio-1.4.sh) + ```shell + export OIDC_CLIENT_ID="" + export OIDC_CLIENT_SECRET="" + ``` -1. Install Istio and enable sidecar injection for the namespace where services will be deployed. For example, - one could install the Istio demo like this: +1. Install Istio 1.9 or later. - [`scripts/install-istio.sh`](scripts/install-istio.sh) - -1. If certs signed by a known CA cannot be obtained, generate self signed certs for the ingress gateway. For example: + ```shell + istioctl install -y + kubectl label namespace default istio-injection=enabled --overwrite + ``` - [`scripts/generate-self-signed-certs-for-ingress-gateway.sh`](scripts/generate-self-signed-certs-for-ingress-gateway.sh) +1. In our example, we use a self signed certificate at localhost for easy setup. +This is used to terminate HTTPS at the ingress gateway since OIDC requires client callback +URI to be hosted on a protected endpoint. -1. Configure the Istio mesh config with an [external authorization provider](https://istio.io/latest/docs/tasks/security/authorization/authz-custom/) as `sample-ext-authz-grpc`. + ```shell + bash ./scripts/generate-self-signed-certs-for-ingress-gateway.sh + ``` + +1. Configure the Istio mesh config with an [external authorization provider](https://istio.io/latest/docs/tasks/security/authorization/authz-custom/). + + ```shell + kubectl edit cm -n istio-system + ``` + + Change the mesh config with the config below. ```yaml data: mesh: |- extensionProviders: - - name: "sample-ext-authz-grpc" + - name: "authservice-grpc" envoyExtAuthzGrpc: - service: ext.authz.local - port: "10003" + service: authservice.default.svc.cluster.local + port: "10003" + ``` + +1. Install authservice via Helm. + + ```shell + helm template authservice \ + --set oidc.clientID=${OIDC_CLIENT_ID} \ + --set oidc.clientSecret=${OIDC_CLIENT_SECRET} \ + | kubectl apply -f - + ``` + +1. Access product page via port-forwarding at local host. + + ```shell + kubectl port-forward service/istio-ingressgateway 8443:443 -n istio-system ``` + At your browser visit the page at https://localhost:8443/productpage. + +### Further Protect via RequestAuthentication and Authorization Policy + +Istio native RequestAuthentication and Authorization policy can be used configure which end user +can access specific apps, at specific paths. For example, you can apply the sample configuration +to only allow authenticated request to access productpage service. + +```shell +kubectl apply -f ./config/productpage-authn-authz.yaml +``` + +## Configure OIDC flow at Ingress Gateway + +TODO(incfly): write it up with sample config and setup. + +## :warning: The REST documnetation needs updates. + ## Deploy Bookinfo Using the Authservice for Token Acquisition (Sidecar integration) The goal of these steps is the protect the `productpage` service with OIDC authentication provided by the mesh. diff --git a/bookinfo-example/authservice/.helmignore b/bookinfo-example/authservice/.helmignore new file mode 100644 index 00000000..0e8a0eb3 --- /dev/null +++ b/bookinfo-example/authservice/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/bookinfo-example/authservice/Chart.yaml b/bookinfo-example/authservice/Chart.yaml new file mode 100644 index 00000000..f0dfc75d --- /dev/null +++ b/bookinfo-example/authservice/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: authservice +description: A Helm chart for Istio Authservice. + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "0.4.1" diff --git a/bookinfo-example/authservice/templates/config.yaml b/bookinfo-example/authservice/templates/config.yaml new file mode 100644 index 00000000..e8bbdda8 --- /dev/null +++ b/bookinfo-example/authservice/templates/config.yaml @@ -0,0 +1,48 @@ +# +# A ConfigMap which contains the configuration of the authservice. +# In bookinfo-with-authservice-template.yaml the authservice container is created +# with this ConfigMap volume mounted inside the container at /etc/authservice, which +# is the location where the authservice expects the file to exist. +# + +--- +kind: ConfigMap +apiVersion: v1 +metadata: + name: authservice +data: + config.json: | + { + "listen_address": "127.0.0.1", + "listen_port": "10003", + "log_level": "trace", + "threads": 8, + "chains": [ + { + "name": "idp_filter_chain", + "filters": [ + { + "oidc": + { + "authorization_uri": "{{ .Values.oidc.authorizationURI }}", + "token_uri": "{{ .Values.oidc.tokenURI }} ", + "callback_uri": "https://localhost:8443/productpage/oauth/callback", + "jwks": "{ \"keys\": [ { \"use\": \"sig\", \"alg\": \"RS256\", \"n\": \"7qnlkR2Ysvik__jqELu5__2Ib4_Pix6NEmEYKY80NyIGBhUQ0QDtijFypOk3cN3aRgb1f3741vQu7PQGMr79J8jM4-sA1A6UQNmfjl-thB5JpdfQrS1n3EpsrPMUvf5w-uBMQnxmiM3hrHgjA107-UxLF_xBG8Vp_EXmZI7y6IfUwTHrNotSpLLBSNH77C8ncFcm9ADsdl-Bav2CjOaef6CpGISCscx2T4LZS6DIafU1M_xYcx3aLET9TojymjZJi2hfZDyF9x_qssrlnxqfgrI71warY8HiXsiZzOTNB6s81Fu9AaxV7YckfLHyvXwOX8lQN53c2IiAuk-T7nf69w\", \"e\": \"AQAB\", \"kty\": \"RSA\", \"kid\": \"0fcc014f22934e47480daf107a340c22bd262b6c\" }, { \"alg\": \"RS256\", \"e\": \"AQAB\", \"kid\": \"462949174f1eedf4f9f9434877be483b324140f5\", \"kty\": \"RSA\", \"n\": \"2BHFUUq8NqZ3pxxi_RJcSIMG5nJoZQ8Nbvf-lW5o7hJ9CmLA4SeUmDL2IVK6CSuskTPj_ohAp_gtOg3PCJvn33grPoJQu38MoMB8kDqA4U-u3A86GGEjWtk6LPo7dEkojZNQkzhZCnEMTuRMtBZXsLWNGJpY3UADA3rxnHnBP1wrSt27iXIE0C6-1N5z00R13r3L0aWC0MuAUgjI2H4dGMr8B3niJ-NjOVPCwG7xSWsCwsSitAuhPGHaDtenB23ZsFJjbuTuiguoSJ9A1qo9kzBOg32xda4derbWasu7Tk8p53PFxXDJGR_h7dM-nsJHl7lAUDqL8zOrf9XXlPTjwQ\", \"use\": \"sig\" } ] }", + "client_id": "{{ .Values.oidc.clientID }}", + "client_secret": "{{ .Values.oidc.clientSecret }}", + "scopes": [], + "cookie_name_prefix": "productpage", + "id_token": { + "preamble": "Bearer", + "header": "Authorization" + }, + "logout": { + "path": "/authservice_logout", + "redirect_uri": "https://localhost:8443/some/logout/path" + } + } + } + ] + } + ] + } diff --git a/bookinfo-example/authservice/templates/deployment.yaml b/bookinfo-example/authservice/templates/deployment.yaml new file mode 100644 index 00000000..663bd9b4 --- /dev/null +++ b/bookinfo-example/authservice/templates/deployment.yaml @@ -0,0 +1,45 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: authservice + # TODO(incfly): change to istio-system when the config map is also updated to that namespace. + # namespace: istio-system + labels: + app: authservice +spec: + replicas: 1 # you can scale up productpage as long as session affinity is enabled via a DestinationRule (see ./config/bookinfo-gateway.yaml) + selector: + matchLabels: + app: authservice + template: + metadata: + labels: + app: authservice + spec: + volumes: + - name: authservice-config # declare the volume containing the authservice ConfigMap + configMap: + name: authservice + containers: + - name: authservice # authservice needs to be deployed in the sample Pod as the productpage + image: {{ .Values.authservice.image }} + imagePullPolicy: Always + ports: + - containerPort: 10003 + volumeMounts: + - name: authservice-config # mount the volume containing the authservice ConfigMap + mountPath: /etc/authservice +--- +apiVersion: v1 +kind: Service +metadata: + name: authservice + # namespace: istio-system + labels: + app: authservice +spec: + ports: + - port: 10003 + name: grpc + selector: + app: authservice diff --git a/bookinfo-example/authservice/templates/ext-authz.yaml b/bookinfo-example/authservice/templates/ext-authz.yaml new file mode 100644 index 00000000..318660ab --- /dev/null +++ b/bookinfo-example/authservice/templates/ext-authz.yaml @@ -0,0 +1,34 @@ +apiVersion: security.istio.io/v1beta1 +kind: AuthorizationPolicy +metadata: + name: ext-authz +spec: + selector: + matchLabels: + app: productpage + action: CUSTOM + provider: + # The provider name must match the extension provider defined in the mesh config. + name: authservice-grpc + rules: + - to: + - operation: + notPaths: ["/public"] # enable all except /public paths. +--- +# TODO(incfly): enable if else check for including this only for sidecar mode. +# Istio requires the external authz provider to be available in the service registry. +# See https://github.com/istio/istio/issues/34622. +apiVersion: networking.istio.io/v1beta1 +kind: ServiceEntry +metadata: + name: authz-svc +spec: + hosts: + - ext.authz.local # not used + ports: + - number: 10003 + name: grpc-ext + protocol: grpc + resolution: STATIC + endpoints: + - address: 127.0.0.1 \ No newline at end of file diff --git a/bookinfo-example/authservice/templates/gateway.yaml b/bookinfo-example/authservice/templates/gateway.yaml new file mode 100644 index 00000000..f54a545f --- /dev/null +++ b/bookinfo-example/authservice/templates/gateway.yaml @@ -0,0 +1,71 @@ +# +# A simple example of a gateway for the bookinfo app. +# + +--- +apiVersion: networking.istio.io/v1alpha3 +kind: Gateway +metadata: + name: bookinfo-gateway +spec: + selector: + istio: ingressgateway # use istio default controller + servers: + - port: + number: 443 + name: https-443 + protocol: HTTPS + hosts: + - "*" + tls: + mode: SIMPLE + credentialName: ingress-tls-cert +--- +apiVersion: networking.istio.io/v1alpha3 +kind: VirtualService +metadata: + name: bookinfo +spec: + hosts: + - "*" + gateways: + - bookinfo-gateway + http: + - match: + - uri: + # Allow the Authentication Request Callback to get routed to productpage so it can be intercepted by the authservice + prefix: /productpage/oauth + - uri: + # Allow the authservice logout request to get routed to productpage so it can be intercepted by the authservice + exact: /authservice_logout + - uri: + exact: /productpage + - uri: + prefix: /static + - uri: + exact: /login + - uri: + exact: /logout + - uri: + prefix: /api/v1/ + route: + - destination: + host: productpage + port: + number: 9080 +--- +# Add a DestinationRule to enable session affinity, which ensures that the requests from the same user-agent reach +# the same instance of productpage, and hence, the same instance of Sidecar and Authservice. This is required when you +# deploy multiple instances of productpage because Authservice currently only supports in-memory session storage. +apiVersion: networking.istio.io/v1alpha3 +kind: DestinationRule +metadata: + name: bookinfo-dest-rule +spec: + host: productpage.default.svc.cluster.local + trafficPolicy: + loadBalancer: + consistentHash: + httpCookie: + name: bookinfo-session-affinity-cookie + ttl: 0s \ No newline at end of file diff --git a/bookinfo-example/authservice/values.yaml b/bookinfo-example/authservice/values.yaml new file mode 100644 index 00000000..0c23aaef --- /dev/null +++ b/bookinfo-example/authservice/values.yaml @@ -0,0 +1,13 @@ +authservice: + # TODO(incfly): change to a proper project wide container registry. + image: gcr.io/jianfeih-images-pub/authservice/authservice:0.4.1 + +oidc: + idpURL: https://account.google.com + authorizationURI: "https://accounts.google.com/o/oauth2/v2/auth" + tokenURI: "https://oauth2.googleapis.com/token" + clientID: your-client-id + clientSecret: your-client-secret + # JSON string containing the identity provider's public key for validating id token. + # jwks: "<>" + diff --git a/bookinfo-example/config/authservice-standalone.yaml b/bookinfo-example/config/authservice-standalone.yaml new file mode 100644 index 00000000..0da81e91 --- /dev/null +++ b/bookinfo-example/config/authservice-standalone.yaml @@ -0,0 +1,47 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: authservice + # TODO(incfly): change to istio-system when the config map is also updated to that namespace. + # namespace: istio-system + labels: + app: authservice +spec: + replicas: 1 # you can scale up productpage as long as session affinity is enabled via a DestinationRule (see ./config/bookinfo-gateway.yaml) + selector: + matchLabels: + app: authservice + template: + metadata: + labels: + app: authservice + spec: + serviceAccountName: bookinfo-productpage + volumes: + - name: bookinfo-authservice-configmap-volume # declare the volume containing the authservice ConfigMap + configMap: + name: bookinfo-authservice-configmap + containers: + - name: authservice # authservice needs to be deployed in the sample Pod as the productpage + # TODO(incfly): change to a proper project wide container registry. + image: gcr.io/jianfeih-images-pub/authservice/authservice:0.4.1 # Manually docker pull the latest authservice image from https://github.com/istio-ecosystem/authservice/packages and push it to your own image registry (e.g. Docker Hub), and use it here. (The Github Package Registry does not work with k8s yet until this issue is fixed and released: https://github.com/kubernetes-sigs/kind/issues/870) + imagePullPolicy: Always + ports: + - containerPort: 10003 + volumeMounts: + - name: bookinfo-authservice-configmap-volume # mount the volume containing the authservice ConfigMap + mountPath: /etc/authservice +--- +apiVersion: v1 +kind: Service +metadata: + name: authservice + # namespace: istio-system + labels: + app: authservice +spec: + ports: + - port: 10003 + name: grpc + selector: + app: authservice \ No newline at end of file diff --git a/bookinfo-example/config/ext-authz-productpage.yaml b/bookinfo-example/config/ext-authz-productpage.yaml new file mode 100644 index 00000000..8ee25852 --- /dev/null +++ b/bookinfo-example/config/ext-authz-productpage.yaml @@ -0,0 +1,34 @@ +apiVersion: security.istio.io/v1beta1 +kind: AuthorizationPolicy +metadata: + name: ext-authz +spec: + selector: + matchLabels: + app: productpage + action: CUSTOM + provider: + # The provider name must match the extension provider defined in the mesh config. + # You can also replace this with sample-ext-authz-http to test the other external authorizer definition. + name: sample-ext-authz-grpc + rules: + - to: + - operation: + notPaths: ["/public"] # enable all except /public paths. +--- +# Istio requires the external authz provider to be available in the service registry. +# See https://github.com/istio/istio/issues/34622. +apiVersion: networking.istio.io/v1beta1 +kind: ServiceEntry +metadata: + name: authz-svc +spec: + hosts: + - ext.authz.local # not used + ports: + - number: 10003 + name: grpc-ext + protocol: grpc + resolution: STATIC + endpoints: + - address: 127.0.0.1 \ No newline at end of file diff --git a/bookinfo-example/config/productpage-authn-authz.yaml b/bookinfo-example/config/productpage-authn-authz.yaml new file mode 100644 index 00000000..ef44f6a3 --- /dev/null +++ b/bookinfo-example/config/productpage-authn-authz.yaml @@ -0,0 +1,27 @@ +apiVersion: security.istio.io/v1beta1 +kind: RequestAuthentication +metadata: + name: productpage-auth +spec: + selector: + matchLabels: + app: productpage + jwtRules: + - issuer: "https://accounts.google.com" + jwksUri: "https://www.googleapis.com/oauth2/v3/certs" + forwardOriginalToken: true +--- +apiVersion: security.istio.io/v1beta1 +kind: AuthorizationPolicy +metadata: + name: productpage-auth +spec: + selector: + matchLabels: + app: productpage + action: ALLOW + rules: + - when: + - key: request.auth.claims[iss] + values: + - "https://accounts.google.com" diff --git a/src/filters/filter_chain.cc b/src/filters/filter_chain.cc index 3139c3d6..e3a61365 100644 --- a/src/filters/filter_chain.cc +++ b/src/filters/filter_chain.cc @@ -66,11 +66,11 @@ std::unique_ptr FilterChainImpl::New() { } auto jwks_keys = google::jwt_verify::Jwks::createFrom( - filter.oidc().jwks(), google::jwt_verify::Jwks::Type::JWKS); + filter.oidc().jwks(), google::jwt_verify::Jwks::Type::JWKS); spdlog::debug("status for jwks parsing: {}, {}", __func__, - google::jwt_verify::getStatusString(jwks_keys->getStatus())); - auto token_request_parser = std::make_shared( - std::move(jwks_keys)); + google::jwt_verify::getStatusString(jwks_keys->getStatus())); + auto token_request_parser = + std::make_shared(std::move(jwks_keys)); auto session_string_generator = std::make_shared();