├── .gitignore ├── LICENSE ├── README.md ├── deploy ├── mitmweb-deploy.yaml ├── mitmweb-http2-deploy.yaml └── mitmweb-service.yaml ├── endpoints.py ├── images ├── mitmproxy-initial-ui.png ├── mitmweb-all-flows.png ├── mitmweb-https-request-body.png └── mitmweb-https-response-body.png └── script.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pem 2 | -------------------------------------------------------------------------------- /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 | # k8s-sniff-https 2 | A simple [mitmproxy](https://mitmproxy.org/) blueprint to intercept HTTPS traffic from app running on Kubernetes. 3 | 4 | **Use case**: 5 | Sometimes you have your own app or a 3rd party app that performs HTTPS calls to remote SaaS backends. 6 | If you are curious like myself and you want to reverse engineer the API calls under the hood, or you want to debug/troubleshoot the traffic when shit hits the fan - you're in the right place! 7 | 8 | **But this is an already solved problem!** 9 | That is absolutely correct. So while there are other tools that intercept traffic originating from the Kubernetes cluster like [Kubeshark](https://www.kubeshark.co/), [tshark](https://www.wireshark.org/docs/man-pages/tshark.html) and even good-ol' [tcpdump](https://www.tcpdump.org/), when TLS traffic is involved you need a [Man-In-The-Middle](https://docs.mitmproxy.org/stable/concepts-howmitmproxyworks/#the-mitm-in-mitmproxy) proxy and you need to distribute the CA bundle to the workloads (Pods running in your cluster). 10 | I found the mitmproxy the most accessible and easy-to-setup-tool out there. 11 | 12 | # Related projects 13 | * [HTTP CONNECT forward proxy](https://github.com/ofirc/k8s-http-proxy) - useful for a simple HTTP(s) proxy that does not apply deep packet inspection 14 | 15 | * [mTLS Proxy with client credentials](https://github.com/ofirc/go-mtls-proxy) - useful in zero-trust settings 16 | 17 | # Overview 18 | ## Prerequisites 19 | **Step 0: clone this repository** 20 | ```sh 21 | git clone https://github.com/ofirc/k8s-sniff-https/ 22 | cd k8s-sniff-https 23 | ``` 24 | 25 | **Step 1: deploy the mitmweb deployment** 26 | 27 | Run the following commands: 28 | ```sh 29 | kubectl apply -f deploy/mitmweb-deploy.yaml 30 | kubectl apply -f deploy/mitmweb-service.yaml 31 | ``` 32 | 33 | **Step 2: pull the CA bundle from the mitmweb Pod** 34 | ```sh 35 | kubectl wait --for=condition=Available deployment/mitmweb --timeout=150s 36 | kubectl wait --for=condition=Ready pod -l app=mitmweb --timeout=150s 37 | # Wait until the file is present inside the selected Pod 38 | POD_NAME=$(kubectl get pod -l app=mitmweb -o=name | cut -d'/' -f2) 39 | CA_BUNDLE=/root/.mitmproxy/mitmproxy-ca-cert.pem 40 | until kubectl exec "$POD_NAME" -- test -f "$CA_BUNDLE"; do 41 | echo "Waiting for $CA_BUNDLE to appear in $POD_NAME..." 42 | sleep 2 43 | done 44 | kubectl cp $POD_NAME:$CA_BUNDLE mitmproxy-ca-cert.pem 45 | ``` 46 | 47 | **Step 3: spin up an ephemeral Pod for experimenting** 48 | ```sh 49 | kubectl run curl --image=curlimages/curl -- sleep infinity 50 | kubectl wait --for=condition=Ready pod/curl --timeout=150s 51 | ``` 52 | 53 | **Step 4: verify the running Pods** 54 | 55 | If all went well, this is what you should see: 56 | ```sh 57 | $ kubectl get pod,svc 58 | NAME READY STATUS RESTARTS AGE 59 | pod/curl 1/1 Running 0 18s 60 | pod/mitmweb-978d5b977-nrkxs 1/1 Running 0 9m2s 61 | 62 | NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE 63 | service/kubernetes ClusterIP 10.96.0.1 443/TCP 18d 64 | service/mitmweb ClusterIP 10.98.161.180 8080/TCP,8081/TCP 8m11s 65 | $ 66 | ``` 67 | 68 | **Step 5: distribute the CA bundle to your workload (Pod)** 69 | ```sh 70 | kubectl cp mitmproxy-ca-cert.pem curl:/home/curl_user/mitmproxy-ca-cert.pem 71 | ``` 72 | 73 | **Step 6: access the mitmweb UI console** 74 | 75 | We need to ensure we can access the mitmweb (the Web UI / console for mitmproxy) on our browser. 76 | 77 | Let's port forward the UI port to our localhost: 78 | ```sh 79 | kubectl port-forward svc/mitmweb 8081:8081 80 | ``` 81 | 82 | and open a browser tab to http://localhost:8081. 83 | 84 | If all went well then it should look like the following: ![The mitmweb initial page](images/mitmproxy-initial-ui.png) 85 | 86 | 87 | ## Sniff HTTP/HTTPS traffic 88 | Execute into the ephemeral curl Pod: 89 | ```sh 90 | kubectl exec -it curl -- sh 91 | ``` 92 | 93 | Trigger an HTTP request: 94 | ```sh 95 | curl -x http://mitmweb:8080 -v http://example.com 96 | ``` 97 | 98 | Then trigger an HTTPS request: 99 | ```sh 100 | curl --cacert mitmproxy-ca-cert.pem -x http://mitmweb:8080 -v https://google.com -d '{secretkey: secretvalue}' 101 | ``` 102 | 103 | If all went well then it should look like the following for all flows: ![All mitmweb flows](images/mitmweb-all-flows.png) 104 | 105 | Then we click on the HTTPS egress flow, we can inspect the request headers and body: ![mitmweb HTTPS request body](images/mitmweb-https-request-body.png) 106 | 107 | as well as the response headers and body: ![mitmweb HTTPS response body](images/mitmweb-https-response-body.png) 108 | 109 | ## Cleanup 110 | ```sh 111 | kubectl delete -f deploy/mitmweb-deploy.yaml 112 | kubectl delete -f deploy/mitmweb-service.yaml 113 | kubectl delete pod curl --force 114 | ``` 115 | 116 | # Frequently Asked Questions (FAQ) 117 | 118 | ### Question: What resources are deployed to my cluster? 119 | - A Kubernetes Deployment of `mitmweb` with one replica 120 | - A Kubernetes Service called `mitmweb` 121 | - An ephemeral Pod running the `curlimages/curl` image, used for testing 122 | 123 | ### Question: What ports are used by this deployment? 124 | - Port 8080 for the mitmweb proxy port 125 | - Port 8081 for the mitmweb web interface port 126 | -------------------------------------------------------------------------------- /deploy/mitmweb-deploy.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: mitmweb 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | app: mitmweb 10 | template: 11 | metadata: 12 | labels: 13 | app: mitmweb 14 | spec: 15 | containers: 16 | - name: mitmweb 17 | image: mitmproxy/mitmproxy:11.1.0 18 | ports: 19 | - containerPort: 8080 # Proxy port 20 | - containerPort: 8081 # Web interface port 21 | command: ["mitmweb"] 22 | args: ["--web-host", "0.0.0.0"] 23 | -------------------------------------------------------------------------------- /deploy/mitmweb-http2-deploy.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: mitmweb 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | app: mitmweb 10 | template: 11 | metadata: 12 | labels: 13 | app: mitmweb 14 | spec: 15 | containers: 16 | - name: mitmweb 17 | image: mitmproxy/mitmproxy:11.1.0 18 | ports: 19 | - containerPort: 8080 # Proxy port 20 | - containerPort: 8081 # Web interface port 21 | command: ["mitmweb"] 22 | args: ["--web-host", "0.0.0.0", "-s", "/scripts/endpoints.py"] 23 | volumeMounts: 24 | - name: script-volume 25 | mountPath: /scripts/endpoints.py 26 | subPath: endpoints.py # Ensure it's mounted as a file 27 | volumes: 28 | - name: script-volume 29 | configMap: 30 | name: mitmproxy-script 31 | -------------------------------------------------------------------------------- /deploy/mitmweb-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: mitmweb 5 | spec: 6 | selector: 7 | app: mitmweb 8 | ports: 9 | - name: proxy 10 | protocol: TCP 11 | port: 8080 # Expose proxy port 12 | targetPort: 8080 13 | - name: web 14 | protocol: TCP 15 | port: 8081 # Expose web interface port 16 | targetPort: 8081 -------------------------------------------------------------------------------- /endpoints.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import logging 3 | import re 4 | 5 | import mitmproxy 6 | from mitmproxy import http 7 | from mitmproxy import tls 8 | from mitmproxy import connection 9 | 10 | DISABLE_TLS_INSPECTION = False 11 | HTTP1_ADDRESSES = [r"^agent\..*\.app\.wiz\.io$"] 12 | HTTP2_ADDRESSES = [ 13 | r"^auth\.app\.wiz\.io$", 14 | r"^wizio\.azurecr\.io$", 15 | r"^.*\.wizio\.azurecr\.io$", 16 | r"^wiziopublic\.azurecr\.io$", # Add wiziopublic.azurecr.io explicitly 17 | r"^registry\.wiz\.io$", 18 | r"^.*\.registry\.wiz\.io$", 19 | r"^public-registry\.wiz\.io$", 20 | r"^.*\.public-registry\.wiz\.io$" 21 | ] 22 | HTTP1_WHITELIST = [re.compile(pattern) for pattern in HTTP1_ADDRESSES] 23 | HTTP2_WHITELIST = [re.compile(pattern) for pattern in HTTP2_ADDRESSES] 24 | IP_TO_SNI_MAP = dict() 25 | 26 | 27 | def get_addr(server: connection.Server): 28 | # .peername may be unset in upstream proxy mode, so we need a fallback. 29 | return server.peername or server.address 30 | 31 | 32 | def tls_clienthello(data: tls.ClientHelloData): 33 | server_address = get_addr(data.context.server) 34 | sni = data.client_hello.sni 35 | IP_TO_SNI_MAP[server_address[0]] = sni 36 | 37 | logging.info(f"TLS ClientHello: SNI={sni}, Address={server_address[0]}") 38 | 39 | # Log the ALPN protocols offered by the client 40 | alpn_protocols = data.client_hello.alpn_protocols 41 | if alpn_protocols: 42 | logging.info(f"Client offered ALPN protocols: {alpn_protocols}") 43 | 44 | if DISABLE_TLS_INSPECTION: 45 | logging.info(f"TLS inspection disabled for {sni}") 46 | data.ignore_connection = True 47 | return 48 | 49 | 50 | _callback = mitmproxy.addons.tlsconfig.alpn_select_callback 51 | logging.info("Overriding alpn_select_callback") 52 | 53 | 54 | def _new_callback(*args, **kwargs): 55 | conn = args[0] 56 | sni = conn.get_servername().decode("utf-8") 57 | 58 | logging.info(f"ALPN callback for: {sni}") 59 | 60 | # Check for HTTP1 domains 61 | if any(pattern.match(sni) for pattern in HTTP1_WHITELIST): 62 | logging.info(f"Forcing HTTP/1.1 for: {sni}") 63 | return b"http/1.1" 64 | 65 | # Check for registry and ACR domains first (more specific check) 66 | if ('registry.wiz.io' in sni or 67 | 'public-registry.wiz.io' in sni or 68 | 'wiziopublic.azurecr.io' in sni or 69 | 'wizio.azurecr.io' in sni): 70 | logging.info(f"Allowing HTTP/2 for registry/ACR domain: {sni}") 71 | return b"h2" 72 | 73 | # Check other HTTP2 domains 74 | if any(pattern.match(sni) for pattern in HTTP2_WHITELIST): 75 | logging.info(f"Allowing HTTP/2 for: {sni}") 76 | return b"h2" 77 | 78 | # For domains not in either whitelist, use the default behavior 79 | logging.info(f"Using default ALPN for: {sni}") 80 | return _callback(*args, **kwargs) 81 | 82 | 83 | mitmproxy.addons.tlsconfig.alpn_select_callback = _new_callback 84 | 85 | 86 | def is_whitelisted(sni, whitelist): 87 | """Check if the SNI matches any regex pattern in the whitelist.""" 88 | return any(pattern.match(sni) for pattern in whitelist) 89 | 90 | 91 | def request(flow: http.HTTPFlow): 92 | host = flow.request.host 93 | sni = IP_TO_SNI_MAP.get(host, host) 94 | 95 | logging.info(f"Processing request: {flow.request.url}") 96 | logging.info(f"SNI: {sni}, IP: {host}") 97 | 98 | # Special case for registry and ACR domains - CHECK THIS FIRST 99 | if ('registry.wiz.io' in sni or 100 | 'public-registry.wiz.io' in sni or 101 | 'wiziopublic.azurecr.io' in sni or 102 | 'wizio.azurecr.io' in sni): 103 | logging.info(f"Registry/ACR domain passthrough: {sni}") 104 | # Log the protocol being used 105 | protocol = "HTTP/2" if flow.request.http_version == "HTTP/2.0" else f"HTTP/{flow.request.http_version}" 106 | logging.info(f"Protocol for {sni}: {protocol}") 107 | return 108 | 109 | # Check if domain is in HTTP1_WHITELIST 110 | elif is_whitelisted(sni, HTTP1_WHITELIST): 111 | if flow.request.is_http10 or flow.request.is_http11: 112 | logging.info(f"HTTP/1 passthrough: {sni}") 113 | return 114 | logging.info(f"Blocking {sni} - HTTP/1 only domain using HTTP/2") 115 | flow.response = http.Response.make( 116 | 400, 117 | b"This domain requires HTTP/1.x", 118 | {"Content-Type": "text/plain"} 119 | ) 120 | # Special case for registry domains with more detailed logging 121 | elif 'registry.wiz.io' in sni or 'public-registry.wiz.io' in sni: 122 | logging.info(f"Registry domain passthrough: {sni}") 123 | # Log the protocol being used 124 | protocol = "HTTP/2" if flow.request.http_version == "HTTP/2.0" else f"HTTP/{flow.request.http_version}" 125 | logging.info(f"Protocol for {sni}: {protocol}") 126 | return 127 | # Check if domain is in HTTP2_WHITELIST 128 | elif is_whitelisted(sni, HTTP2_WHITELIST): 129 | logging.info(f"HTTP/2 passthrough: {sni}") 130 | return 131 | else: 132 | logging.info(f"Blocking request to non-whitelisted domain: {sni}") 133 | flow.response = http.Response.make( 134 | 403, 135 | b"This domain is not allowed.", 136 | {"Content-Type": "text/plain"} 137 | ) 138 | -------------------------------------------------------------------------------- /images/mitmproxy-initial-ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ofirc/k8s-sniff-https/4e7686f41a41fe5f66e8445ae4c4be8b9332e12a/images/mitmproxy-initial-ui.png -------------------------------------------------------------------------------- /images/mitmweb-all-flows.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ofirc/k8s-sniff-https/4e7686f41a41fe5f66e8445ae4c4be8b9332e12a/images/mitmweb-all-flows.png -------------------------------------------------------------------------------- /images/mitmweb-https-request-body.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ofirc/k8s-sniff-https/4e7686f41a41fe5f66e8445ae4c4be8b9332e12a/images/mitmweb-https-request-body.png -------------------------------------------------------------------------------- /images/mitmweb-https-response-body.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ofirc/k8s-sniff-https/4e7686f41a41fe5f66e8445ae4c4be8b9332e12a/images/mitmweb-https-response-body.png -------------------------------------------------------------------------------- /script.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import logging 3 | import re 4 | 5 | import mitmproxy 6 | from mitmproxy import http 7 | from mitmproxy import tls 8 | from mitmproxy import connection 9 | 10 | DISABLE_TLS_INSPECTION = False 11 | HTTP1_ADDRESSES = [] # Add regex patterns here 12 | HTTP2_ADDRESSES = [] # Add regex patterns here 13 | HTTP1_WHITELIST = [re.compile(pattern) for pattern in HTTP1_ADDRESSES] 14 | HTTP2_WHITELIST = [re.compile(pattern) for pattern in HTTP2_ADDRESSES] 15 | IP_TO_SNI_MAP = dict() 16 | 17 | 18 | def get_addr(server: connection.Server): 19 | # .peername may be unset in upstream proxy mode, so we need a fallback. 20 | return server.peername or server.address 21 | 22 | 23 | def tls_clienthello(data: tls.ClientHelloData): 24 | server_address = get_addr(data.context.server) 25 | sni = data.client_hello.sni 26 | IP_TO_SNI_MAP[server_address[0]] = sni 27 | 28 | if DISABLE_TLS_INSPECTION: 29 | data.ignore_connection = True 30 | return 31 | 32 | 33 | _callback = mitmproxy.addons.tlsconfig.alpn_select_callback 34 | logging.info("Overriding alpn_select_callback") 35 | 36 | 37 | def _new_callback(*args, **kwargs): 38 | conn = args[0] 39 | sni = conn.get_servername().decode("utf-8") 40 | 41 | if any(pattern.match(sni) for pattern in HTTP1_WHITELIST): 42 | logging.info(f"Calling: {sni} set http2 to False") 43 | return b"http/1.1" 44 | 45 | return _callback(*args, **kwargs) 46 | 47 | 48 | mitmproxy.addons.tlsconfig.alpn_select_callback = _new_callback 49 | 50 | 51 | def is_whitelisted(sni, whitelist): 52 | """Check if the SNI matches any regex pattern in the whitelist.""" 53 | return any(pattern.match(sni) for pattern in whitelist) 54 | 55 | 56 | def request(flow: http.HTTPFlow): 57 | # Get the host of the request 58 | host = flow.request.host 59 | sni = IP_TO_SNI_MAP.get(host, host) 60 | 61 | if is_whitelisted(sni, HTTP1_WHITELIST): 62 | if flow.request.is_http10 or flow.request.is_http11: 63 | logging.info(f"HTTP1 passthrough: {host}.") 64 | return 65 | logging.info(f"HTTP1 passthrough: {host} is allowed only HTTP1 communication but was communicating in HTTP2, blocking.") 66 | elif is_whitelisted(sni, HTTP2_WHITELIST): 67 | return 68 | 69 | logging.info(f"Blocking request to {sni} ip: {host}.") 70 | flow.response = http.Response.make( 71 | 403, # HTTP status code for Forbidden 72 | b"This domain is not allowed.", # Response body 73 | {"Content-Type": "text/plain"} # Headers 74 | ) 75 | 76 | --------------------------------------------------------------------------------