Enable the use of istio for the monitoring setup

The monitoring setup was only usable with an ingress controller like
Nginx. However, if being used in a cluster running one or multiple
Gerrit instances, it might be worth considering a service mesh to have
better control of the network traffic. In this case, it would be worth
considering also integrating the monitoring setup with the service
mesh to avoid multiple load balancers.

This change adds the resources to configure the setup to run in an
istio service mesh. Istio is already expected to be installed.
The ports 80 and 443 have to be enabled in the istio-ingressgateway.

If configured the setup will then set up a gateway, which allows
access to Grafana. The access to Prometheus and Loki is only possible
with a JWT-token. This was done, since istio does not provide basic
authentication like the NGINX ingress controller and neither do
Prometheus or Loki.

The installer will automatically put a bearer token into the Promtail
configurations.

If a key is required, the gerrit-monitoring.py -c <config> jwt command
can be used to get one.

At the moment JWT tokens do not expire, since there is currently no
mechanism supported to automatically cycle the tokens. This may be
added in a later step.

Change-Id: Ic6542a3bdfdbcaa63552d0e049d62e12d1f8acaf
diff --git a/.pylintrc b/.pylintrc
index 3322b5d..87736a7 100644
--- a/.pylintrc
+++ b/.pylintrc
@@ -1,5 +1,5 @@
 [MESSAGES CONTROL]
-disable=C0330, C0114, R0903
+disable=C0330, C0114, R0903, W0511
 
 [BASIC]
 no-docstring-rgx=(__.*__)|(_.*)
diff --git a/Pipfile.lock b/Pipfile.lock
index f287e90..333e8ad 100644
--- a/Pipfile.lock
+++ b/Pipfile.lock
@@ -215,6 +215,32 @@
             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
             "version": "==1.2.14"
         },
+        "cryptography": {
+            "hashes": [
+                "sha256:0f1212a66329c80d68aeeb39b8a16d54ef57071bf22ff4e521657b27372e327d",
+                "sha256:1e056c28420c072c5e3cb36e2b23ee55e260cb04eee08f702e0edfec3fb51959",
+                "sha256:240f5c21aef0b73f40bb9f78d2caff73186700bf1bc6b94285699aff98cc16c6",
+                "sha256:26965837447f9c82f1855e0bc8bc4fb910240b6e0d16a664bb722df3b5b06873",
+                "sha256:37340614f8a5d2fb9aeea67fd159bfe4f5f4ed535b1090ce8ec428b2f15a11f2",
+                "sha256:3d10de8116d25649631977cb37da6cbdd2d6fa0e0281d014a5b7d337255ca713",
+                "sha256:3d8427734c781ea5f1b41d6589c293089704d4759e34597dce91014ac125aad1",
+                "sha256:7ec5d3b029f5fa2b179325908b9cd93db28ab7b85bb6c1db56b10e0b54235177",
+                "sha256:8e56e16617872b0957d1c9742a3f94b43533447fd78321514abbe7db216aa250",
+                "sha256:de4e5f7f68220d92b7637fc99847475b59154b7a1b3868fb7385337af54ac9ca",
+                "sha256:eb8cc2afe8b05acbd84a43905832ec78e7b3873fb124ca190f574dca7389a87d",
+                "sha256:ee77aa129f481be46f8d92a1a7db57269a2f23052d5f2433b4621bb457081cc9"
+            ],
+            "markers": "python_version >= '3.6'",
+            "version": "==3.4.7"
+        },
+        "deprecated": {
+            "hashes": [
+                "sha256:08452d69b6b5bc66e8330adde0a4f8642e969b9e1702904d137eeb29c8ffc771",
+                "sha256:6d2de2de7931a968874481ef30208fd4e08da39177d61d3d4ebdf4366e7dbca1"
+            ],
+            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
+            "version": "==1.2.12"
+        },
         "idna": {
             "hashes": [
                 "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4",
@@ -237,6 +263,13 @@
             "markers": "python_version >= '3.6'",
             "version": "==1.5.0"
         },
+        "jwcrypto": {
+            "hashes": [
+                "sha256:12976a09895ec0076ce17c49ab7be64d6e63bcd7fd9a773e3fedf8011537a5f6",
+                "sha256:63531529218ba9869e14ef8c9e7b516865ede3facf9b0ef3d3ba68014da211f9"
+            ],
+            "version": "==0.9.1"
+        },
         "passlib": {
             "hashes": [
                 "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1",
@@ -332,6 +365,14 @@
             "index": "pypi",
             "version": "==2.31.0"
         },
+        "six": {
+            "hashes": [
+                "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
+                "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
+            ],
+            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
+            "version": "==1.16.0"
+        },
         "urllib3": {
             "hashes": [
                 "sha256:7a7c7003b000adf9e7ca2a377c9688bbc54ed41b985789ed576570342a375cd2",
diff --git a/README.md b/README.md
index 08bfa96..60e1cec 100644
--- a/README.md
+++ b/README.md
@@ -106,43 +106,49 @@
 These options have to be configured in the `./config.yaml` before installing and
 are listed here:
 
-| option                                             | description                                                                            |
-|----------------------------------------------------|----------------------------------------------------------------------------------------|
-| `gerritServers`                                    | List of Gerrit servers to scrape. For details refer to section [below](#gerritServers) |
-| `namespace`                                        | The namespace the charts are installed to                                              |
-| `tls.skipVerify`                                   | Whether to skip TLS certificate verification                                           |
-| `tls.caCert`                                       | CA certificate used for TLS certificate verification                                   |
-| `monitoring.prometheus.server.host`                | Prometheus server ingress hostname                                                     |
-| `monitoring.prometheus.server.username`            | Username for Prometheus                                                                |
-| `monitoring.prometheus.server.password`            | Password for Prometheus                                                                |
-| `monitoring.prometheus.server.tls.cert`            | TLS certificate                                                                        |
-| `monitoring.prometheus.server.tls.key`             | TLS key                                                                                |
-| `monitoring.prometheus.alertmanager.slack.apiUrl`  | API URL of the Slack Webhook                                                           |
-| `monitoring.prometheus.alertmanager.slack.channel` | Channel to which the alerts should be posted                                           |
-| `monitoring.grafana.host`                          | Grafana ingress hostname                                                               |
-| `monitoring.grafana.tls.cert`                      | TLS certificate                                                                        |
-| `monitoring.grafana.tls.key`                       | TLS key                                                                                |
-| `monitoring.grafana.admin.username`                | Username for the admin user                                                            |
-| `monitoring.grafana.admin.password`                | Password for the admin user                                                            |
-| `monitoring.grafana.ldap.enabled`                  | Whether to enable LDAP                                                                 |
-| `monitoring.grafana.ldap.host`                     | Hostname of LDAP server                                                                |
-| `monitoring.grafana.ldap.port`                     | Port of LDAP server (Has to be `quoted`!)                                              |
-| `monitoring.grafana.ldap.password`                 | Password of LDAP server                                                                |
-| `monitoring.grafana.ldap.bind_dn`                  | Bind DN (username) of the LDAP server                                                  |
-| `monitoring.grafana.ldap.accountBases`             | List of base DNs to discover accounts (Has to have the format `"['a', 'b']"`)          |
-| `monitoring.grafana.ldap.groupBases`               | List of base DNs to discover groups (Has to have the format `"['a', 'b']"`)            |
-| `monitoring.grafana.dashboards.editable`           | Whether dashboards can be edited manually in the UI                                    |
-| `logging.loki.host`                                | Loki ingress hostname                                                                  |
-| `logging.loki.username`                            | Username for Loki                                                                      |
-| `logging.loki.password`                            | Password for Loki                                                                      |
-| `logging.loki.s3.protocol`                         | Protocol used for communicating with S3                                                |
-| `logging.loki.s3.host`                             | Hostname of the S3 object store                                                        |
-| `logging.loki.s3.accessToken`                      | The EC2 accessToken used for authentication with S3                                    |
-| `logging.loki.s3.secret`                           | The secret associated with the accessToken                                             |
-| `logging.loki.s3.bucket`                           | The name of the S3 bucket                                                              |
-| `logging.loki.s3.region`                           | The region in which the S3 bucket is hosted                                            |
-| `logging.loki.tls.cert`                            | TLS certificate                                                                        |
-| `logging.loki.tls.key`                             | TLS key                                                                                |
+| option                                             | description                                                                             |
+|----------------------------------------------------|-----------------------------------------------------------------------------------------|
+| `gerritServers`                                    | List of Gerrit servers to scrape. For details refer to section [below](#gerritServers)  |
+| `namespace`                                        | The namespace the charts are installed to                                               |
+| `tls.skipVerify`                                   | Whether to skip TLS certificate verification                                            |
+| `tls.caCert`                                       | CA certificate used for TLS certificate verification                                    |
+| `istio.enabled`                                    | Whether to use istio                                                                    |
+| `istio.crt`                                        | TLS cert for Ingress gateway (should have alternative names for URLs of all components) |
+| `istio.key`                                        | TLS key for Ingress gateway                                                             |
+| `istio.jwt.cert`                                   | RSA certificate to be used to create JWT tokens                                         |
+| `istio.jwt.key`                                    | RSA key to be used to create JWT tokens                                                 |
+| `istio.jwt.issuer`                                 | Issuer to be used for tokens (e.g. an email address)                                    |
+| `monitoring.prometheus.server.host`                | Prometheus server ingress hostname                                                      |
+| `monitoring.prometheus.server.username`            | Username for Prometheus (only required if not using istio)                              |
+| `monitoring.prometheus.server.password`            | Password for Prometheus (only required if not using istio)                              |
+| `monitoring.prometheus.server.tls.cert`            | TLS certificate                                                                         |
+| `monitoring.prometheus.server.tls.key`             | TLS key                                                                                 |
+| `monitoring.prometheus.alertmanager.slack.apiUrl`  | API URL of the Slack Webhook                                                            |
+| `monitoring.prometheus.alertmanager.slack.channel` | Channel to which the alerts should be posted                                            |
+| `monitoring.grafana.host`                          | Grafana ingress hostname                                                                |
+| `monitoring.grafana.tls.cert`                      | TLS certificate (only required if not using istio)                                      |
+| `monitoring.grafana.tls.key`                       | TLS key (only required if not using istio)                                              |
+| `monitoring.grafana.admin.username`                | Username for the admin user                                                             |
+| `monitoring.grafana.admin.password`                | Password for the admin user                                                             |
+| `monitoring.grafana.ldap.enabled`                  | Whether to enable LDAP                                                                  |
+| `monitoring.grafana.ldap.host`                     | Hostname of LDAP server                                                                 |
+| `monitoring.grafana.ldap.port`                     | Port of LDAP server (Has to be `quoted`!)                                               |
+| `monitoring.grafana.ldap.password`                 | Password of LDAP server                                                                 |
+| `monitoring.grafana.ldap.bind_dn`                  | Bind DN (username) of the LDAP server                                                   |
+| `monitoring.grafana.ldap.accountBases`             | List of base DNs to discover accounts (Has to have the format `"['a', 'b']"`)           |
+| `monitoring.grafana.ldap.groupBases`               | List of base DNs to discover groups (Has to have the format `"['a', 'b']"`)             |
+| `monitoring.grafana.dashboards.editable`           | Whether dashboards can be edited manually in the UI                                     |
+| `logging.loki.host`                                | Loki ingress hostname                                                                   |
+| `logging.loki.username`                            | Username for Loki (only required if not using istio)                                    |
+| `logging.loki.password`                            | Password for Loki (only required if not using istio)                                    |
+| `logging.loki.s3.protocol`                         | Protocol used for communicating with S3                                                 |
+| `logging.loki.s3.host`                             | Hostname of the S3 object store                                                         |
+| `logging.loki.s3.accessToken`                      | The EC2 accessToken used for authentication with S3                                     |
+| `logging.loki.s3.secret`                           | The secret associated with the accessToken                                              |
+| `logging.loki.s3.bucket`                           | The name of the S3 bucket                                                               |
+| `logging.loki.s3.region`                           | The region in which the S3 bucket is hosted                                             |
+| `logging.loki.tls.cert`                            | TLS certificate (only required if not using istio)                                      |
+| `logging.loki.tls.key`                             | TLS key (only required if not using istio)                                              |
 
 ### `gerritServers`
 
@@ -198,6 +204,23 @@
 The `gerrit_monitoring.py install`-command will decrypt the file before templating,
 if it was encrypted with `sops`.
 
+## Using Istio
+
+The easiest way of using the monitoring setup, is to use an Ingress Controller,
+but it is also possible to use the setup within an Istio service mesh. To do this,
+Istio has to be already installed in the cluster and the istio-ingressgateway
+has to open the ports 80 and 443.
+Authentication and authorization for Prometheus and Loki for users outside of the
+cluster will be done by JWT-tokens. Promtail configurations created by the installer
+will automatically get a token configured during the installation. Should another
+token be needed, the follwoing command can be used to create a token:
+
+```sh
+pipenv run python ./gerrit-monitoring.py \
+  --config config.yaml \
+  jwt
+```
+
 ## Installation
 
 Before using the script, set up a python environment using `pipenv install`.
diff --git a/cfgmgr/abstract.py b/cfgmgr/abstract.py
index 1b1a3b8..b1ca729 100644
--- a/cfgmgr/abstract.py
+++ b/cfgmgr/abstract.py
@@ -13,7 +13,13 @@
 # limitations under the License.
 
 import abc
+import json
 
+from copy import deepcopy
+
+import python_jwt
+
+import jwcrypto.jwk as jwk
 from passlib.apache import HtpasswdFile
 
 
@@ -29,6 +35,9 @@
             ["logging", "loki"],
             ["monitoring", "prometheus", "server"],
         ]
+        self.config = self._parse()
+        if self.config["istio"]["enabled"]:
+            self.jwks = self._create_jwks()
 
     def get_config(self):
         """Parse the configuration and return it as a dictionary.
@@ -37,17 +46,59 @@
             dict -- Dictionary containing the unencrypted configuration as parsed
               from the file
         """
+        return self._add_computed_values()
 
-        config = self._parse()
-        for component in self.requires_htpasswd:
-            section = config
-            for i in component:
-                section = section[i]
-            section["htpasswd"] = self._create_htpasswd_entry(
-                section["username"], section["password"]
-            )
+    def get_jwt_token(self, payload):
+        """Generate JWT token from the configured private key.
+
+        Args:
+            payload (dict): Token payload (https://tools.ietf.org/html/rfc7519#section-3.1)
+
+        Returns:
+            String: JWT token
+        """
+        private_key = jwk.JWK.from_pem(
+            self.config["istio"]["jwt"]["key"].encode("utf-8")
+        ).export()
+        # TODO: The tokens should get a lifetime, as soon as a mechanism is in place of
+        # automatically cycling them
+        return python_jwt.generate_jwt(payload, jwk.JWK.from_json(private_key), "RS256")
+
+    def _add_computed_values(self):
+        config = deepcopy(self.config)
+
+        if config["istio"]["enabled"]:
+            config["istio"]["jwt"]["jwks"] = json.dumps(self.jwks)
+
+            for gerrit in config["gerritServers"]["other"]:
+                payload = {
+                    "iss": config["istio"]["jwt"]["issuer"],
+                    "sub": f"promtail_{gerrit['host']}",
+                }
+                gerrit["promtail"]["token"] = self.get_jwt_token(payload)
+
+            payload = {
+                "iss": config["istio"]["jwt"]["issuer"],
+                "sub": f"promtail_cluster",
+            }
+            config["logging"]["promtail"] = {"token": self.get_jwt_token(payload)}
+        else:
+            for component in self.requires_htpasswd:
+                section = config
+                for i in component:
+                    section = section[i]
+                section["htpasswd"] = self._create_htpasswd_entry(
+                    section["username"], section["password"]
+                )
+
         return config
 
+    def _create_jwks(self):
+        public_key = jwk.JWK.from_pem(
+            self.config["istio"]["jwt"]["cert"].encode("utf-8")
+        ).export()
+        return {"keys": [json.loads(public_key)]}
+
     @staticmethod
     def _create_htpasswd_entry(username, password):
         htpasswd = HtpasswdFile()
diff --git a/charts/grafana/configuration/grafana.tls.secret.yaml b/charts/grafana/configuration/grafana.tls.secret.yaml
index 7a21443..cdcf737 100644
--- a/charts/grafana/configuration/grafana.tls.secret.yaml
+++ b/charts/grafana/configuration/grafana.tls.secret.yaml
@@ -1,5 +1,6 @@
 #@ load("@ytt:data", "data")
 #@ load("@ytt:base64", "base64")
+#@ if not data.values.istio.enabled:
 apiVersion: v1
 kind: Secret
 metadata:
@@ -9,3 +10,4 @@
 data:
   tls.crt: #@ base64.encode(data.values.monitoring.grafana.tls.cert)
   tls.key: #@ base64.encode(data.values.monitoring.grafana.tls.key)
+#@ end
diff --git a/charts/grafana/grafana.yaml b/charts/grafana/grafana.yaml
index 16e073b..42085de 100644
--- a/charts/grafana/grafana.yaml
+++ b/charts/grafana/grafana.yaml
@@ -160,7 +160,7 @@
 
 
 ingress:
-  enabled: true
+  enabled: #@ not data.values.istio.enabled
   # For Kubernetes >= 1.18 you should specify the ingress-controller via the field ingressClassName
   # See https://kubernetes.io/blog/2020/04/02/improvements-to-the-ingress-api-in-kubernetes-1.18/#specifying-the-class-of-an-ingress
   ingressClassName: nginx
diff --git a/charts/grafana/istio/grafana.virtual-service.yaml b/charts/grafana/istio/grafana.virtual-service.yaml
new file mode 100644
index 0000000..bb2e11d
--- /dev/null
+++ b/charts/grafana/istio/grafana.virtual-service.yaml
@@ -0,0 +1,21 @@
+#@ load("@ytt:data", "data")
+#@ if data.values.istio.enabled:
+apiVersion: networking.istio.io/v1alpha3
+kind: VirtualService
+metadata:
+  name: grafana-istio-virtual-service
+  namespace: #@ data.values.namespace
+  labels:
+    app: gerrit-monitoring
+spec:
+  hosts:
+  - #@ data.values.monitoring.grafana.host
+  gateways:
+  - monitoring-istio-gateway
+  http:
+  - route:
+    - destination:
+        host: #@ "grafana-{}.{}.svc.cluster.local".format(data.values.namespace, data.values.namespace)
+        port:
+          number: 80
+#@ end
diff --git a/charts/istio.yaml b/charts/istio.yaml
new file mode 100644
index 0000000..5ab42f3
--- /dev/null
+++ b/charts/istio.yaml
@@ -0,0 +1,86 @@
+#@ load("@ytt:data", "data")
+#@ load("@ytt:base64", "base64")
+#@ if data.values.istio.enabled:
+apiVersion: v1
+kind: Secret
+metadata:
+  name: #@ "monitoring-{}-istio-tls-secret".format(data.values.namespace)
+  namespace: istio-system
+  labels:
+    app: gerrit-monitoring
+    namespace: #@ data.values.namespace
+type: kubernetes.io/tls
+data:
+  tls.crt: #@ base64.encode(data.values.istio.cert)
+  tls.key: #@ base64.encode(data.values.istio.key)
+---
+apiVersion: networking.istio.io/v1alpha3
+kind: Gateway
+metadata:
+  name: monitoring-istio-gateway
+  namespace: #@ data.values.namespace
+  labels:
+    app: gerrit-monitoring
+spec:
+  selector:
+    istio: ingressgateway
+  servers:
+  - port:
+      number: 80
+      name: http
+      protocol: HTTP
+    hosts:
+    - #@ data.values.monitoring.prometheus.server.host
+    - #@ data.values.monitoring.grafana.host
+    - #@ data.values.logging.loki.host
+    tls:
+      httpsRedirect: true
+  - port:
+      number: 443
+      name: https
+      protocol: HTTPS
+    hosts:
+    - #@ data.values.monitoring.prometheus.server.host
+    - #@ data.values.monitoring.grafana.host
+    - #@ data.values.logging.loki.host
+    tls:
+      mode: SIMPLE
+      credentialName: #@ "monitoring-{}-istio-tls-secret".format(data.values.namespace)
+---
+apiVersion: security.istio.io/v1beta1
+kind: RequestAuthentication
+metadata:
+  name: monitoring-request-authentication
+  namespace: istio-system
+  labels:
+    app: gerrit-monitoring
+spec:
+  selector:
+    matchLabels:
+      istio: ingressgateway
+  jwtRules:
+  - issuer: #@ data.values.istio.jwt.issuer
+    jwks: #@ data.values.istio.jwt.jwks
+---
+apiVersion: "security.istio.io/v1beta1"
+kind: "AuthorizationPolicy"
+metadata:
+  name: monitoring-auth-policy
+  namespace: istio-system
+  labels:
+    app: gerrit-monitoring
+spec:
+  selector:
+    matchLabels:
+      istio: ingressgateway
+  action: DENY
+  rules:
+  - from:
+    - source:
+        notRequestPrincipals: ["*"]
+    to:
+    - operation:
+        hosts:
+        - #@ data.values.monitoring.prometheus.server.host
+        - #@ data.values.logging.loki.host
+#@ end
diff --git a/charts/loki/configuration/loki.basic-auth.secret.yaml b/charts/loki/configuration/loki.basic-auth.secret.yaml
index 33ecbd8..bf6b492 100644
--- a/charts/loki/configuration/loki.basic-auth.secret.yaml
+++ b/charts/loki/configuration/loki.basic-auth.secret.yaml
@@ -1,5 +1,6 @@
 #@ load("@ytt:data", "data")
 #@ load("@ytt:base64", "base64")
+#@ if not data.values.istio.enabled:
 apiVersion: v1
 kind: Secret
 metadata:
@@ -8,3 +9,4 @@
 data:
   auth: #@ base64.encode(data.values.logging.loki.htpasswd)
 type: Opaque
+#@ end
diff --git a/charts/loki/configuration/loki.tls.secret.yaml b/charts/loki/configuration/loki.tls.secret.yaml
index 3a789ca..3586d5d 100644
--- a/charts/loki/configuration/loki.tls.secret.yaml
+++ b/charts/loki/configuration/loki.tls.secret.yaml
@@ -1,5 +1,6 @@
 #@ load("@ytt:data", "data")
 #@ load("@ytt:base64", "base64")
+#@ if not data.values.istio.enabled:
 apiVersion: v1
 kind: Secret
 metadata:
@@ -9,3 +10,4 @@
 data:
   tls.crt: #@ base64.encode(data.values.logging.loki.tls.cert)
   tls.key: #@ base64.encode(data.values.logging.loki.tls.key)
+#@ end
diff --git a/charts/loki/istio/loki.virtual-service.yaml b/charts/loki/istio/loki.virtual-service.yaml
new file mode 100644
index 0000000..ec67370
--- /dev/null
+++ b/charts/loki/istio/loki.virtual-service.yaml
@@ -0,0 +1,21 @@
+#@ load("@ytt:data", "data")
+#@ if data.values.istio.enabled:
+apiVersion: networking.istio.io/v1alpha3
+kind: VirtualService
+metadata:
+  name: loki-istio-virtual-service
+  namespace: #@ data.values.namespace
+  labels:
+    app: gerrit-monitoring
+spec:
+  hosts:
+  - #@ data.values.logging.loki.host
+  gateways:
+  - monitoring-istio-gateway
+  http:
+  - route:
+    - destination:
+        host: #@ "loki-{}.{}.svc.cluster.local".format(data.values.namespace, data.values.namespace)
+        port:
+          number: 3100
+#@ end
diff --git a/charts/loki/loki.yaml b/charts/loki/loki.yaml
index fe44174..da3b47d 100644
--- a/charts/loki/loki.yaml
+++ b/charts/loki/loki.yaml
@@ -6,7 +6,7 @@
   pullPolicy: IfNotPresent
 
 ingress:
-  enabled: true
+  enabled: #@ not data.values.istio.enabled
   annotations:
     kubernetes.io/ingress.class: nginx
     nginx.ingress.kubernetes.io/auth-type: basic
@@ -130,7 +130,8 @@
   # existingClaim:
 
 ## Pod Labels
-podLabels: {}
+podLabels:
+  istio-jwt: enabled
 
 ## Pod Annotations
 podAnnotations:
diff --git a/charts/namespace.yaml b/charts/namespace.yaml
index 53a9f7e..256d638 100644
--- a/charts/namespace.yaml
+++ b/charts/namespace.yaml
@@ -3,3 +3,7 @@
 kind: Namespace
 metadata:
   name: #@ data.values.namespace
+  #@ if data.values.istio.enabled:
+  labels:
+    istio-injection: enabled
+  #@ end
diff --git a/charts/prometheus/configuration/prometheus.basic-auth.secret.yaml b/charts/prometheus/configuration/prometheus.basic-auth.secret.yaml
index 6969fa7..1a26f70 100644
--- a/charts/prometheus/configuration/prometheus.basic-auth.secret.yaml
+++ b/charts/prometheus/configuration/prometheus.basic-auth.secret.yaml
@@ -1,5 +1,6 @@
 #@ load("@ytt:data", "data")
 #@ load("@ytt:base64", "base64")
+#@ if not data.values.istio.enabled:
 apiVersion: v1
 kind: Secret
 metadata:
@@ -8,3 +9,4 @@
 data:
   auth: #@ base64.encode(data.values.monitoring.prometheus.server.htpasswd)
 type: Opaque
+#@ end
diff --git a/charts/prometheus/configuration/prometheus.tls.secret.yaml b/charts/prometheus/configuration/prometheus.tls.secret.yaml
index 31590fd..ba8d04a 100644
--- a/charts/prometheus/configuration/prometheus.tls.secret.yaml
+++ b/charts/prometheus/configuration/prometheus.tls.secret.yaml
@@ -1,5 +1,6 @@
 #@ load("@ytt:data", "data")
 #@ load("@ytt:base64", "base64")
+#@ if not data.values.istio.enabled:
 apiVersion: v1
 kind: Secret
 metadata:
@@ -9,3 +10,4 @@
 data:
   tls.crt: #@ base64.encode(data.values.monitoring.prometheus.server.tls.cert)
   tls.key: #@ base64.encode(data.values.monitoring.prometheus.server.tls.key)
+#@ end
diff --git a/charts/prometheus/istio/prometheus.virtual-service.yaml b/charts/prometheus/istio/prometheus.virtual-service.yaml
new file mode 100644
index 0000000..251bc1a
--- /dev/null
+++ b/charts/prometheus/istio/prometheus.virtual-service.yaml
@@ -0,0 +1,21 @@
+#@ load("@ytt:data", "data")
+#@ if data.values.istio.enabled:
+apiVersion: networking.istio.io/v1alpha3
+kind: VirtualService
+metadata:
+  name: prometheus-istio-virtual-service
+  namespace: #@ data.values.namespace
+  labels:
+    app: gerrit-monitoring
+spec:
+  hosts:
+  - #@ data.values.monitoring.prometheus.server.host
+  gateways:
+  - monitoring-istio-gateway
+  http:
+  - route:
+    - destination:
+        host: #@ "prometheus-{}-server.{}.svc.cluster.local".format(data.values.namespace, data.values.namespace)
+        port:
+          number: 80
+#@ end
diff --git a/charts/prometheus/prometheus.yaml b/charts/prometheus/prometheus.yaml
index 94a3b04..e21af18 100644
--- a/charts/prometheus/prometheus.yaml
+++ b/charts/prometheus/prometheus.yaml
@@ -705,7 +705,7 @@
   ingress:
     ## If true, Prometheus server Ingress will be created
     ##
-    enabled: true
+    enabled: #@ not data.values.istio.enabled
 
     ## Prometheus server Ingress annotations
     ##
@@ -846,7 +846,8 @@
 
   ## Labels to be added to Prometheus server pods
   ##
-  podLabels: {}
+  podLabels:
+    istio-jwt: enabled
 
   ## Prometheus AlertManager configuration
   ##
diff --git a/charts/promtail/promtail.yaml b/charts/promtail/promtail.yaml
index 161a84d..6bb2b30 100644
--- a/charts/promtail/promtail.yaml
+++ b/charts/promtail/promtail.yaml
@@ -247,6 +247,10 @@
     # Maximum time to wait for server to respond to a request
     timeout: 10s
 
+    #@ if data.values.istio.enabled:
+    bearer_token: #@ data.values.logging.promtail.token
+    #@ end
+
     backoff_config:
       # Initial backoff time between retries
       minbackoff: 100ms
diff --git a/config.yaml b/config.yaml
index 9a30f15..12b4ff3 100644
--- a/config.yaml
+++ b/config.yaml
@@ -25,6 +25,14 @@
 tls:
   skipVerify: true
   caCert:
+istio:
+  enabled: false
+  cert:
+  key:
+  jwt:
+    cert:
+    key:
+    issuer:
 monitoring:
   prometheus:
     server:
diff --git a/gerrit_monitoring.py b/gerrit_monitoring.py
index 3c78780..c390bb5 100755
--- a/gerrit_monitoring.py
+++ b/gerrit_monitoring.py
@@ -17,7 +17,7 @@
 import os.path
 
 from cfgmgr import get_config_manager
-from subcommands import encrypt, install, uninstall
+from subcommands import encrypt, install, jwt, uninstall
 
 
 def _run_encrypt(args):
@@ -40,6 +40,10 @@
     )
 
 
+def _run_jwt(args):
+    jwt(get_config_manager(os.path.abspath(args.config)))
+
+
 def _run_uninstall(args):
     uninstall(get_config_manager(args.config))
 
@@ -132,6 +136,9 @@
         action="store",
     )
 
+    parser_jwt = subparsers.add_parser("jwt", help="Create JWT token")
+    parser_jwt.set_defaults(func=_run_jwt)
+
     args = parser.parse_args()
     args.func(args)
 
diff --git a/promtail/promtailLocalConfig.yaml b/promtail/promtailLocalConfig.yaml
index d17d038..f816eca 100644
--- a/promtail/promtailLocalConfig.yaml
+++ b/promtail/promtailLocalConfig.yaml
@@ -16,9 +16,13 @@
       #@ if not data.values.tls.skipVerify:
       ca_file: #@ "{}/promtail.ca.crt".format(data.values.gerritServers.other[i].promtail.storagePath)
       #@ end
+    #@ if data.values.istio.enabled:
+    bearer_token: #@ data.values.gerritServers.other[i].promtail.token
+    #@ else:
     basic_auth:
       username: #@ data.values.logging.loki.username
       password: #@ data.values.logging.loki.password
+    #@ end
 scrape_configs:
 - job_name: gerrit_error
   static_configs:
diff --git a/subcommands/__init__.py b/subcommands/__init__.py
index 9509bd5..9e28db4 100644
--- a/subcommands/__init__.py
+++ b/subcommands/__init__.py
@@ -14,4 +14,5 @@
 
 from .encrypt import encrypt
 from .install import install
+from .jwt import jwt
 from .uninstall import uninstall
diff --git a/subcommands/encrypt.py b/subcommands/encrypt.py
index c29663b..513f0f3 100644
--- a/subcommands/encrypt.py
+++ b/subcommands/encrypt.py
@@ -23,6 +23,7 @@
     "caCert",
     "cert",
     "htpasswd",
+    "issuer",
     "key",
     "password",
     "secret",
diff --git a/subcommands/install.py b/subcommands/install.py
index 22f48f1..1a30b48 100644
--- a/subcommands/install.py
+++ b/subcommands/install.py
@@ -27,6 +27,7 @@
 
 
 TEMPLATES = [
+    "charts/istio.yaml",
     "charts/namespace.yaml",
     "charts/prometheus",
     "charts/promtail",
@@ -245,6 +246,10 @@
     """
     config = config_manager.get_config()
 
+    if config["istio"]["enabled"]:
+        LOOSE_RESOURCES.append("istio.yaml")
+        LOOSE_RESOURCES.append("istio")
+
     if not os.path.exists(output_dir):
         os.mkdir(output_dir)
     elif os.listdir(output_dir):
diff --git a/subcommands/jwt.py b/subcommands/jwt.py
new file mode 100644
index 0000000..ec43e79
--- /dev/null
+++ b/subcommands/jwt.py
@@ -0,0 +1,34 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import getpass
+import os
+
+
+def jwt(config_manager):
+    """Create JWT token.
+
+    Args:
+        config_manager {AbstractConfigManager} -- ConfigManager
+    """
+    config = config_manager.get_config()
+    payload = {
+        "iss": config["istio"]["jwt"]["issuer"],
+        "sub": f"{getpass.getuser()}@{os.uname()[1]}",
+    }
+
+    token = config_manager.get_jwt_token(payload)
+
+    print(f"Payload: {payload}")
+    print(f"JWT token:\n{token}")