Repository: linkerd/linkerd2
Pull request #7713 (2022-01-26 16:47:29)

Pull out smoke tests from `install_test.go`
As part of `install_test.go`, we have a couple of test cases meant to
act as "smoke tests" and flag any obvious failures before the rest of
the tests are run. The smoke tests are heavily coupled to the install
stages and are unnecessarily run for each suite. This adds a bit of an
overhead and makes it harder to change and manage the code in
`install_test.go`.

To fix, we split up the tests from the install tests. During the
process, some golden files had to be copied over into a new testdata
directory. There are also duplicated functions across the two which will
be refactored gradually. 

At the moment, the tests will _not_ be run right after `install_test.go`
and will instead be ran as part of the deep suite. This is only
temporary, a more deterministic order will be enforced once more tests
are refactored.

**Edit**: 
~For this change, `getCheckOutput` has also been
refactored into its own method on the TestHelper struct.~

Refactoring leads to cyclical import between `testutil` and `pkg/healthcheck`.

Signed-off-by: Matei David <matei@buoyant.io>

<!--  Thanks for sending a pull request!

If you already have a well-structured git commit message, chances are GitHub
set the title and description of this PR to the git commit message subject and
body, respectively. If so, you may delete these instructions and submit your PR.

If this is your first time, please read our contributor guide:
https://github.com/linkerd/linkerd2/blob/main/CONTRIBUTING.md

The title and description of your Pull Request should match the git commit
subject and body, respectively. Git commit messages are structured as follows:

```
Subject

Problem

Solution

Validation

Fixes #[GitHub issue ID]

DCO Sign off
```

Example git commit message:

```
Introduce Pull Request Template

GitHub's community guidelines recommend a pull request template, the repo was
lacking one.

Introduce a `PULL_REQUEST_TEMPLATE.md` file.

Once merged, the
[Community profile checklist](https://github.com/linkerd/linkerd2/community)
should indicate the repo now provides a pull request template.

Fixes #3321

Signed-off-by: Jane Smith <jane.smith@example.com>
```

Note the git commit message subject becomes the pull request title.

For more details around git commits, see the section on Committing in our
contributor guide:
https://github.com/linkerd/linkerd2/blob/main/CONTRIBUTING.md#committing
-->



Patch:
diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile
index f188f6962..b7a64d574 100644
--- a/.devcontainer/Dockerfile
+++ b/.devcontainer/Dockerfile
@@ -50,8 +50,9 @@ ARG YQ_VERSION=v4.2.0
 RUN scurl -vo $HOME/bin/yq "https://github.com/mikefarah/yq/releases/download/${YQ_VERSION}/yq_linux_amd64" \
     && chmod +x $HOME/bin/yq
 
+ARG RUST_TOOLCHAIN=1.56.1
 RUN scurl -v https://sh.rustup.rs \
-    | sh -s -- -y --default-toolchain 1.56.1 -c rustfmt -c clippy -c rls
+    | sh -s -- -y --default-toolchain "${RUST_TOOLCHAIN}" -c rustfmt -c clippy -c rls
 
 RUN mkdir /tmp/cargo-deny && cd /tmp/cargo-deny && \
     scurl -v https://github.com/EmbarkStudios/cargo-deny/releases/download/0.11.0/cargo-deny-0.11.0-x86_64-unknown-linux-musl.tar.gz | tar zxf - && \
diff --git a/.github/workflows/devcontainer.yml b/.github/workflows/devcontainer.yml
new file mode 100644
index 000000000..3c51fbd4a
--- /dev/null
+++ b/.github/workflows/devcontainer.yml
@@ -0,0 +1,53 @@
+name: Devcontainer
+
+# When a pull request is opened that changes the Devcontainer configuration,
+# ensure that the container continues to build properly.
+on:
+  pull_request:
+    paths:
+      - rust-toolchain
+      - .devcontainer/**
+      - .github/workflows/devcontainer.yml
+
+permissions:
+  contents: read
+
+jobs:
+  build:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
+      - run: docker build . -f .devcontainer/Dockerfile
+
+  rust-version:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
+      - run: |
+          versions=$(sed -nE 's/^ARG RUST_TOOLCHAIN=([^ ]+)/\1/p' .devcontainer/Dockerfile)
+          ex=0
+          for mismatch in $(echo "$versions" | grep -vF "$(cat rust-toolchain)" || true) ; do
+            echo "::error file=.devcontainer/Dockerfile::Devcontainer uses incorrect rust version(s): $mismatch"
+            ex=$((ex + 1))
+          done
+          exit $ex
+
+  devcontainer-image:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
+      - run: |
+          export DEBIAN_FRONTEND=noninteractive
+          sudo apt-get update
+          sudo apt-get -y --no-install-recommends install jq
+      - run: |
+          # Strip jsonc comments because `jq` doesn't support them.
+          image=$(sed -E '/^\s*\/\/.*/d' .devcontainer/devcontainer.json |jq -Mr .image)
+          if [ "$image" == "null" ]; then
+            echo "::error file=.devcontainer/devcontainer.json::Missing image"
+            exit 1
+          fi
+          if ! docker pull "$image" ; then
+            echo "::error file=.devcontainer/devcontainer.json::Unable to pull image: $image"
+            exit 1
+          fi
diff --git a/.github/workflows/policy_controller.yml b/.github/workflows/policy_controller.yml
index 949c4e356..dcb22cfca 100644
--- a/.github/workflows/policy_controller.yml
+++ b/.github/workflows/policy_controller.yml
@@ -82,3 +82,31 @@ jobs:
       - uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
       - run: cargo test --workspace --no-run
       - run: cargo test --workspace
+
+  rust-toolchain:
+    name: rust toolchain
+    runs-on: ubuntu-latest
+    timeout-minutes: 2
+    steps:
+      - uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
+      - run: |
+          ex=0
+
+          # Check this workflow against the version in rust-toolchain.
+          versions=$(sed -nE 's|.*docker://(.*/)?rust:([^ #]+).*|\2|p' .github/workflows/policy_controller.yml)
+          for mismatch in $(echo "$versions" | grep -vF "$(cat rust-toolchain)" || true) ; do
+            echo "::error file=.github/workflows/policy_controller.yml::Workflow uses incorrect rust version(s): $mismatch"
+            ex=$((ex + 1))
+          done
+
+          # Check the policy-controller dockerfiles workflow against the version
+          # in rust-toolchain.
+          for f in policy-controller/*.dockerfile ; do
+            versions=$(sed -nE 's|ARG RUST_IMAGE=(.*/)?rust:([^ #]+).*|\2|p' "$f")
+            for mismatch in $(echo "$versions" | grep -vF "$(cat rust-toolchain)" || true) ; do
+              echo "::error file=\"$f\"::$f uses incorrect rust version(s): $mismatch"
+              ex=$((ex + 1))
+            done
+          done
+
+          exit $ex
diff --git a/ADOPTERS.md b/ADOPTERS.md
index bed52dc48..a7e3ece7e 100644
--- a/ADOPTERS.md
+++ b/ADOPTERS.md
@@ -22,6 +22,7 @@
 - [Doji](https://www.doji.cn/)
 - [Expedia](https://www.expedia.com)
 - [Facelift](https://www.facelift-bbt.com/en)
+- [Figure](https://www.figure.com)
 - [finleap connect](https://connect.finleap.com/)
 - [Giant Swarm](https://www.giantswarm.io)
 - [HomeChoice](https://www.homechoice.co.za/)
diff --git a/CHANGES.md b/CHANGES.md
index 634903ffb..e1fcb32a4 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,5 +1,22 @@
 # Changes
 
+## edge-22.1.5
+
+This edge release adds support for per-request Access Logging for HTTP inbound
+requests in Linkerd. A new annotation i.e. `config.linkerd.io/access-log` is added,
+which configures the proxies to emit access logs to stderr. `apache` and `json`
+are the supported configuration options, emitting access logs in Apache Common
+Log Format and JSON respectively.
+
+Special thanks to @tustvold for all the initial work around this!
+
+* Updated injector to support the new `config.linkerd.io/access-log` annotation
+* Added a new `LINKERD2_PROXY_ACCESS_LOG` proxy environment variable to configure
+  the access log format (thanks @tustvold)
+* Updated service mirror controller to emit relevant events when
+  mirroring is skipped for a service
+* Updated various dependencies across the project (thanks @dependabot)
+
 ## edge-22.1.4
 
 This edge release features a new configuration annotation, support for
diff --git a/Cargo.lock b/Cargo.lock
index 05cf71658..865156b34 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -89,15 +89,6 @@ version = "1.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
 
-[[package]]
-name = "block-buffer"
-version = "0.9.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4"
-dependencies = [
- "generic-array",
-]
-
 [[package]]
 name = "bumpalo"
 version = "3.9.1"
@@ -138,9 +129,9 @@ dependencies = [
 
 [[package]]
 name = "clap"
-version = "3.0.12"
+version = "3.0.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2afefa54b5c7dd40918dc1e09f213a171ab5937aadccab45e804780b238f9f43"
+checksum = "08799f92c961c7a1cf0cc398a9073da99e21ce388b46372c37f3191f2f3eed3e"
 dependencies = [
  "bitflags",
  "clap_derive",
@@ -179,15 +170,6 @@ version = "0.8.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
 
-[[package]]
-name = "cpufeatures"
-version = "0.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "95059428f66df56b63431fdb4e1947ed2190586af5c5a8a8b71122bdf5a7f469"
-dependencies = [
- "libc",
-]
-
 [[package]]
 name = "darling"
 version = "0.13.1"
@@ -234,15 +216,6 @@ dependencies = [
  "syn",
 ]
 
-[[package]]
-name = "digest"
-version = "0.9.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066"
-dependencies = [
- "generic-array",
-]
-
 [[package]]
 name = "dirs-next"
 version = "2.0.0"
@@ -427,16 +400,6 @@ dependencies = [
  "slab",
 ]
 
-[[package]]
-name = "generic-array"
-version = "0.14.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803"
-dependencies = [
- "typenum",
- "version_check",
-]
-
 [[package]]
 name = "getrandom"
 version = "0.2.4"
@@ -450,9 +413,9 @@ dependencies = [
 
 [[package]]
 name = "h2"
-version = "0.3.10"
+version = "0.3.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0c9de88456263e249e241fcd211d3954e2c9b0ef7ccfc235a444eb367cae3689"
+checksum = "d9f1f717ddc7b2ba36df7e871fd88db79326551d3d6f1fc406fbfd28b582ff8e"
 dependencies = [
  "bytes",
  "fnv",
@@ -473,31 +436,6 @@ version = "0.11.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
 
-[[package]]
-name = "headers"
-version = "0.3.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a4c4eb0471fcb85846d8b0690695ef354f9afb11cb03cac2e1d7c9253351afb0"
-dependencies = [
- "base64",
- "bitflags",
- "bytes",
- "headers-core",
- "http",
- "httpdate",
- "mime",
- "sha-1",
-]
-
-[[package]]
-name = "headers-core"
-version = "0.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429"
-dependencies = [
- "http",
-]
-
 [[package]]
 name = "heck"
 version = "0.3.3"
@@ -595,10 +533,10 @@ dependencies = [
  "http",
  "hyper",
  "log",
- "rustls 0.20.2",
+ "rustls",
  "rustls-native-certs",
  "tokio",
- "tokio-rustls 0.23.2",
+ "tokio-rustls",
 ]
 
 [[package]]
@@ -781,7 +719,7 @@ dependencies = [
  "openssl",
  "pem",
  "pin-project",
- "rustls 0.20.2",
+ "rustls",
  "rustls-pemfile",
  "secrecy",
  "serde",
@@ -859,9 +797,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
 
 [[package]]
 name = "libc"
-version = "0.2.114"
+version = "0.2.115"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b0005d08a8f7b65fb8073cb697aa0b12b631ed251ce73d862ce50eeb52ce3b50"
+checksum = "0a8d982fa7a96a000f6ec4cfe966de9703eccde29750df2bb8949da91b0e818d"
 
 [[package]]
 name = "linked-hash-map"
@@ -884,11 +822,13 @@ dependencies = [
  "linkerd-policy-controller-grpc",
  "linkerd-policy-controller-k8s-api",
  "linkerd-policy-controller-k8s-index",
+ "rustls",
+ "rustls-pemfile",
  "serde_json",
  "tokio",
+ "tokio-rustls",
  "tracing",
  "tracing-subscriber",
- "warp",
 ]
 
 [[package]]
@@ -1001,22 +941,6 @@ version = "2.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a"
 
-[[package]]
-name = "mime"
-version = "0.3.16"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
-
-[[package]]
-name = "mime_guess"
-version = "2.0.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2684d4c2e97d99848d30b324b00c8fcc7e5c897b7cbb5819b09e7c90e8baf212"
-dependencies = [
- "mime",
- "unicase",
-]
-
 [[package]]
 name = "mio"
 version = "0.7.14"
@@ -1107,12 +1031,6 @@ version = "1.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "da32515d9f6e6e489d7bc9d84c71b060db7247dc035bbe44eac88cf87486d8d5"
 
-[[package]]
-name = "opaque-debug"
-version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
-
 [[package]]
 name = "openssl"
 version = "0.10.38"
@@ -1462,19 +1380,6 @@ dependencies = [
  "winapi",
 ]
 
-[[package]]
-name = "rustls"
-version = "0.19.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "35edb675feee39aec9c99fa5ff985081995a06d594114ae14cbe797ad7b7a6d7"
-dependencies = [
- "base64",
- "log",
- "ring",
- "sct 0.6.1",
- "webpki 0.21.4",
-]
-
 [[package]]
 name = "rustls"
 version = "0.20.2"
@@ -1483,8 +1388,8 @@ checksum = "d37e5e2290f3e040b594b1a9e04377c2c671f1a1cfd9bfdef82106ac1c113f84"
 dependencies = [
  "log",
  "ring",
- "sct 0.7.0",
- "webpki 0.22.0",
+ "sct",
+ "webpki",
 ]
 
 [[package]]
@@ -1548,28 +1453,12 @@ dependencies = [
  "syn",
 ]
 
-[[package]]
-name = "scoped-tls"
-version = "1.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2"
-
 [[package]]
 name = "scopeguard"
 version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
 
-[[package]]
-name = "sct"
-version = "0.6.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b362b83898e0e69f38515b82ee15aa80636befe47c3b6d3d89a911e78fc228ce"
-dependencies = [
- "ring",
- "untrusted",
-]
-
 [[package]]
 name = "sct"
 version = "0.7.0"
@@ -1666,18 +1555,6 @@ dependencies = [
  "serde",
 ]
 
-[[package]]
-name = "serde_urlencoded"
-version = "0.7.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
-dependencies = [
- "form_urlencoded",
- "itoa 1.0.1",
- "ryu",
- "serde",
-]
-
 [[package]]
 name = "serde_yaml"
 version = "0.8.23"
@@ -1690,19 +1567,6 @@ dependencies = [
  "yaml-rust",
 ]
 
-[[package]]
-name = "sha-1"
-version = "0.9.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "99cd6713db3cf16b6c84e06321e049a9b9f699826e16096d23bbcc44d15d51a6"
-dependencies = [
- "block-buffer",
- "cfg-if",
- "cpufeatures",
- "digest",
- "opaque-debug",
-]
-
 [[package]]
 name = "sharded-slab"
 version = "0.1.4"
@@ -1735,9 +1599,9 @@ checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83"
 
 [[package]]
 name = "socket2"
-version = "0.4.3"
+version = "0.4.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0f82496b90c36d70af5fcd482edaa2e0bd16fade569de1330405fecbbdac736b"
+checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0"
 dependencies = [
  "libc",
  "winapi",
@@ -1828,9 +1692,9 @@ dependencies = [
 
 [[package]]
 name = "tokio"
-version = "1.15.0"
+version = "1.16.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fbbf1c778ec206785635ce8ad57fe52b3009ae9e0c9f574a728f3049d3e55838"
+checksum = "95e99d373042c30406d317cfc5bfad7b5d604bdd31dab72cf8739abebaa64aee"
 dependencies = [
  "bytes",
  "libc",
@@ -1876,26 +1740,15 @@ dependencies = [
  "tokio",
 ]
 
-[[package]]
-name = "tokio-rustls"
-version = "0.22.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bc6844de72e57df1980054b38be3a9f4702aba4858be64dd700181a8a6d0e1b6"
-dependencies = [
- "rustls 0.19.1",
- "tokio",
- "webpki 0.21.4",
-]
-
 [[package]]
 name = "tokio-rustls"
 version = "0.23.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a27d5f2b839802bd8267fa19b0530f5a08b9c08cd417976be2a65d130fe1c11b"
 dependencies = [
- "rustls 0.20.2",
+ "rustls",
  "tokio",
- "webpki 0.22.0",
+ "webpki",
 ]
 
 [[package]]
@@ -2119,21 +1972,6 @@ version = "0.2.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642"
 
-[[package]]
-name = "typenum"
-version = "1.15.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987"
-
-[[package]]
-name = "unicase"
-version = "2.6.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6"
-dependencies = [
- "version_check",
-]
-
 [[package]]
 name = "unicode-segmentation"
 version = "1.8.0"
@@ -2174,35 +2012,6 @@ dependencies = [
  "try-lock",
 ]
 
-[[package]]
-name = "warp"
-version = "0.3.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3cef4e1e9114a4b7f1ac799f16ce71c14de5778500c5450ec6b7b920c55b587e"
-dependencies = [
- "bytes",
- "futures-channel",
- "futures-util",
- "headers",
- "http",
- "hyper",
- "log",
- "mime",
- "mime_guess",
- "percent-encoding",
- "pin-project",
- "scoped-tls",
- "serde",
- "serde_json",
- "serde_urlencoded",
- "tokio",
- "tokio-rustls 0.22.0",
- "tokio-stream",
- "tokio-util",
- "tower-service",
- "tracing",
-]
-
 [[package]]
 name = "wasi"
 version = "0.10.0+wasi-snapshot-preview1"
@@ -2273,16 +2082,6 @@ dependencies = [
  "wasm-bindgen",
 ]
 
-[[package]]
-name = "webpki"
-version = "0.21.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b8e38c0608262c46d4a56202ebabdeb094cef7e560ca7a226c6bf055188aa4ea"
-dependencies = [
- "ring",
- "untrusted",
-]
-
 [[package]]
 name = "webpki"
 version = "0.22.0"
@@ -2337,6 +2136,6 @@ dependencies = [
 
 [[package]]
 name = "zeroize"
-version = "1.5.0"
+version = "1.5.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cc222aec311c323c717f56060324f32b82da1ce1dd81d9a09aa6a9030bfe08db"
+checksum = "4062c749be08d90be727e9c5895371c3a0e49b90ba2b9592dc7afda95cc2b719"
diff --git a/bin/helm b/bin/helm
index 1ab232b81..181dce159 100755
--- a/bin/helm
+++ b/bin/helm
@@ -2,7 +2,7 @@
 
 set -eu
 
-helmversion=v3.2.1
+helmversion=v3.8.0
 bindir=$( cd "${0%/*}" && pwd )
 targetbin=$( cd "$bindir"/.. && pwd )/target/bin
 helmbin=$targetbin/helm-$helmversion
diff --git a/charts/linkerd-control-plane/Chart.yaml b/charts/linkerd-control-plane/Chart.yaml
index 9adb9ac79..35ec4bccf 100644
--- a/charts/linkerd-control-plane/Chart.yaml
+++ b/charts/linkerd-control-plane/Chart.yaml
@@ -16,7 +16,7 @@ dependencies:
 - name: partials
   version: 0.1.0
   repository: file://../partials
-version: 1.1.2-edge
+version: 1.1.3-edge
 icon: https://linkerd.io/images/logo-only-200h.png
 maintainers:
   - name: Linkerd authors
diff --git a/charts/linkerd-control-plane/README.md b/charts/linkerd-control-plane/README.md
index 78c1444d6..2dede0f2e 100644
--- a/charts/linkerd-control-plane/README.md
+++ b/charts/linkerd-control-plane/README.md
@@ -3,7 +3,7 @@
 Linkerd gives you observability, reliability, and security
 for your microservices — with no code change required.
 
-![Version: 1.1.2-edge](https://img.shields.io/badge/Version-1.1.2--edge-informational?style=flat-square)
+![Version: 1.1.3-edge](https://img.shields.io/badge/Version-1.1.3--edge-informational?style=flat-square)
 ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square)
 ![AppVersion: edge-XX.X.X](https://img.shields.io/badge/AppVersion-edge--XX.X.X-informational?style=flat-square)
 
diff --git a/charts/linkerd2-cni/Chart.yaml b/charts/linkerd2-cni/Chart.yaml
index 84fb388ce..8522d8fcb 100644
--- a/charts/linkerd2-cni/Chart.yaml
+++ b/charts/linkerd2-cni/Chart.yaml
@@ -9,5 +9,5 @@ description: |
 kubeVersion: ">=1.20.0-0"
 icon: https://linkerd.io/images/logo-only-200h.png
 name: "linkerd2-cni"
-version: 30.0.2-edge
+version: 30.0.3-edge
 
diff --git a/charts/linkerd2-cni/README.md b/charts/linkerd2-cni/README.md
index 0e9551681..5c3c88054 100644
--- a/charts/linkerd2-cni/README.md
+++ b/charts/linkerd2-cni/README.md
@@ -6,7 +6,7 @@ Linkerd [CNI plugin](https://linkerd.io/2/features/cni/) takes care of setting
 up your pod's network so  incoming and outgoing traffic is proxied through the
 data plane.
 
-![Version: 30.0.2-edge](https://img.shields.io/badge/Version-30.0.2--edge-informational?style=flat-square)
+![Version: 30.0.3-edge](https://img.shields.io/badge/Version-30.0.3--edge-informational?style=flat-square)
 
 ![AppVersion: edge-XX.X.X](https://img.shields.io/badge/AppVersion-edge--XX.X.X-informational?style=flat-square)
 
@@ -36,6 +36,7 @@ Kubernetes: `>=1.20.0-0`
 | outboundProxyPort | int | `4140` | Outbound port for the proxy container |
 | portsToRedirect | string | `""` | Ports to redirect to proxy |
 | priorityClassName | string | `""` | Kubernetes priorityClassName for the CNI plugin's Pods |
+| privileged | bool | `false` | Run the install-cni container in privileged mode |
 | proxyAdminPort | int | `4191` | Admin port for the proxy container |
 | proxyControlPort | int | `4190` | Control port for the proxy container |
 | proxyUID | int | `2102` | User id under which the proxy shall be ran |
diff --git a/charts/linkerd2-cni/templates/cni-plugin.yaml b/charts/linkerd2-cni/templates/cni-plugin.yaml
index a64832552..c21f3f933 100644
--- a/charts/linkerd2-cni/templates/cni-plugin.yaml
+++ b/charts/linkerd2-cni/templates/cni-plugin.yaml
@@ -49,7 +49,9 @@ metadata:
   labels:
     linkerd.io/cni-resource: "true"
 spec:
+  {{- if not .Values.privileged }}
   allowPrivilegeEscalation: false
+  {{- end }}
   fsGroup:
     rule: RunAsAny
   hostNetwork: true
@@ -250,6 +252,7 @@ spec:
           name: linkerd-tmp-dir
         securityContext:
           readOnlyRootFilesystem: true
+          privileged: {{.Values.privileged}}
       volumes:
       {{- if ne .Values.destCNIBinDir .Values.destCNINetDir }}
       - name: cni-bin-dir
diff --git a/charts/linkerd2-cni/values.yaml b/charts/linkerd2-cni/values.yaml
index 2c87466d6..ae6312b5d 100644
--- a/charts/linkerd2-cni/values.yaml
+++ b/charts/linkerd2-cni/values.yaml
@@ -33,6 +33,9 @@ priorityClassName: ""
 # Note PSP has been deprecated since k8s v1.21
 enablePSP: false
 
+# -- Run the install-cni container in privileged mode
+privileged: false
+
 # -|- Tolerations section, See the
 # [K8S documentation](https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/)
 # for more information
diff --git a/go.mod b/go.mod
index 472878980..49b6eb4de 100644
--- a/go.mod
+++ b/go.mod
@@ -36,13 +36,13 @@ require (
 	github.com/spf13/pflag v1.0.5
 	go.opencensus.io v0.23.0
 	golang.org/x/net v0.0.0-20220107192237-5cfca573fb4d
-	golang.org/x/tools v0.1.8
+	golang.org/x/tools v0.1.9
 	google.golang.org/grpc v1.44.0
 	google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.2.0
 	google.golang.org/protobuf v1.27.1
 	helm.sh/helm/v3 v3.8.0
 	k8s.io/api v0.23.3
-	k8s.io/apiextensions-apiserver v0.23.2
+	k8s.io/apiextensions-apiserver v0.23.3
 	k8s.io/apimachinery v0.23.3
 	k8s.io/client-go v0.23.3
 	k8s.io/code-generator v0.23.3
diff --git a/go.sum b/go.sum
index 224ddf7b5..bfa5008ed 100644
--- a/go.sum
+++ b/go.sum
@@ -1327,8 +1327,8 @@ golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 golang.org/x/tools v0.1.6-0.20210820212750-d4cc65f0b2ff/go.mod h1:YD9qOF0M9xpSpdWTBbzEl5e/RnCefISl8E5Noe10jFM=
-golang.org/x/tools v0.1.8 h1:P1HhGGuLW4aAclzjtmJdf0mJOjVUZUzOTqkAkWL+l6w=
-golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
+golang.org/x/tools v0.1.9 h1:j9KsMiaP1c3B0OTQGth0/k+miLGTgLsAFUCrF2vLcF8=
+golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -1551,36 +1551,30 @@ honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9
 honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
 k8s.io/api v0.20.6/go.mod h1:X9e8Qag6JV/bL5G6bU8sdVRltWKmdHsFUGS3eVndqE8=
 k8s.io/api v0.23.1/go.mod h1:WfXnOnwSqNtG62Y1CdjoMxh7r7u9QXGCkA1u0na2jgo=
-k8s.io/api v0.23.2/go.mod h1:sYuDb3flCtRPI8ghn6qFrcK5ZBu2mhbElxRE95qpwlI=
 k8s.io/api v0.23.3 h1:KNrME8KHGr12Ozjf8ytOewKzZh6hl/hHUZeHddT3a38=
 k8s.io/api v0.23.3/go.mod h1:w258XdGyvCmnBj/vGzQMj6kzdufJZVUwEM1U2fRJwSQ=
 k8s.io/apiextensions-apiserver v0.23.1/go.mod h1:0qz4fPaHHsVhRApbtk3MGXNn2Q9M/cVWWhfHdY2SxiM=
-k8s.io/apiextensions-apiserver v0.23.2 h1:N6CIVAhmF0ahgFKUMDdV/AUyckhUb4nIyVPohPtdUPk=
-k8s.io/apiextensions-apiserver v0.23.2/go.mod h1:9cs7avT6+GfzbB0pambTvH11wcaR85QQg4ovl9s15UU=
+k8s.io/apiextensions-apiserver v0.23.3 h1:JvPJA7hSEAqMRteveq4aj9semilAZYcJv+9HHFWfUdM=
+k8s.io/apiextensions-apiserver v0.23.3/go.mod h1:/ZpRXdgKZA6DvIVPEmXDCZJN53YIQEUDF+hrpIQJL38=
 k8s.io/apimachinery v0.20.6/go.mod h1:ejZXtW1Ra6V1O5H8xPBGz+T3+4gfkTCeExAHKU57MAc=
 k8s.io/apimachinery v0.23.1/go.mod h1:SADt2Kl8/sttJ62RRsi9MIV4o8f5S3coArm0Iu3fBno=
-k8s.io/apimachinery v0.23.2/go.mod h1:zDqeV0AK62LbCI0CI7KbWCAYdLg+E+8UXJ0rIz5gmS8=
 k8s.io/apimachinery v0.23.3 h1:7IW6jxNzrXTsP0c8yXz2E5Yx/WTzVPTsHIx/2Vm0cIk=
 k8s.io/apimachinery v0.23.3/go.mod h1:BEuFMMBaIbcOqVIJqNZJXGFTP4W6AycEpb5+m/97hrM=
 k8s.io/apiserver v0.20.6/go.mod h1:QIJXNt6i6JB+0YQRNcS0hdRHJlMhflFmsBDeSgT1r8Q=
 k8s.io/apiserver v0.23.1/go.mod h1:Bqt0gWbeM2NefS8CjWswwd2VNAKN6lUKR85Ft4gippY=
-k8s.io/apiserver v0.23.2/go.mod h1:Kdt8gafkPev9Gfh+H6lCPbmRu42f7BfhOfHKKa3dtyU=
 k8s.io/apiserver v0.23.3/go.mod h1:3HhsTmC+Pn+Jctw+Ow0LHA4dQ4oXrQ4XJDzrVDG64T4=
 k8s.io/cli-runtime v0.23.1 h1:vHUZrq1Oejs0WaJnxs09mLHKScvIIl2hMSthhS8o8Yo=
 k8s.io/cli-runtime v0.23.1/go.mod h1:r9r8H/qfXo9w+69vwUL7LokKlLRKW5D6A8vUKCx+YL0=
 k8s.io/client-go v0.20.6/go.mod h1:nNQMnOvEUEsOzRRFIIkdmYOjAZrC8bgq0ExboWSU1I0=
 k8s.io/client-go v0.23.1/go.mod h1:6QSI8fEuqD4zgFK0xbdwfB/PthBsIxCJMa3s17WlcO0=
-k8s.io/client-go v0.23.2/go.mod h1:k3YbsWg6GWdHF1THHTQP88X9RhB1DWPo3Dq7KfU/D1c=
 k8s.io/client-go v0.23.3 h1:23QYUmCQ/W6hW78xIwm3XqZrrKZM+LWDqW2zfo+szJs=
 k8s.io/client-go v0.23.3/go.mod h1:47oMd+YvAOqZM7pcQ6neJtBiFH7alOyfunYN48VsmwE=
 k8s.io/code-generator v0.19.7/go.mod h1:lwEq3YnLYb/7uVXLorOJfxg+cUu2oihFhHZ0n9NIla0=
 k8s.io/code-generator v0.23.1/go.mod h1:V7yn6VNTCWW8GqodYCESVo95fuiEg713S8B7WacWZDA=
-k8s.io/code-generator v0.23.2/go.mod h1:S0Q1JVA+kSzTI1oUvbKAxZY/DYbA/ZUb4Uknog12ETk=
 k8s.io/code-generator v0.23.3 h1:NSAKIkvkL8OaWr5DrF9CXGBJjhMp3itplT/6fwHQcAY=
 k8s.io/code-generator v0.23.3/go.mod h1:S0Q1JVA+kSzTI1oUvbKAxZY/DYbA/ZUb4Uknog12ETk=
 k8s.io/component-base v0.20.6/go.mod h1:6f1MPBAeI+mvuts3sIdtpjljHWBQ2cIy38oBIWMYnrM=
 k8s.io/component-base v0.23.1/go.mod h1:6llmap8QtJIXGDd4uIWJhAq0Op8AtQo6bDW2RrNMTeo=
-k8s.io/component-base v0.23.2/go.mod h1:wS9Z03MO3oJ0RU8bB/dbXTiluGju+SC/F5i660gxB8c=
 k8s.io/component-base v0.23.3/go.mod h1:1Smc4C60rWG7d3HjSYpIwEbySQ3YWg0uzH5a2AtaTLg=
 k8s.io/component-helpers v0.23.1/go.mod h1:ZK24U+2oXnBPcas2KolLigVVN9g5zOzaHLkHiQMFGr0=
 k8s.io/cri-api v0.20.6/go.mod h1:ew44AjNXwyn1s0U4xCKGodU7J1HzBeZ1MpGrpa5r8Yc=
diff --git a/jaeger/charts/linkerd-jaeger/Chart.yaml b/jaeger/charts/linkerd-jaeger/Chart.yaml
index ce79a8527..e811693f1 100644
--- a/jaeger/charts/linkerd-jaeger/Chart.yaml
+++ b/jaeger/charts/linkerd-jaeger/Chart.yaml
@@ -11,7 +11,7 @@ kubeVersion: ">=1.20.0-0"
 name: linkerd-jaeger
 sources:
 - https://github.com/linkerd/linkerd2/
-version: 30.1.0-edge
+version: 30.1.1-edge
 icon: https://linkerd.io/images/logo-only-200h.png
 maintainers:
   - name: Linkerd authors
diff --git a/jaeger/charts/linkerd-jaeger/README.md b/jaeger/charts/linkerd-jaeger/README.md
index e0863f39d..18a30586c 100644
--- a/jaeger/charts/linkerd-jaeger/README.md
+++ b/jaeger/charts/linkerd-jaeger/README.md
@@ -3,7 +3,7 @@
 The Linkerd-Jaeger extension adds distributed tracing to Linkerd using
 OpenCensus and Jaeger.
 
-![Version: 30.1.0-edge](https://img.shields.io/badge/Version-30.1.0--edge-informational?style=flat-square)
+![Version: 30.1.1-edge](https://img.shields.io/badge/Version-30.1.1--edge-informational?style=flat-square)
 
 ![AppVersion: edge-XX.X.X](https://img.shields.io/badge/AppVersion-edge--XX.X.X-informational?style=flat-square)
 
diff --git a/multicluster/charts/linkerd-multicluster/Chart.yaml b/multicluster/charts/linkerd-multicluster/Chart.yaml
index f11784410..be4d82f9e 100644
--- a/multicluster/charts/linkerd-multicluster/Chart.yaml
+++ b/multicluster/charts/linkerd-multicluster/Chart.yaml
@@ -11,7 +11,7 @@ kubeVersion: ">=1.20.0-0"
 name: "linkerd-multicluster"
 sources:
 - https://github.com/linkerd/linkerd2/
-version: 30.0.2-edge
+version: 30.0.3-edge
 icon: https://linkerd.io/images/logo-only-200h.png
 maintainers:
   - name: Linkerd authors
diff --git a/multicluster/charts/linkerd-multicluster/README.md b/multicluster/charts/linkerd-multicluster/README.md
index b63b08c38..8b4d35028 100644
--- a/multicluster/charts/linkerd-multicluster/README.md
+++ b/multicluster/charts/linkerd-multicluster/README.md
@@ -3,7 +3,7 @@
 The Linkerd-Multicluster extension contains resources to support multicluster
 linking to remote clusters
 
-![Version: 30.0.2-edge](https://img.shields.io/badge/Version-30.0.2--edge-informational?style=flat-square)
+![Version: 30.0.3-edge](https://img.shields.io/badge/Version-30.0.3--edge-informational?style=flat-square)
 
 ![AppVersion: edge-XX.X.X](https://img.shields.io/badge/AppVersion-edge--XX.X.X-informational?style=flat-square)
 
diff --git a/policy-controller/Cargo.toml b/policy-controller/Cargo.toml
index 52f235905..e10e2cce8 100644
--- a/policy-controller/Cargo.toml
+++ b/policy-controller/Cargo.toml
@@ -8,7 +8,7 @@ publish = false
 [features]
 default = ["native-tls"]
 native-tls = ["kube/native-tls"]
-rustls = ["kube/rustls-tls"]
+rustls-tls = ["kube/rustls-tls"]
 
 [dependencies]
 anyhow = "1"
@@ -21,8 +21,11 @@ linkerd-policy-controller-core = { path = "./core" }
 linkerd-policy-controller-grpc = { path = "./grpc" }
 linkerd-policy-controller-k8s-index = { path = "./k8s/index" }
 linkerd-policy-controller-k8s-api = { path = "./k8s/api" }
+rustls = { version = "0.20", default-features = false }
+rustls-pemfile = "0.2"
 serde_json = "1"
 tokio = { version = "1", features = [
+    "fs",
     "macros",
     "parking_lot",
     "rt",
@@ -30,6 +33,7 @@ tokio = { version = "1", features = [
     "signal",
     "sync",
 ] }
+tokio-rustls = { version = "0.23", default-features = false }
 tracing = "0.1"
 tracing-subscriber = { version = "0.3", default-features = false, features = [
     "env-filter",
@@ -38,7 +42,6 @@ tracing-subscriber = { version = "0.3", default-features = false, features = [
     "smallvec",
     "tracing-log",
 ] }
-warp = { version = "0.3", default-features = false, features = ["tls"] }
 
 [target.x86_64-unknown-linux-gnu.dependencies]
 jemallocator = "0.3"
diff --git a/policy-controller/src/admission.rs b/policy-controller/src/admission.rs
index 2731eca8a..ec6e8dffd 100644
--- a/policy-controller/src/admission.rs
+++ b/policy-controller/src/admission.rs
@@ -1,29 +1,57 @@
 use crate::api;
-use anyhow::{anyhow, bail, Result};
+use anyhow::{anyhow, bail, Context, Result};
 use api::policy::ServerSpec;
+use futures::future;
+use hyper::{body::Buf, http, Body, Request, Response};
 use kube::{api::Api, core::DynamicObject, ResourceExt};
-use std::convert::{Infallible, TryInto};
+use std::convert::TryInto;
+use std::task;
 use tracing::{debug, info, warn};
-use warp::{filters::BoxedFilter, http, reply, Filter};
 
-/// Builds an API handler for a validating admission webhook that examines `Server` resources.
-pub fn routes(client: kube::Client) -> BoxedFilter<(impl warp::Reply,)> {
-    warp::post()
-        .and(warp::path::end())
-        // 64KB should be more than enough for any `Server` instance we can expect.
-        .and(warp::body::content_length_limit(64 * 1024).and(warp::body::json()))
-        .and(warp::any().map(move || client.clone()))
-        .and_then(|review: Review, client| async move {
-            let req: Request = match review.try_into() {
+#[derive(Clone)]
+pub struct Service {
+    pub client: kube::Client,
+}
+
+impl hyper::service::Service<Request<Body>> for Service {
+    type Response = Response<Body>;
+    type Error = anyhow::Error;
+    type Future = future::BoxFuture<'static, Result<Response<Body>>>;
+
+    fn poll_ready(&mut self, _cx: &mut task::Context<'_>) -> task::Poll<Result<()>> {
+        task::Poll::Ready(Ok(()))
+    }
+
+    fn call(&mut self, req: Request<Body>) -> Self::Future {
+        if req.method() != http::Method::POST || req.uri().path() != "/" {
+            return Box::pin(future::ok(
+                Response::builder()
+                    .status(http::StatusCode::NOT_FOUND)
+                    .body(Body::empty())
+                    .expect("not found response must be valid"),
+            ));
+        }
+        let client = self.client.clone();
+        Box::pin(async move {
+            let bytes = hyper::body::aggregate(req.into_body()).await?;
+            let review: Review = match serde_json::from_reader(bytes.reader()) {
+                Ok(review) => review,
+                Err(error) => {
+                    warn!(%error, "Failed to parse request body");
+                    return json_response(AdmissionResponse::invalid(error).into_review());
+                }
+            };
+
+            let req: AdmissionRequest = match review.try_into() {
                 Ok(req) => req,
                 Err(error) => {
                     warn!(%error, "Invalid admission request");
-                    return ok(reply::json(&Response::invalid(error).into_review()));
+                    return json_response(AdmissionResponse::invalid(error).into_review());
                 }
             };
             debug!(?req);
 
-            let rsp = Response::from(&req);
+            let rsp = AdmissionResponse::from(&req);
 
             // Parse the server instance under review before doing anything with the API--i.e., if
             // this fails we don't have to waste the API calls.
@@ -31,7 +59,7 @@ pub fn routes(client: kube::Client) -> BoxedFilter<(impl warp::Reply,)> {
                 Ok(s) => s,
                 Err(error) => {
                     warn!(%error, "Failed to deserialize server from admission request");
-                    return ok(reply::json(&Response::invalid(error).into_review()));
+                    return json_response(AdmissionResponse::invalid(error).into_review());
                 }
             };
 
@@ -44,7 +72,10 @@ pub fn routes(client: kube::Client) -> BoxedFilter<(impl warp::Reply,)> {
                 Ok(servers) => servers,
                 Err(error) => {
                     warn!(%error, "Failed to list servers");
-                    return ok(http::StatusCode::INTERNAL_SERVER_ERROR);
+                    return Ok(Response::builder()
+                        .status(http::StatusCode::INTERNAL_SERVER_ERROR)
+                        .body(Body::empty())
+                        .expect("error response must be valid"));
                 }
             };
 
@@ -57,23 +88,27 @@ pub fn routes(client: kube::Client) -> BoxedFilter<(impl warp::Reply,)> {
                 }
             };
             debug!(?rsp);
-            ok(reply::json(&rsp.into_review()))
+            json_response(rsp.into_review())
         })
-        .with(warp::trace::request())
-        .boxed()
+    }
 }
 
-type Review = kube::core::admission::AdmissionReview<DynamicObject>;
-type Request = kube::core::admission::AdmissionRequest<DynamicObject>;
-type Response = kube::core::admission::AdmissionResponse;
-
-#[inline]
-fn ok(reply: impl warp::Reply + 'static) -> Result<Box<dyn warp::Reply>, Infallible> {
-    Ok(Box::new(reply))
+fn json_response(rsp: AdmissionReview) -> Result<Response<Body>> {
+    let bytes = serde_json::to_vec(&rsp).context("failed to encode admission review as JSON")?;
+    Ok(Response::builder()
+        .status(http::StatusCode::OK)
+        .header(http::header::CONTENT_TYPE, "application/json")
+        .body(Body::from(bytes))
+        .expect("admission review response must be valid"))
 }
 
-/// Parses a `Server` instance and it's namespace from the admission request.
-fn parse_server(req: Request) -> Result<(String, String, api::policy::ServerSpec)> {
+type Review = kube::core::admission::AdmissionReview<DynamicObject>;
+type AdmissionRequest = kube::core::admission::AdmissionRequest<DynamicObject>;
+type AdmissionResponse = kube::core::admission::AdmissionResponse;
+type AdmissionReview = kube::core::admission::AdmissionReview<DynamicObject>;
+
+/// Parses a `Server` instance and its namespace from the admission request.
+fn parse_server(req: AdmissionRequest) -> Result<(String, String, api::policy::ServerSpec)> {
     let obj = req.object.ok_or_else(|| anyhow!("missing server"))?;
     let ns = obj
         .namespace()
diff --git a/policy-controller/src/main.rs b/policy-controller/src/main.rs
index d17f753da..2616e8fd8 100644
--- a/policy-controller/src/main.rs
+++ b/policy-controller/src/main.rs
@@ -1,15 +1,21 @@
 #![deny(warnings, rust_2018_idioms)]
 #![forbid(unsafe_code)]
 
-use anyhow::{bail, Context, Error, Result};
+use anyhow::{anyhow, bail, Context, Error, Result};
 use clap::Parser;
 use futures::{future, prelude::*};
 use linkerd_policy_controller::k8s::DefaultPolicy;
 use linkerd_policy_controller::{admin, admission};
 use linkerd_policy_controller_core::IpNet;
-use std::net::SocketAddr;
-use tokio::{sync::watch, time};
-use tracing::{debug, info, info_span, instrument, Instrument};
+use std::{io, net::SocketAddr, sync::Arc};
+use tokio::{
+    fs,
+    net::{TcpListener, TcpStream},
+    sync::watch,
+    time,
+};
+use tokio_rustls::TlsAcceptor;
+use tracing::{debug, error, info, info_span, instrument, Instrument};
 use tracing_subscriber::{fmt::format, prelude::*, EnvFilter};
 
 #[cfg(all(target_os = "linux", target_arch = "x86_64", target_env = "gnu"))]
@@ -111,17 +117,10 @@ async fn main() -> Result<()> {
     // Run the gRPC server, serving results by looking up against the index handle.
     tokio::spawn(grpc(grpc_addr, cluster_networks, handle, drain_rx.clone()));
 
-    // Run the admission controller
     if let Some(bind_addr) = admission_addr {
-        let (listen_addr, serve) = warp::serve(admission::routes(client))
-            .tls()
-            .cert_path("/var/run/linkerd/tls/tls.crt")
-            .key_path("/var/run/linkerd/tls/tls.key")
-            .bind_with_graceful_shutdown(bind_addr, async move {
-                let _ = drain_rx.signaled().await;
-            });
-        info!(addr = %listen_addr, "Admission controller server listening");
+        let (listen_addr, serve) = bind_admission_controller(bind_addr, client).await?;
         tokio::spawn(serve.instrument(info_span!("admission")));
+        info!(addr = %listen_addr, "Admission controller server listening");
     }
 
     // Block the main thread on the shutdown signal. Once it fires, wait for the background tasks to
@@ -232,3 +231,108 @@ impl std::str::FromStr for LogFormat {
         }
     }
 }
+
+// Load public certificate from file.
+async fn load_certs(filename: &str) -> anyhow::Result<Vec<rustls::Certificate>> {
+    // Open certificate file.
+    let pem = fs::read(filename).await?;
+    let mut reader = io::BufReader::new(pem.as_slice());
+
+    // Load and return certificate.
+    let certs = rustls_pemfile::certs(&mut reader)?;
+    Ok(certs.into_iter().map(rustls::Certificate).collect())
+}
+
+// Load private key from file.
+async fn load_private_key(filename: &str) -> anyhow::Result<rustls::PrivateKey> {
+    // Open keyfile.
+    let pem = fs::read(filename).await?;
+    let mut reader = io::BufReader::new(pem.as_slice());
+
+    // Load and return a single private key.
+    let keys = rustls_pemfile::rsa_private_keys(&mut reader)?;
+    if keys.len() != 1 {
+        return Err(anyhow!("expected a single private key"));
+    }
+
+    Ok(rustls::PrivateKey(keys[0].clone()))
+}
+
+/// Bind the specified address and serve the admission controller using the
+/// provided kubernetes client.
+async fn bind_admission_controller(
+    bind_addr: SocketAddr,
+    client: kube::Client,
+) -> Result<(
+    SocketAddr,
+    impl std::future::Future<Output = ()> + Send + 'static,
+)> {
+    let tcp = TcpListener::bind(&bind_addr).await?;
+    let listen_addr = tcp.local_addr()?;
+    // Connection accept loop
+    let serve = async move {
+        loop {
+            let socket = match tcp.accept().await {
+                Ok((socket, _)) => socket,
+                Err(err) => {
+                    error!(%err, "Failed to accept connection");
+                    continue;
+                }
+            };
+            let client_addr = match socket.peer_addr() {
+                Ok(addr) => addr,
+                Err(error) => {
+                    error!(%error, "Failed to get peer address");
+                    continue;
+                }
+            };
+
+            tokio::spawn(
+                serve_admission_controller_conn(socket, client.clone())
+                    .map_err(|error| error!(%error))
+                    .instrument(info_span!("connection", client.addr = %client_addr)),
+            );
+        }
+    };
+
+    Ok((listen_addr, serve))
+}
+
+/// Serve an HTTP server for the admission controller on the given TCP
+/// connection.
+async fn serve_admission_controller_conn(socket: TcpStream, client: kube::Client) -> Result<()> {
+    let tls = {
+        // Load public certificate.
+        let certs = load_certs("/var/run/linkerd/tls/tls.crt")
+            .await
+            .with_context(|| "failed to load certificate")?;
+
+        // Load private key.
+        let key = load_private_key("/var/run/linkerd/tls/tls.key")
+            .await
+            .with_context(|| "failed to load private key")?;
+
+        let mut cfg = rustls::ServerConfig::builder()
+            .with_safe_defaults()
+            // Do not use client certificate authentication.
+            .with_no_client_auth()
+            .with_single_cert(certs, key)
+            .with_context(|| "failed to configure TLS")?;
+
+        // Configure ALPN to accept HTTP/2, HTTP/1.1 in that order.
+        cfg.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
+        TlsAcceptor::from(Arc::new(cfg))
+    };
+
+    let stream = tls.accept(socket).await.with_context(|| "TLS error")?;
+
+    match hyper::server::conn::Http::new()
+        .serve_connection(stream, admission::Service { client })
+        .await
+        .with_context(|| "Connection closed")
+    {
+        Ok(()) => debug!("Connection closed"),
+        Err(error) => info!(%error, "Connection closed"),
+    };
+    Ok(())
+}
diff --git a/viz/charts/linkerd-viz/Chart.yaml b/viz/charts/linkerd-viz/Chart.yaml
index 06de5a85e..3035e905f 100644
--- a/viz/charts/linkerd-viz/Chart.yaml
+++ b/viz/charts/linkerd-viz/Chart.yaml
@@ -11,7 +11,7 @@ kubeVersion: ">=1.20.0-0"
 name: "linkerd-viz"
 sources:
 - https://github.com/linkerd/linkerd2/
-version: 30.0.2-edge
+version: 30.0.3-edge
 icon: https://linkerd.io/images/logo-only-200h.png
 maintainers:
   - name: Linkerd authors
diff --git a/viz/charts/linkerd-viz/README.md b/viz/charts/linkerd-viz/README.md
index f60eb4f08..360f8e3a2 100644
--- a/viz/charts/linkerd-viz/README.md
+++ b/viz/charts/linkerd-viz/README.md
@@ -3,7 +3,7 @@
 The Linkerd-Viz extension contains observability and visualization
 components for Linkerd.
 
-![Version: 30.0.2-edge](https://img.shields.io/badge/Version-30.0.2--edge-informational?style=flat-square)
+![Version: 30.0.3-edge](https://img.shields.io/badge/Version-30.0.3--edge-informational?style=flat-square)
 
 ![AppVersion: edge-XX.X.X](https://img.shields.io/badge/AppVersion-edge--XX.X.X-informational?style=flat-square)
 


Test patch:
diff --git a/cli/cmd/testdata/install-cni-plugin_default.golden b/cli/cmd/testdata/install-cni-plugin_default.golden
index 137c6d7d0..b6ea82f9d 100644
--- a/cli/cmd/testdata/install-cni-plugin_default.golden
+++ b/cli/cmd/testdata/install-cni-plugin_default.golden
@@ -150,6 +150,7 @@ spec:
           name: linkerd-tmp-dir
         securityContext:
           readOnlyRootFilesystem: true
+          privileged: 
       volumes:
       - name: cni-bin-dir
         hostPath:
diff --git a/cli/cmd/testdata/install-cni-plugin_fully_configured.golden b/cli/cmd/testdata/install-cni-plugin_fully_configured.golden
index fdab5acca..475f197a1 100644
--- a/cli/cmd/testdata/install-cni-plugin_fully_configured.golden
+++ b/cli/cmd/testdata/install-cni-plugin_fully_configured.golden
@@ -151,6 +151,7 @@ spec:
           name: linkerd-tmp-dir
         securityContext:
           readOnlyRootFilesystem: true
+          privileged: 
       volumes:
       - name: cni-bin-dir
         hostPath:
diff --git a/cli/cmd/testdata/install-cni-plugin_fully_configured_equal_dsts.golden b/cli/cmd/testdata/install-cni-plugin_fully_configured_equal_dsts.golden
index 268c67e3e..4adb84485 100644
--- a/cli/cmd/testdata/install-cni-plugin_fully_configured_equal_dsts.golden
+++ b/cli/cmd/testdata/install-cni-plugin_fully_configured_equal_dsts.golden
@@ -149,6 +149,7 @@ spec:
           name: linkerd-tmp-dir
         securityContext:
           readOnlyRootFilesystem: true
+          privileged: 
       volumes:
       - name: cni-net-dir
         hostPath:
diff --git a/cli/cmd/testdata/install-cni-plugin_fully_configured_no_namespace.golden b/cli/cmd/testdata/install-cni-plugin_fully_configured_no_namespace.golden
index fdab5acca..475f197a1 100644
--- a/cli/cmd/testdata/install-cni-plugin_fully_configured_no_namespace.golden
+++ b/cli/cmd/testdata/install-cni-plugin_fully_configured_no_namespace.golden
@@ -151,6 +151,7 @@ spec:
           name: linkerd-tmp-dir
         securityContext:
           readOnlyRootFilesystem: true
+          privileged: 
       volumes:
       - name: cni-bin-dir
         hostPath:
diff --git a/cli/cmd/testdata/install-cni-plugin_skip_ports.golden b/cli/cmd/testdata/install-cni-plugin_skip_ports.golden
index 6b5b60659..87d8c777c 100644
--- a/cli/cmd/testdata/install-cni-plugin_skip_ports.golden
+++ b/cli/cmd/testdata/install-cni-plugin_skip_ports.golden
@@ -151,6 +151,7 @@ spec:
           name: linkerd-tmp-dir
         securityContext:
           readOnlyRootFilesystem: true
+          privileged: 
       volumes:
       - name: cni-bin-dir
         hostPath:
diff --git a/cli/cmd/testdata/install_cni_helm_default_output.golden b/cli/cmd/testdata/install_cni_helm_default_output.golden
index a3e9a3fc9..7c3206c82 100644
--- a/cli/cmd/testdata/install_cni_helm_default_output.golden
+++ b/cli/cmd/testdata/install_cni_helm_default_output.golden
@@ -142,6 +142,7 @@ spec:
           name: linkerd-tmp-dir
         securityContext:
           readOnlyRootFilesystem: true
+          privileged: 
       volumes:
       - name: cni-bin-dir
         hostPath:
diff --git a/cli/cmd/testdata/install_cni_helm_override_output.golden b/cli/cmd/testdata/install_cni_helm_override_output.golden
index 075c208d7..877911c9e 100644
--- a/cli/cmd/testdata/install_cni_helm_override_output.golden
+++ b/cli/cmd/testdata/install_cni_helm_override_output.golden
@@ -143,6 +143,7 @@ spec:
           name: linkerd-tmp-dir
         securityContext:
           readOnlyRootFilesystem: true
+          privileged: 
       volumes:
       - name: cni-bin-dir
         hostPath:
diff --git a/cli/cmd/testdata/install_controlplane_tracing_output.golden b/cli/cmd/testdata/install_controlplane_tracing_output.golden
index 66d55946d..2b95167c5 100644
--- a/cli/cmd/testdata/install_controlplane_tracing_output.golden
+++ b/cli/cmd/testdata/install_controlplane_tracing_output.golden
@@ -6,7 +6,7 @@ metadata:
   annotations:
     linkerd.io/created-by: linkerd/cli dev-undefined
   labels:
-    helm.sh/chart: linkerd-control-plane-1.1.2-edge
+    helm.sh/chart: linkerd-control-plane-1.1.3-edge
     linkerd.io/control-plane-ns: linkerd
 spec:
   group: policy.linkerd.io
@@ -152,7 +152,7 @@ metadata:
   annotations:
     linkerd.io/created-by: linkerd/cli dev-undefined
   labels:
-    helm.sh/chart: linkerd-control-plane-1.1.2-edge
+    helm.sh/chart: linkerd-control-plane-1.1.3-edge
     linkerd.io/control-plane-ns: linkerd
 spec:
   group: policy.linkerd.io
@@ -441,7 +441,7 @@ metadata:
   annotations:
     linkerd.io/created-by: linkerd/cli dev-undefined
   labels:
-    helm.sh/chart: linkerd-control-plane-1.1.2-edge
+    helm.sh/chart: linkerd-control-plane-1.1.3-edge
     linkerd.io/control-plane-ns: linkerd
 spec:
   group: linkerd.io
diff --git a/cli/cmd/testdata/install_custom_domain.golden b/cli/cmd/testdata/install_custom_domain.golden
index e18e8a8d7..e74b1aaf9 100644
--- a/cli/cmd/testdata/install_custom_domain.golden
+++ b/cli/cmd/testdata/install_custom_domain.golden
@@ -6,7 +6,7 @@ metadata:
   annotations:
     linkerd.io/created-by: linkerd/cli dev-undefined
   labels:
-    helm.sh/chart: linkerd-control-plane-1.1.2-edge
+    helm.sh/chart: linkerd-control-plane-1.1.3-edge
     linkerd.io/control-plane-ns: linkerd
 spec:
   group: policy.linkerd.io
@@ -152,7 +152,7 @@ metadata:
   annotations:
     linkerd.io/created-by: linkerd/cli dev-undefined
   labels:
-    helm.sh/chart: linkerd-control-plane-1.1.2-edge
+    helm.sh/chart: linkerd-control-plane-1.1.3-edge
     linkerd.io/control-plane-ns: linkerd
 spec:
   group: policy.linkerd.io
@@ -441,7 +441,7 @@ metadata:
   annotations:
     linkerd.io/created-by: linkerd/cli dev-undefined
   labels:
-    helm.sh/chart: linkerd-control-plane-1.1.2-edge
+    helm.sh/chart: linkerd-control-plane-1.1.3-edge
     linkerd.io/control-plane-ns: linkerd
 spec:
   group: linkerd.io
diff --git a/cli/cmd/testdata/install_custom_registry.golden b/cli/cmd/testdata/install_custom_registry.golden
index 0b1654dfa..84e0e2298 100644
--- a/cli/cmd/testdata/install_custom_registry.golden
+++ b/cli/cmd/testdata/install_custom_registry.golden
@@ -6,7 +6,7 @@ metadata:
   annotations:
     linkerd.io/created-by: linkerd/cli dev-undefined
   labels:
-    helm.sh/chart: linkerd-control-plane-1.1.2-edge
+    helm.sh/chart: linkerd-control-plane-1.1.3-edge
     linkerd.io/control-plane-ns: linkerd
 spec:
   group: policy.linkerd.io
@@ -152,7 +152,7 @@ metadata:
   annotations:
     linkerd.io/created-by: linkerd/cli dev-undefined
   labels:
-    helm.sh/chart: linkerd-control-plane-1.1.2-edge
+    helm.sh/chart: linkerd-control-plane-1.1.3-edge
     linkerd.io/control-plane-ns: linkerd
 spec:
   group: policy.linkerd.io
@@ -441,7 +441,7 @@ metadata:
   annotations:
     linkerd.io/created-by: linkerd/cli dev-undefined
   labels:
-    helm.sh/chart: linkerd-control-plane-1.1.2-edge
+    helm.sh/chart: linkerd-control-plane-1.1.3-edge
     linkerd.io/control-plane-ns: linkerd
 spec:
   group: linkerd.io
diff --git a/cli/cmd/testdata/install_default.golden b/cli/cmd/testdata/install_default.golden
index e18e8a8d7..e74b1aaf9 100644
--- a/cli/cmd/testdata/install_default.golden
+++ b/cli/cmd/testdata/install_default.golden
@@ -6,7 +6,7 @@ metadata:
   annotations:
     linkerd.io/created-by: linkerd/cli dev-undefined
   labels:
-    helm.sh/chart: linkerd-control-plane-1.1.2-edge
+    helm.sh/chart: linkerd-control-plane-1.1.3-edge
     linkerd.io/control-plane-ns: linkerd
 spec:
   group: policy.linkerd.io
@@ -152,7 +152,7 @@ metadata:
   annotations:
     linkerd.io/created-by: linkerd/cli dev-undefined
   labels:
-    helm.sh/chart: linkerd-control-plane-1.1.2-edge
+    helm.sh/chart: linkerd-control-plane-1.1.3-edge
     linkerd.io/control-plane-ns: linkerd
 spec:
   group: policy.linkerd.io
@@ -441,7 +441,7 @@ metadata:
   annotations:
     linkerd.io/created-by: linkerd/cli dev-undefined
   labels:
-    helm.sh/chart: linkerd-control-plane-1.1.2-edge
+    helm.sh/chart: linkerd-control-plane-1.1.3-edge
     linkerd.io/control-plane-ns: linkerd
 spec:
   group: linkerd.io
diff --git a/cli/cmd/testdata/install_default_override_dst_get_nets.golden b/cli/cmd/testdata/install_default_override_dst_get_nets.golden
index 2e9a0cce4..21c8502b3 100644
--- a/cli/cmd/testdata/install_default_override_dst_get_nets.golden
+++ b/cli/cmd/testdata/install_default_override_dst_get_nets.golden
@@ -6,7 +6,7 @@ metadata:
   annotations:
     linkerd.io/created-by: linkerd/cli dev-undefined
   labels:
-    helm.sh/chart: linkerd-control-plane-1.1.2-edge
+    helm.sh/chart: linkerd-control-plane-1.1.3-edge
     linkerd.io/control-plane-ns: linkerd
 spec:
   group: policy.linkerd.io
@@ -152,7 +152,7 @@ metadata:
   annotations:
     linkerd.io/created-by: linkerd/cli dev-undefined
   labels:
-    helm.sh/chart: linkerd-control-plane-1.1.2-edge
+    helm.sh/chart: linkerd-control-plane-1.1.3-edge
     linkerd.io/control-plane-ns: linkerd
 spec:
   group: policy.linkerd.io
@@ -441,7 +441,7 @@ metadata:
   annotations:
     linkerd.io/created-by: linkerd/cli dev-undefined
   labels:
-    helm.sh/chart: linkerd-control-plane-1.1.2-edge
+    helm.sh/chart: linkerd-control-plane-1.1.3-edge
     linkerd.io/control-plane-ns: linkerd
 spec:
   group: linkerd.io
diff --git a/cli/cmd/testdata/install_default_token.golden b/cli/cmd/testdata/install_default_token.golden
index 601ad4205..891b8b8ee 100644
--- a/cli/cmd/testdata/install_default_token.golden
+++ b/cli/cmd/testdata/install_default_token.golden
@@ -6,7 +6,7 @@ metadata:
   annotations:
     linkerd.io/created-by: linkerd/cli dev-undefined
   labels:
-    helm.sh/chart: linkerd-control-plane-1.1.2-edge
+    helm.sh/chart: linkerd-control-plane-1.1.3-edge
     linkerd.io/control-plane-ns: linkerd
 spec:
   group: policy.linkerd.io
@@ -152,7 +152,7 @@ metadata:
   annotations:
     linkerd.io/created-by: linkerd/cli dev-undefined
   labels:
-    helm.sh/chart: linkerd-control-plane-1.1.2-edge
+    helm.sh/chart: linkerd-control-plane-1.1.3-edge
     linkerd.io/control-plane-ns: linkerd
 spec:
   group: policy.linkerd.io
@@ -441,7 +441,7 @@ metadata:
   annotations:
     linkerd.io/created-by: linkerd/cli dev-undefined
   labels:
-    helm.sh/chart: linkerd-control-plane-1.1.2-edge
+    helm.sh/chart: linkerd-control-plane-1.1.3-edge
     linkerd.io/control-plane-ns: linkerd
 spec:
   group: linkerd.io
diff --git a/cli/cmd/testdata/install_ha_output.golden b/cli/cmd/testdata/install_ha_output.golden
index 5a865a584..09e43e211 100644
--- a/cli/cmd/testdata/install_ha_output.golden
+++ b/cli/cmd/testdata/install_ha_output.golden
@@ -6,7 +6,7 @@ metadata:
   annotations:
     linkerd.io/created-by: linkerd/cli dev-undefined
   labels:
-    helm.sh/chart: linkerd-control-plane-1.1.2-edge
+    helm.sh/chart: linkerd-control-plane-1.1.3-edge
     linkerd.io/control-plane-ns: linkerd
 spec:
   group: policy.linkerd.io
@@ -152,7 +152,7 @@ metadata:
   annotations:
     linkerd.io/created-by: linkerd/cli dev-undefined
   labels:
-    helm.sh/chart: linkerd-control-plane-1.1.2-edge
+    helm.sh/chart: linkerd-control-plane-1.1.3-edge
     linkerd.io/control-plane-ns: linkerd
 spec:
   group: policy.linkerd.io
@@ -441,7 +441,7 @@ metadata:
   annotations:
     linkerd.io/created-by: linkerd/cli dev-undefined
   labels:
-    helm.sh/chart: linkerd-control-plane-1.1.2-edge
+    helm.sh/chart: linkerd-control-plane-1.1.3-edge
     linkerd.io/control-plane-ns: linkerd
 spec:
   group: linkerd.io
diff --git a/cli/cmd/testdata/install_ha_with_overrides_output.golden b/cli/cmd/testdata/install_ha_with_overrides_output.golden
index 0f658dc98..aac99c693 100644
--- a/cli/cmd/testdata/install_ha_with_overrides_output.golden
+++ b/cli/cmd/testdata/install_ha_with_overrides_output.golden
@@ -6,7 +6,7 @@ metadata:
   annotations:
     linkerd.io/created-by: linkerd/cli dev-undefined
   labels:
-    helm.sh/chart: linkerd-control-plane-1.1.2-edge
+    helm.sh/chart: linkerd-control-plane-1.1.3-edge
     linkerd.io/control-plane-ns: linkerd
 spec:
   group: policy.linkerd.io
@@ -152,7 +152,7 @@ metadata:
   annotations:
     linkerd.io/created-by: linkerd/cli dev-undefined
   labels:
-    helm.sh/chart: linkerd-control-plane-1.1.2-edge
+    helm.sh/chart: linkerd-control-plane-1.1.3-edge
     linkerd.io/control-plane-ns: linkerd
 spec:
   group: policy.linkerd.io
@@ -441,7 +441,7 @@ metadata:
   annotations:
     linkerd.io/created-by: linkerd/cli dev-undefined
   labels:
-    helm.sh/chart: linkerd-control-plane-1.1.2-edge
+    helm.sh/chart: linkerd-control-plane-1.1.3-edge
     linkerd.io/control-plane-ns: linkerd
 spec:
   group: linkerd.io
diff --git a/cli/cmd/testdata/install_heartbeat_disabled_output.golden b/cli/cmd/testdata/install_heartbeat_disabled_output.golden
index 8bf580c89..166a4db04 100644
--- a/cli/cmd/testdata/install_heartbeat_disabled_output.golden
+++ b/cli/cmd/testdata/install_heartbeat_disabled_output.golden
@@ -6,7 +6,7 @@ metadata:
   annotations:
     linkerd.io/created-by: linkerd/cli dev-undefined
   labels:
-    helm.sh/chart: linkerd-control-plane-1.1.2-edge
+    helm.sh/chart: linkerd-control-plane-1.1.3-edge
     linkerd.io/control-plane-ns: linkerd
 spec:
   group: policy.linkerd.io
@@ -152,7 +152,7 @@ metadata:
   annotations:
     linkerd.io/created-by: linkerd/cli dev-undefined
   labels:
-    helm.sh/chart: linkerd-control-plane-1.1.2-edge
+    helm.sh/chart: linkerd-control-plane-1.1.3-edge
     linkerd.io/control-plane-ns: linkerd
 spec:
   group: policy.linkerd.io
@@ -441,7 +441,7 @@ metadata:
   annotations:
     linkerd.io/created-by: linkerd/cli dev-undefined
   labels:
-    helm.sh/chart: linkerd-control-plane-1.1.2-edge
+    helm.sh/chart: linkerd-control-plane-1.1.3-edge
     linkerd.io/control-plane-ns: linkerd
 spec:
   group: linkerd.io
diff --git a/cli/cmd/testdata/install_no_init_container.golden b/cli/cmd/testdata/install_no_init_container.golden
index 998bd70e0..1507a2ade 100644
--- a/cli/cmd/testdata/install_no_init_container.golden
+++ b/cli/cmd/testdata/install_no_init_container.golden
@@ -6,7 +6,7 @@ metadata:
   annotations:
     linkerd.io/created-by: linkerd/cli dev-undefined
   labels:
-    helm.sh/chart: linkerd-control-plane-1.1.2-edge
+    helm.sh/chart: linkerd-control-plane-1.1.3-edge
     linkerd.io/control-plane-ns: linkerd
 spec:
   group: policy.linkerd.io
@@ -152,7 +152,7 @@ metadata:
   annotations:
     linkerd.io/created-by: linkerd/cli dev-undefined
   labels:
-    helm.sh/chart: linkerd-control-plane-1.1.2-edge
+    helm.sh/chart: linkerd-control-plane-1.1.3-edge
     linkerd.io/control-plane-ns: linkerd
 spec:
   group: policy.linkerd.io
@@ -441,7 +441,7 @@ metadata:
   annotations:
     linkerd.io/created-by: linkerd/cli dev-undefined
   labels:
-    helm.sh/chart: linkerd-control-plane-1.1.2-edge
+    helm.sh/chart: linkerd-control-plane-1.1.3-edge
     linkerd.io/control-plane-ns: linkerd
 spec:
   group: linkerd.io
diff --git a/cli/cmd/testdata/install_output.golden b/cli/cmd/testdata/install_output.golden
index 8819d2150..74e91791b 100644
--- a/cli/cmd/testdata/install_output.golden
+++ b/cli/cmd/testdata/install_output.golden
@@ -6,7 +6,7 @@ metadata:
   annotations:
     linkerd.io/created-by: CliVersion
   labels:
-    helm.sh/chart: linkerd-control-plane-1.1.2-edge
+    helm.sh/chart: linkerd-control-plane-1.1.3-edge
     linkerd.io/control-plane-ns: linkerd
 spec:
   group: policy.linkerd.io
@@ -152,7 +152,7 @@ metadata:
   annotations:
     linkerd.io/created-by: CliVersion
   labels:
-    helm.sh/chart: linkerd-control-plane-1.1.2-edge
+    helm.sh/chart: linkerd-control-plane-1.1.3-edge
     linkerd.io/control-plane-ns: linkerd
 spec:
   group: policy.linkerd.io
@@ -441,7 +441,7 @@ metadata:
   annotations:
     linkerd.io/created-by: CliVersion
   labels:
-    helm.sh/chart: linkerd-control-plane-1.1.2-edge
+    helm.sh/chart: linkerd-control-plane-1.1.3-edge
     linkerd.io/control-plane-ns: linkerd
 spec:
   group: linkerd.io
diff --git a/cli/cmd/testdata/install_proxy_ignores.golden b/cli/cmd/testdata/install_proxy_ignores.golden
index 01e4940d1..f0267a6c8 100644
--- a/cli/cmd/testdata/install_proxy_ignores.golden
+++ b/cli/cmd/testdata/install_proxy_ignores.golden
@@ -6,7 +6,7 @@ metadata:
   annotations:
     linkerd.io/created-by: linkerd/cli dev-undefined
   labels:
-    helm.sh/chart: linkerd-control-plane-1.1.2-edge
+    helm.sh/chart: linkerd-control-plane-1.1.3-edge
     linkerd.io/control-plane-ns: linkerd
 spec:
   group: policy.linkerd.io
@@ -152,7 +152,7 @@ metadata:
   annotations:
     linkerd.io/created-by: linkerd/cli dev-undefined
   labels:
-    helm.sh/chart: linkerd-control-plane-1.1.2-edge
+    helm.sh/chart: linkerd-control-plane-1.1.3-edge
     linkerd.io/control-plane-ns: linkerd
 spec:
   group: policy.linkerd.io
@@ -441,7 +441,7 @@ metadata:
   annotations:
     linkerd.io/created-by: linkerd/cli dev-undefined
   labels:
-    helm.sh/chart: linkerd-control-plane-1.1.2-edge
+    helm.sh/chart: linkerd-control-plane-1.1.3-edge
     linkerd.io/control-plane-ns: linkerd
 spec:
   group: linkerd.io
diff --git a/cli/cmd/testdata/install_values_file.golden b/cli/cmd/testdata/install_values_file.golden
index c666c40fa..905d71acd 100644
--- a/cli/cmd/testdata/install_values_file.golden
+++ b/cli/cmd/testdata/install_values_file.golden
@@ -6,7 +6,7 @@ metadata:
   annotations:
     linkerd.io/created-by: linkerd/cli dev-undefined
   labels:
-    helm.sh/chart: linkerd-control-plane-1.1.2-edge
+    helm.sh/chart: linkerd-control-plane-1.1.3-edge
     linkerd.io/control-plane-ns: linkerd
 spec:
   group: policy.linkerd.io
@@ -152,7 +152,7 @@ metadata:
   annotations:
     linkerd.io/created-by: linkerd/cli dev-undefined
   labels:
-    helm.sh/chart: linkerd-control-plane-1.1.2-edge
+    helm.sh/chart: linkerd-control-plane-1.1.3-edge
     linkerd.io/control-plane-ns: linkerd
 spec:
   group: policy.linkerd.io
@@ -441,7 +441,7 @@ metadata:
   annotations:
     linkerd.io/created-by: linkerd/cli dev-undefined
   labels:
-    helm.sh/chart: linkerd-control-plane-1.1.2-edge
+    helm.sh/chart: linkerd-control-plane-1.1.3-edge
     linkerd.io/control-plane-ns: linkerd
 spec:
   group: linkerd.io
diff --git a/test/integration/install_test.go b/test/integration/install_test.go
index 964713870..715d1894f 100644
--- a/test/integration/install_test.go
+++ b/test/integration/install_test.go
@@ -57,28 +57,6 @@ var (
 		{Namespace: "linkerd-multicluster", Name: "linkerd-gateway"},
 	}
 
-	injectionCases = []struct {
-		ns          string
-		annotations map[string]string
-		injectArgs  []string
-	}{
-		{
-			ns: "smoke-test",
-			annotations: map[string]string{
-				k8s.ProxyInjectAnnotation: k8s.ProxyInjectEnabled,
-			},
-			injectArgs: nil,
-		},
-		{
-			ns:         "smoke-test-manual",
-			injectArgs: []string{"--manual"},
-		},
-		{
-			ns:         "smoke-test-ann",
-			injectArgs: []string{},
-		},
-	}
-
 	//skippedInboundPorts lists some ports to be marked as skipped, which will
 	// be verified in test/integration/inject
 	skippedInboundPorts       = "1234,5678"
@@ -1115,124 +1093,6 @@ func TestDashboard(t *testing.T) {
 	}
 }
 
-func TestInject(t *testing.T) {
-	resources, err := testutil.ReadFile("testdata/smoke_test.yaml")
-	if err != nil {
-		testutil.AnnotatedFatalf(t, "failed to read smoke test file",
-			"failed to read smoke test file: %s", err)
-	}
-
-	ctx := context.Background()
-	for _, tc := range injectionCases {
-		tc := tc // pin
-		t.Run(tc.ns, func(t *testing.T) {
-			var out string
-
-			prefixedNs := TestHelper.GetTestNamespace(tc.ns)
-
-			err := TestHelper.CreateDataPlaneNamespaceIfNotExists(ctx, prefixedNs, tc.annotations)
-			if err != nil {
-				testutil.AnnotatedFatalf(t, fmt.Sprintf("failed to create %s namespace", prefixedNs),
-					"failed to create %s namespace: %s", prefixedNs, err)
-			}
-
-			if tc.injectArgs != nil {
-				cmd := []string{"inject"}
-				cmd = append(cmd, tc.injectArgs...)
-				cmd = append(cmd, "testdata/smoke_test.yaml")
-
-				var injectReport string
-				out, injectReport, err = TestHelper.PipeToLinkerdRun("", cmd...)
-				if err != nil {
-					testutil.AnnotatedFatalf(t, "'linkerd inject' command failed",
-						"'linkerd inject' command failed: %s\n%s", err, out)
-				}
-
-				err = TestHelper.ValidateOutput(injectReport, "inject.report.golden")
-				if err != nil {
-					testutil.AnnotatedFatalf(t, "received unexpected output",
-						"received unexpected output\n%s", err.Error())
-				}
-			} else {
-				out = resources
-			}
-
-			out, err = TestHelper.KubectlApply(out, prefixedNs)
-			if err != nil {
-				testutil.AnnotatedFatalf(t, "'kubectl apply' command failed",
-					"'kubectl apply' command failed\n%s", out)
-			}
-
-			for _, deploy := range []string{"smoke-test-terminus", "smoke-test-gateway"} {
-				if err := TestHelper.CheckPods(ctx, prefixedNs, deploy, 1); err != nil {
-					if rce, ok := err.(*testutil.RestartCountError); ok {
-						testutil.AnnotatedWarn(t, "CheckPods timed-out", rce)
-					} else {
-						testutil.AnnotatedFatal(t, "CheckPods timed-out", err)
-					}
-				}
-			}
-
-			url, err := TestHelper.URLFor(ctx, prefixedNs, "smoke-test-gateway", 8080)
-			if err != nil {
-				testutil.AnnotatedFatalf(t, "failed to get URL",
-					"failed to get URL: %s", err)
-			}
-
-			output, err := TestHelper.HTTPGetURL(url)
-			if err != nil {
-				testutil.AnnotatedFatalf(t, "unexpected error",
-					"unexpected error: %v %s", err, output)
-			}
-
-			expectedStringInPayload := "\"payload\":\"BANANA\""
-			if !strings.Contains(output, expectedStringInPayload) {
-				testutil.AnnotatedFatalf(t, "application response doesn't contain the expected response",
-					"expected application response to contain string [%s], but it was [%s]",
-					expectedStringInPayload, output)
-			}
-		})
-	}
-}
-
-func TestServiceProfileDeploy(t *testing.T) {
-	bbProto, err := TestHelper.HTTPGetURL("https://raw.githubusercontent.com/BuoyantIO/bb/v0.0.5/api.proto")
-	if err != nil {
-		testutil.AnnotatedFatalf(t, "unexpected error",
-			"unexpected error: %v %s", err, bbProto)
-	}
-
-	for _, tc := range injectionCases {
-		tc := tc // pin
-		t.Run(tc.ns, func(t *testing.T) {
-			prefixedNs := TestHelper.GetTestNamespace(tc.ns)
-
-			cmd := []string{"profile", "-n", prefixedNs, "--proto", "-", "smoke-test-terminus-svc"}
-			bbSP, stderr, err := TestHelper.PipeToLinkerdRun(bbProto, cmd...)
-			if err != nil {
-				testutil.AnnotatedFatalf(t, "unexpected error",
-					"unexpected error: %v %s", err, stderr)
-			}
-
-			out, err := TestHelper.KubectlApply(bbSP, prefixedNs)
-			if err != nil {
-				testutil.AnnotatedFatalf(t, "'kubectl apply' command failed",
-					"'kubectl apply' command failed: %s\n%s", err, out)
-			}
-		})
-	}
-}
-
-func TestCheckProxy(t *testing.T) {
-	for _, tc := range injectionCases {
-		tc := tc // pin
-		t.Run(tc.ns, func(t *testing.T) {
-			prefixedNs := TestHelper.GetTestNamespace(tc.ns)
-			testCheckCommand(t, "proxy", TestHelper.GetVersion(), prefixedNs, "")
-		})
-	}
-}
-
 func TestRestarts(t *testing.T) {
 	expectedDeployments := testutil.LinkerdDeployReplicasEdge
 	if !TestHelper.ExternalPrometheus() {
@@ -1249,20 +1109,6 @@ func TestRestarts(t *testing.T) {
 	}
 }
 
-// TestCleanUp deletes the resources used in the above tests
-func TestCleanUp(t *testing.T) {
-	ctx := context.Background()
-	for _, tc := range injectionCases {
-		tc := tc // pin
-		t.Run(tc.ns, func(t *testing.T) {
-			prefixedNs := TestHelper.GetTestNamespace(tc.ns)
-			if err := TestHelper.DeleteNamespaceIfExists(ctx, prefixedNs); err != nil {
-				testutil.AnnotatedFatal(t, "Deleting namespace failed", err)
-			}
-		})
-	}
-}
-
 func TestCheckMulticluster(t *testing.T) {
 	if TestHelper.GetMulticlusterHelmReleaseName() != "" || TestHelper.Multicluster() {
 		cmd := []string{"multicluster", "check", "--wait=60m"}
diff --git a/test/integration/smoke/install_smoke_test.go b/test/integration/smoke/install_smoke_test.go
new file mode 100644
index 000000000..d31cb764c
--- /dev/null
+++ b/test/integration/smoke/install_smoke_test.go
@@ -0,0 +1,146 @@
+package smoke
+
+import (
+	"bytes"
+	"context"
+	"fmt"
+	"html/template"
+	"os"
+	"strings"
+	"testing"
+	"time"
+
+	"github.com/linkerd/linkerd2/pkg/healthcheck"
+	"github.com/linkerd/linkerd2/pkg/version"
+	"github.com/linkerd/linkerd2/testutil"
+)
+
+//////////////////////
+///   TEST SETUP   ///
+//////////////////////
+
+var (
+	TestHelper *testutil.TestHelper
+)
+
+func TestMain(m *testing.M) {
+	TestHelper = testutil.NewTestHelper()
+	if TestHelper.CNI() {
+		os.Exit(0)
+	}
+	os.Exit(m.Run())
+}
+
+//////////////////////
+/// TEST EXECUTION ///
+//////////////////////
+
+func TestSmoke(t *testing.T) {
+	ctx := context.Background()
+	TestHelper.WithDataPlaneNamespace(ctx, "smoke-test", map[string]string{}, t, func(t *testing.T, ns string) {
+		cmd := []string{"inject", "testdata/smoke_test.yaml"}
+		out, injectReport, err := TestHelper.PipeToLinkerdRun("", cmd...)
+		if err != nil {
+			testutil.AnnotatedFatalf(t, "'linkerd inject' command failed",
+				"'linkerd inject' command failed: %s\n%s", err, out)
+		}
+
+		err = TestHelper.ValidateOutput(injectReport, "inject.report.golden")
+		if err != nil {
+			testutil.AnnotatedFatalf(t, "received unexpected output",
+				"received unexpected output\n%s", err.Error())
+		}
+
+		out, err = TestHelper.KubectlApply(out, ns)
+		if err != nil {
+			testutil.AnnotatedFatalf(t, "'kubectl apply' command failed",
+				"'kubectl apply' command failed\n%s", out)
+		}
+
+		// Wait for pods to in smoke-test dpeloyment to come up
+		for _, deploy := range []string{"smoke-test-terminus", "smoke-test-gateway"} {
+			if err := TestHelper.CheckPods(ctx, ns, deploy, 1); err != nil {
+				if rce, ok := err.(*testutil.RestartCountError); ok {
+					testutil.AnnotatedWarn(t, "CheckPods timed-out", rce)
+				} else {
+					testutil.AnnotatedFatal(t, "CheckPods timed-out", err)
+				}
+			}
+		}
+
+		// Test 'linkerd check --proxy' with the current image version
+		cmd = []string{"check", "--proxy", "--expected-version", TestHelper.GetVersion(), "--namespace", ns, "--wait=60m"}
+		expected := getCheckOutput(t, "check.proxy.golden", TestHelper.GetLinkerdNamespace())
+
+		// Use a short time window for check tests to get rid of transient
+		// errors
+		timeout := 5 * time.Minute
+		err = TestHelper.RetryFor(timeout, func() error {
+			out, err := TestHelper.LinkerdRun(cmd...)
+			if err != nil {
+				return fmt.Errorf("'linkerd check' command failed\n%s\n%s", err, out)
+			}
+
+			if !strings.Contains(out, expected) {
+				return fmt.Errorf(
+					"Expected:\n%s\nActual:\n%s", expected, out)
+			}
+
+			return nil
+		})
+
+		if err != nil {
+			testutil.AnnotatedFatal(t, fmt.Sprintf("'linkerd check' command timed-out (%s)", timeout), err)
+		}
+
+		// Test traffic from smoke-test client to server
+		url, err := TestHelper.URLFor(ctx, ns, "smoke-test-gateway", 8080)
+		if err != nil {
+			testutil.AnnotatedFatalf(t, "failed to get URL",
+				"failed to get URL: %s", err)
+		}
+
+		output, err := TestHelper.HTTPGetURL(url)
+		if err != nil {
+			testutil.AnnotatedFatalf(t, "unexpected error",
+				"unexpected error: %v %s", err, output)
+		}
+
+		expectedStringInPayload := "\"payload\":\"BANANA\""
+		if !strings.Contains(output, expectedStringInPayload) {
+			testutil.AnnotatedFatalf(t, "application response doesn't contain the expected response",
+				"expected application response to contain string [%s], but it was [%s]",
+				expectedStringInPayload, output)
+		}
+	})
+
+}
+
+func getCheckOutput(t *testing.T, goldenFile string, namespace string) string {
+	pods, err := TestHelper.KubernetesHelper.GetPods(context.Background(), namespace, nil)
+	if err != nil {
+		testutil.AnnotatedFatal(t, fmt.Sprintf("failed to retrieve pods: %s", err), err)
+	}
+
+	proxyVersionErr := ""
+	err = healthcheck.CheckProxyVersionsUpToDate(pods, version.Channels{})
+	if err != nil {
+		proxyVersionErr = err.Error()
+	}
+
+	tpl := template.Must(template.ParseFiles("testdata" + "/" + goldenFile))
+	vars := struct {
+		ProxyVersionErr string
+		HintURL         string
+	}{
+		proxyVersionErr,
+		healthcheck.HintBaseURL(TestHelper.GetVersion()),
+	}
+
+	var expected bytes.Buffer
+	if err := tpl.Execute(&expected, vars); err != nil {
+		testutil.AnnotatedFatal(t, fmt.Sprintf("failed to parse %s template: %s", goldenFile, err), err)
+	}
+
+	return expected.String()
+}
diff --git a/test/integration/smoke/testdata/check.proxy.golden b/test/integration/smoke/testdata/check.proxy.golden
new file mode 100644
index 000000000..8ce22e02f
--- /dev/null
+++ b/test/integration/smoke/testdata/check.proxy.golden
@@ -0,0 +1,78 @@
+Linkerd core checks
+===================
+
+kubernetes-api
+--------------
+√ can initialize the client
+√ can query the Kubernetes API
+
+kubernetes-version
+------------------
+√ is running the minimum Kubernetes API version
+√ is running the minimum kubectl version
+
+linkerd-existence
+-----------------
+√ 'linkerd-config' config map exists
+√ heartbeat ServiceAccount exist
+√ control plane replica sets are ready
+√ no unschedulable pods
+√ control plane pods are ready
+√ cluster networks contains all node podCIDRs
+
+linkerd-config
+--------------
+√ control plane Namespace exists
+√ control plane ClusterRoles exist
+√ control plane ClusterRoleBindings exist
+√ control plane ServiceAccounts exist
+√ control plane CustomResourceDefinitions exist
+√ control plane MutatingWebhookConfigurations exist
+√ control plane ValidatingWebhookConfigurations exist
+√ proxy-init container runs as root user if docker container runtime is used
+
+linkerd-identity
+----------------
+√ certificate config is valid
+√ trust anchors are using supported crypto algorithm
+√ trust anchors are within their validity period
+√ trust anchors are valid for at least 60 days
+√ issuer cert is using supported crypto algorithm
+√ issuer cert is within its validity period
+√ issuer cert is valid for at least 60 days
+√ issuer cert is issued by the trust anchor
+
+linkerd-webhooks-and-apisvc-tls
+-------------------------------
+√ proxy-injector webhook has valid cert
+√ proxy-injector cert is valid for at least 60 days
+√ sp-validator webhook has valid cert
+√ sp-validator cert is valid for at least 60 days
+√ policy-validator webhook has valid cert
+√ policy-validator cert is valid for at least 60 days
+
+linkerd-identity-data-plane
+---------------------------
+√ data plane proxies certificate match CA
+
+linkerd-version
+---------------
+√ can determine the latest version
+√ cli is up-to-date
+
+linkerd-control-plane-proxy
+---------------------------
+√ control plane proxies are healthy
+√ control plane proxies are up-to-date
+√ control plane proxies and cli versions match
+
+linkerd-data-plane
+------------------
+√ data plane namespace exists
+√ data plane proxies are ready
+√ data plane is up-to-date
+√ data plane and cli versions match
+√ data plane pod labels are configured correctly
+√ data plane service labels are configured correctly
+√ data plane service annotations are configured correctly
+√ opaque ports are properly annotated
diff --git a/test/integration/smoke/testdata/inject.report.golden b/test/integration/smoke/testdata/inject.report.golden
new file mode 100644
index 000000000..d1b4570b5
--- /dev/null
+++ b/test/integration/smoke/testdata/inject.report.golden
@@ -0,0 +1,10 @@
+
+deployment "smoke-test-terminus" injected
+service "smoke-test-terminus-svc" skipped
+server "smoke-test-terminus" skipped
+serverauthorization "smoke-test-terminus" skipped
+deployment "smoke-test-gateway" injected
+service "smoke-test-gateway-svc" skipped
+server "smoke-test-proxy-admin" skipped
+serverauthorization "smoke-test-proxy-admin" skipped
+
diff --git a/test/integration/testdata/smoke_test.yaml b/test/integration/smoke/testdata/smoke_test.yaml
similarity index 100%
rename from test/integration/testdata/smoke_test.yaml
rename to test/integration/smoke/testdata/smoke_test.yaml
diff --git a/test/integration/tracing/tracing_test.go b/test/integration/tracing/tracing_test.go
index 207b45b07..3c0e21ccd 100644
--- a/test/integration/tracing/tracing_test.go
+++ b/test/integration/tracing/tracing_test.go
@@ -50,6 +50,11 @@ func TestTracing(t *testing.T) {
 		t.Skip("Skipped. Jaeger & Open Census images does not support ARM yet")
 	}
 
+	// Require an environment variable to be set for this test to be run.
+	if os.Getenv("RUN_FLAKEY_TEST") == "" {
+		t.Skip("Skipping due to flakiness. See linkerd/linkerd2#7538")
+	}
+
 	// linkerd-jaeger extension
 	tracingNs := "linkerd-jaeger"
 	out, err := TestHelper.LinkerdRun("jaeger", "install")