Repository: linkerd/linkerd2
Pull request #7968 (2022-02-25 23:26:39)

Add read limits in various places
There are various places where we call `ioutil.ReadAll` to read the body of a request or response.  This read is unbounded and can lead to arbitrary memory consumption.  We introduce a `ReadAllLimit` alternative which errors out if the read exceeds a certain size.  We then add limits to most of the reads.

Note: It's not clear to me what body sizes we might reasonably expect for these so the limits I chose were arbitrary and based on a gut feeling, attempting to air on the permissive side.  I would appreciate any feedback on what you think reasonable limits might be for each of these cases.

I left in one unbounded read in the `linkerd profile` CLI command where we read the contents of the input file.

Signed-off-by: Alex Leong <alex@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/.github/workflows/codecov.yml b/.github/workflows/codecov.yml
index 1f3b0b3b4..2b3ae421c 100644
--- a/.github/workflows/codecov.yml
+++ b/.github/workflows/codecov.yml
@@ -16,7 +16,7 @@ jobs:
     container:
       image: golang:1.17.3
     steps:
-      - uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
+      - uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
       - run: go get gotest.tools/gotestsum@v0.4.2
       - run: gotestsum -- -cover -coverprofile=coverage.out -v -mod=readonly ./...
       - uses: codecov/codecov-action@f32b3a3741e1053eb607407145bc9619351dc93b
@@ -31,7 +31,7 @@ jobs:
     container:
       image: node:14-stretch
     steps:
-      - uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
+      - uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
       - name: Yarn setup
         run: bin/scurl -o- https://yarnpkg.com/install.sh | bash -s -- --version 1.21.1 --network-concurrency 1
       - name: Unit tests
@@ -53,7 +53,7 @@ jobs:
       image: docker://rust:1.59.0
       options: --security-opt seccomp=unconfined
     steps:
-      - uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
+      - uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
       - run: mkdir -p target && cd target && bin/scurl -v https://github.com/xd009642/tarpaulin/releases/download/0.18.0/cargo-tarpaulin-0.18.0-travis.tar.gz | tar zxvf - && chmod 755 cargo-tarpaulin
       - run: target/cargo-tarpaulin tarpaulin --workspace --out Xml
       - uses: codecov/codecov-action@f32b3a3741e1053eb607407145bc9619351dc93b
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index 01fe940b4..de683899e 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -35,7 +35,7 @@ jobs:
 
     steps:
     - name: Checkout
-      uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
+      uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
 
     - name: Initialize
       # Unpinned action version so that we automatically get analyzer updates.
diff --git a/.github/workflows/devcontainer.yml b/.github/workflows/devcontainer.yml
index 3c51fbd4a..f89a0e02c 100644
--- a/.github/workflows/devcontainer.yml
+++ b/.github/workflows/devcontainer.yml
@@ -16,13 +16,13 @@ jobs:
   build:
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
+      - uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
       - run: docker build . -f .devcontainer/Dockerfile
 
   rust-version:
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
+      - uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
       - run: |
           versions=$(sed -nE 's/^ARG RUST_TOOLCHAIN=([^ ]+)/\1/p' .devcontainer/Dockerfile)
           ex=0
@@ -35,7 +35,7 @@ jobs:
   devcontainer-image:
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
+      - uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
       - run: |
           export DEBIAN_FRONTEND=noninteractive
           sudo apt-get update
diff --git a/.github/workflows/policy_controller.yml b/.github/workflows/policy_controller.yml
index edc61deba..2be0a44ed 100644
--- a/.github/workflows/policy_controller.yml
+++ b/.github/workflows/policy_controller.yml
@@ -27,7 +27,7 @@ jobs:
     container:
       image: docker://rust:1.59.0
     steps:
-      - uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
+      - uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
       - run: rustup component add rustfmt
       - run: cargo fmt --all -- --check
 
@@ -42,7 +42,7 @@ jobs:
     # Prevent sudden announcement of a new advisory from failing Ci.
     continue-on-error: ${{ matrix.checks == 'advisories' }}
     steps:
-    - uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
+    - uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
     - uses: EmbarkStudios/cargo-deny-action@8acbae97b5d01b0481ae14cee8fcd8f5aa9e374d
       with:
         command: check ${{ matrix.checks }}
@@ -54,7 +54,7 @@ jobs:
       image: docker://rust:1.59.0
     steps:
       - run: rustup component add clippy
-      - uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
+      - uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
       - run: |
           bin/scurl -o /usr/local/bin/cargo-action-fmt "https://github.com/olix0r/cargo-action-fmt/releases/download/release%2F${CARGO_ACTION_FMT_VERSION}/cargo-action-fmt-x86_64-unknown-linux-gnu"
           chmod 755 /usr/local/bin/cargo-action-fmt
@@ -67,7 +67,7 @@ jobs:
     container:
       image: docker://rust:1.59.0
     steps:
-      - uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
+      - uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
       - run: |
           bin/scurl -o /usr/local/bin/cargo-action-fmt "https://github.com/olix0r/cargo-action-fmt/releases/download/release%2F${CARGO_ACTION_FMT_VERSION}/cargo-action-fmt-x86_64-unknown-linux-gnu"
           chmod 755 /usr/local/bin/cargo-action-fmt
@@ -88,7 +88,7 @@ jobs:
     container:
       image: docker://rust:1.59.0
     steps:
-      - uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
+      - uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
       - run: cargo fetch
       - run: cargo test --workspace --frozen --no-run
       - run: cargo test --workspace --frozen
@@ -98,7 +98,7 @@ jobs:
     runs-on: ubuntu-latest
     timeout-minutes: 2
     steps:
-      - uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
+      - uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
       - run: |
           ex=0
 
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 3b10ea11a..19401ffef 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -33,7 +33,7 @@ jobs:
     timeout-minutes: 30
     steps:
     - name: Checkout code
-      uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
+      uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
     - uses: ./.github/actions/docker-build
       with:
         docker-registry: ${{ env.DOCKER_REGISTRY }}
@@ -77,7 +77,7 @@ jobs:
     - name: Set up Docker Buildx
       uses: docker/setup-buildx-action@94ab11c41e45d028884a99163086648e898eed25
     - name: Checkout code
-      uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
+      uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
     - run: |
         . bin/_tag.sh
         echo "TAG=$(CI_FORCE_CLEAN=1 bin/root-tag)" >> $GITHUB_ENV
@@ -109,7 +109,7 @@ jobs:
     name: Policy controller manifest
     steps:
     - name: Checkout code
-      uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
+      uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
     - run: |
         . bin/_tag.sh
         echo "TAG=$(CI_FORCE_CLEAN=1 bin/root-tag)" >> $GITHUB_ENV
@@ -139,12 +139,12 @@ jobs:
     needs: [docker_build]
     steps:
     - name: Checkout code
-      uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
-    - uses: actions/setup-go@bfdd3570ce990073878bf10f6b2d79082de49492
+      uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
+    - uses: actions/setup-go@f6164bd8c8acb4a71fb2791a8b6c4024ff038dab
       with:
         go-version: '1.17'
     - name: Download image archives
-      uses: actions/download-artifact@f023be2c48cc18debc3bacd34cb396e0295e2869
+      uses: actions/download-artifact@fb598a63ae348fa914e94cd0ff38f362e927b741
       with:
         name: image-archives
         path: image-archives
@@ -172,8 +172,8 @@ jobs:
     runs-on: ubuntu-20.04
     steps:
     - name: Checkout code
-      uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
-    - uses: actions/setup-go@bfdd3570ce990073878bf10f6b2d79082de49492
+      uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
+    - uses: actions/setup-go@f6164bd8c8acb4a71fb2791a8b6c4024ff038dab
       with:
         go-version: '1.17'
     - name: Set environment variables from scripts
@@ -198,8 +198,8 @@ jobs:
     needs: [docker_build, policy_controller_manifest]
     steps:
     - name: Checkout code
-      uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
-    - uses: actions/setup-go@bfdd3570ce990073878bf10f6b2d79082de49492
+      uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
+    - uses: actions/setup-go@f6164bd8c8acb4a71fb2791a8b6c4024ff038dab
       with:
         go-version: '1.17'
     - name: Install linkerd CLI
@@ -242,7 +242,7 @@ jobs:
     steps:
     - name: Checkout code
       if: startsWith(github.ref, 'refs/tags/stable')
-      uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
+      uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
     - name: Chocolatey - update nuspec
       if: startsWith(github.ref, 'refs/tags/stable')
       run: |
@@ -270,7 +270,7 @@ jobs:
       contents: write
     steps:
     - name: Checkout code
-      uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
+      uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
     - name: Set environment variables from scripts
       run: |
         . bin/_tag.sh
@@ -279,7 +279,7 @@ jobs:
         extract_release_notes NOTES.md
     - name: Download choco package
       if: startsWith(github.ref, 'refs/tags/stable')
-      uses: actions/download-artifact@f023be2c48cc18debc3bacd34cb396e0295e2869
+      uses: actions/download-artifact@fb598a63ae348fa914e94cd0ff38f362e927b741
       with:
         name: choco
         path: choco
@@ -327,7 +327,7 @@ jobs:
     needs: [website_publish]
     steps:
     - name: Checkout code
-      uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
+      uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
     - name: Set environment variables from scripts
       run: |
         . bin/_tag.sh
@@ -357,7 +357,7 @@ jobs:
     needs: [gh_release]
     steps:
     - name: Checkout code
-      uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
+      uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
     - name: Log into GCP
       uses: 'google-github-actions/auth@8d125895b958610ec414ca4dae010257eaa814d3'
       with:
diff --git a/.github/workflows/rust_audit.yml b/.github/workflows/rust_audit.yml
index dc9cf8e46..ce7b205bc 100644
--- a/.github/workflows/rust_audit.yml
+++ b/.github/workflows/rust_audit.yml
@@ -41,7 +41,7 @@ jobs:
 
     name: RUSTSEC audit (${{ matrix.repo }})
     steps:
-      - uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
+      - uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
         with:
           repository: linkerd/${{ matrix.repo }}
       - uses: olix0r/rustsecbot@560c817c7a019e1f0c2318721b55156409be7f8c
diff --git a/.github/workflows/static_checks.yml b/.github/workflows/static_checks.yml
index 91b4a5ed4..f2ed2597d 100644
--- a/.github/workflows/static_checks.yml
+++ b/.github/workflows/static_checks.yml
@@ -20,7 +20,7 @@ jobs:
       options: --user root
     steps:
     - name: Checkout code
-      uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
+      uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
     - name: Go lint
       run: golangci-lint run --verbose --timeout=10m
   go_format:
@@ -31,7 +31,7 @@ jobs:
       image: golang:1.17.3
     steps:
     - name: Checkout code
-      uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
+      uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
     - name: Format
       run: bin/fmt
   proto_diff:
@@ -44,7 +44,7 @@ jobs:
       - name: Prerequisites
         run: apt-get update && DEBIAN_FRONTEND=noninteractive apt-get -y --no-install-recommends install unzip
       - name: Checkout code
-        uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
+        uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
       - name: Diff proto files
         run: bin/protoc-diff
   shellcheck:
@@ -53,7 +53,7 @@ jobs:
     runs-on: ubuntu-20.04
     steps:
     - name: Checkout code
-      uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
+      uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
     - name: shellcheck
       # For more information on shellcheck failures:
       # https://github.com/koalaman/shellcheck/wiki/Checks
@@ -65,7 +65,7 @@ jobs:
     runs-on: ubuntu-20.04
     steps:
     - name: Checkout code
-      uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
+      uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
     - name: Chocolatey - lint
       uses: devblackops/github-action-psscriptanalyzer@854038567344559afaaa8ccb7a014452b99d86ee
       env:
@@ -80,7 +80,7 @@ jobs:
     runs-on: ubuntu-20.04
     steps:
     - name: Checkout code
-      uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
+      uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
     - name: Markdown lint
       run: bin/markdownlint-all
   chart_docs_diff:
@@ -88,6 +88,6 @@ jobs:
     runs-on: ubuntu-20.04
     steps:
     - name: Checkout code
-      uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
+      uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
     - name: Check docs for diff
       run: bin/helm-docs-diff
diff --git a/.golangci.yml b/.golangci.yml
index 864a5dc4f..8d0f0fee5 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -9,29 +9,18 @@ run:
   deadline: 5m
   skip-dirs:
   - controller/gen
-issues:
-  exclude-use-default: false
-  exclude-rules:
-  # Append should be able to assign to a different var/slice.
-  - linters:
-    - gocritic
-    text: "appendAssign: append result not assigned to the same slice"
-  - linters:
-    - gocritic
-    text: "singleCaseSwitch: should rewrite switch statement to if statement"
-  - linters:
-    - gocritic
-    text: "ifElseChain: rewrite if-else to switch statement"
 
 linters:
   enable:
   - bodyclose
   - deadcode
   - depguard
+  - errcheck
   - errorlint
   - exportloopref
   - goconst
   - gocritic
+  - gosec
   - gosimple
   - govet
   - ineffassign
@@ -48,13 +37,99 @@ linters:
   # - gochecknoglobals
   # - gochecknoinits
   # - gocyclo
-  # - gosec
   # - interfacer
   # - lll
   # - maligned
   # - prealloc
   # - stylecheck
   disable:
-  - errcheck
   - staticcheck
   - structcheck
+
+issues:
+  exclude-use-default: false
+  exclude-rules:
+  # Ignore errors when performing the following file operations. If these are
+  # not handled separately already, they tend to be insignificant.
+  - linters:
+    - errcheck
+    text: Error return value of `.*\.(Copy|Flush|Write|WriteTo)` is not checked
+
+  # Ignore error values when closing file or HTTP response bodies. These
+  # generally happen as cleanup and are part of defer statements.
+  - linters:
+    - errcheck
+    text: Error return value of `.*\.Close` is not checked
+
+  # Ignore error checks for CLI output.
+  - linters:
+    - errcheck
+    text: Error return value of `(plugin|spin|termbox)\.(Clear|Color|Flush|Run)` is not checked
+
+  # The errcheck linter catches these instances and we exclude them with the
+  # rule above; therefore we'll ignore redundant warnings through gosec.
+  - linters:
+    - gosec
+    text: "G104: Errors unhandled."
+
+  # This gives false negatives if a variable name is too close to the pattern
+  # used to determine if a variable is a credential.
+  - linters:
+    - gosec
+    text: "G101: Potential hardcoded credentials"
+
+  # Flag operations are fallible if the flag does not exist. We assume these
+  # exist as they are generally flags we are deprecating or use only for
+  # development.
+  - linters:
+    - errcheck
+    text: Error return value of `(.*)\.(MarkDeprecated|MarkHidden|Set)` is not checked
+
+  # Flag completion is not critical to the CLI and errors are ignored if
+  # registration fails.
+  - linters:
+    - errcheck
+    text: Error return value of `.*\.RegisterFlagCompletionFunc` is not checked
+
+  # Errors that occur when gracefully shutting down control plane components
+  # are insignificant.
+  - linters:
+    - errcheck
+    text: Error return value of `(adminServer|apiServer|server)\.Shutdown` is not checked
+
+  # Append should be able to assign to a different var/slice.
+  - linters:
+    - gocritic
+    text: "appendAssign: append result not assigned to the same slice"
+
+  # This does not always result in more readable code.
+  - linters:
+    - gocritic
+    text: "singleCaseSwitch: should rewrite switch statement to if statement"
+
+  # This does not always result in more readable code.
+  - linters:
+    - gocritic
+    text: "ifElseChain: rewrite if-else to switch statement"
+
+  # Test files do not need to be linted by gosec since all OS operations are
+  # not dynamic.
+  - linters:
+    - gosec
+    path: .*test.*
+    text: "G204: Subprocess launched with .*"
+
+  # In tests we are usually mocking components or have a good idea about the
+  # errors that we expect. For this reason, we ignore unchecked errors in
+  # all test files.
+  - path: .*test.*\.go
+    text: Error return value of `.*` is not checked
+
+  # In tests we'll ignore unchecked filename operations because the values
+  # are not dynamic.
+  - path: (.*test.*\.go|fake)
+    text: "G304: Potential file inclusion via variable"
+
+  # This ignores the errors returned from AddToScheme operations.
+  - path: pkg/k8s/fake.go
+    text: Error return value is not checked
diff --git a/ADOPTERS.md b/ADOPTERS.md
index a47eebf2d..d2fc9e1f8 100644
--- a/ADOPTERS.md
+++ b/ADOPTERS.md
@@ -37,6 +37,7 @@
 - [Kurio](https://kurio.id)
 - [LeadIQ](https://leadiq.com)
 - [Lendico](https://www.lendico.de/)
+- [Livspace](https://www.livspace.com/)
 - [Lumoa](https://www.lumoa.me/)
 - [M1 Finance](https://www.m1finance.com/)
 - [manager.cl](https://www.manager.cl/)
diff --git a/Cargo.lock b/Cargo.lock
index 9b7e9b778..375395aea 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -128,9 +128,9 @@ dependencies = [
 
 [[package]]
 name = "clap"
-version = "3.1.2"
+version = "3.1.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5177fac1ab67102d8989464efd043c6ff44191b1557ec1ddd489b4f7e1447e77"
+checksum = "ced1892c55c910c1219e98d6fc8d71f6bddba7905866ce740066d8bfea859312"
 dependencies = [
  "bitflags",
  "clap_derive",
@@ -142,9 +142,9 @@ dependencies = [
 
 [[package]]
 name = "clap_derive"
-version = "3.1.0"
+version = "3.1.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5fd1122e63869df2cb309f449da1ad54a7c6dfeb7c7e6ccd8e0825d9eb93bb72"
+checksum = "da95d038ede1a964ce99f49cbe27a7fb538d1da595e4b4f70b8c8f338d17bf16"
 dependencies = [
  "heck 0.4.0",
  "proc-macro-error",
@@ -238,11 +238,10 @@ dependencies = [
 
 [[package]]
 name = "drain"
-version = "0.1.0"
+version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9701380e51dff309f25d5b3d6639aa757d940821f7bc966faa58b65b5b7e4a18"
+checksum = "2f1a0abf3fcefad9b4dd0e414207a7408e12b68414a01e6bb19b897d5bd7632d"
 dependencies = [
- "futures",
  "tokio",
 ]
 
@@ -569,6 +568,17 @@ version = "1.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
 
+[[package]]
+name = "idna"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8"
+dependencies = [
+ "matches",
+ "unicode-bidi",
+ "unicode-normalization",
+]
+
 [[package]]
 name = "indexmap"
 version = "1.8.0"
@@ -670,9 +680,12 @@ dependencies = [
  "base64",
  "bytes",
  "chrono",
+ "http",
+ "percent-encoding",
  "serde",
  "serde-value",
  "serde_json",
+ "url",
 ]
 
 [[package]]
@@ -713,7 +726,7 @@ dependencies = [
  "pem",
  "pin-project",
  "rustls",
- "rustls-pemfile",
+ "rustls-pemfile 0.2.1",
  "secrecy",
  "serde",
  "serde_json",
@@ -782,6 +795,31 @@ dependencies = [
  "tracing",
 ]
 
+[[package]]
+name = "kubert"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94e81a95b19fad6a076aabe1d5f9b711c4121b2888d418ca1fcca4f8841531ba"
+dependencies = [
+ "clap",
+ "drain",
+ "futures-core",
+ "futures-util",
+ "hyper",
+ "kube-client",
+ "kube-core",
+ "kube-runtime",
+ "pin-project-lite",
+ "rustls-pemfile 0.3.0",
+ "serde",
+ "thiserror",
+ "tokio",
+ "tokio-rustls",
+ "tower-service",
+ "tracing",
+ "tracing-subscriber",
+]
+
 [[package]]
 name = "lazy_static"
 version = "1.4.0"
@@ -810,18 +848,18 @@ dependencies = [
  "futures",
  "hyper",
  "jemallocator",
+ "k8s-openapi",
  "kube",
+ "kubert",
  "linkerd-policy-controller-core",
  "linkerd-policy-controller-grpc",
  "linkerd-policy-controller-k8s-api",
  "linkerd-policy-controller-k8s-index",
- "rustls",
- "rustls-pemfile",
+ "parking_lot 0.12.0",
  "serde_json",
+ "thiserror",
  "tokio",
- "tokio-rustls",
  "tracing",
- "tracing-subscriber",
 ]
 
 [[package]]
@@ -857,6 +895,7 @@ dependencies = [
  "futures",
  "k8s-openapi",
  "kube",
+ "parking_lot 0.12.0",
  "schemars",
  "serde",
  "serde_json",
@@ -874,6 +913,7 @@ dependencies = [
  "async-stream",
  "async-trait",
  "futures",
+ "kubert",
  "linkerd-policy-controller-core",
  "linkerd-policy-controller-k8s-api",
  "parking_lot 0.12.0",
@@ -1319,9 +1359,9 @@ dependencies = [
 
 [[package]]
 name = "redox_syscall"
-version = "0.2.10"
+version = "0.2.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff"
+checksum = "8380fe0152551244f0747b1bf41737e0f8a74f97a14ccefd1148187271634f3c"
 dependencies = [
  "bitflags",
 ]
@@ -1405,7 +1445,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "5ca9ebdfa27d3fc180e42879037b5338ab1c040c06affd00d8338598e7800943"
 dependencies = [
  "openssl-probe",
- "rustls-pemfile",
+ "rustls-pemfile 0.2.1",
  "schannel",
  "security-framework",
 ]
@@ -1419,6 +1459,15 @@ dependencies = [
  "base64",
 ]
 
+[[package]]
+name = "rustls-pemfile"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ee86d63972a7c661d1536fefe8c3c8407321c3df668891286de28abcd087360"
+dependencies = [
+ "base64",
+]
+
 [[package]]
 name = "ryu"
 version = "1.0.9"
@@ -1652,9 +1701,9 @@ dependencies = [
 
 [[package]]
 name = "textwrap"
-version = "0.14.2"
+version = "0.15.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0066c8d12af8b5acd21e00547c3797fde4e8677254a7ee429176ccebbe93dd80"
+checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb"
 
 [[package]]
 name = "thiserror"
@@ -1685,6 +1734,21 @@ dependencies = [
  "once_cell",
 ]
 
+[[package]]
+name = "tinyvec"
+version = "1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c1c1d5a42b6245520c249549ec267180beaffcc0615401ac8e31853d4b6d8d2"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
+
 [[package]]
 name = "tokio"
 version = "1.17.0"
@@ -1982,6 +2046,21 @@ version = "0.2.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642"
 
+[[package]]
+name = "unicode-bidi"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f"
+
+[[package]]
+name = "unicode-normalization"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9"
+dependencies = [
+ "tinyvec",
+]
+
 [[package]]
 name = "unicode-segmentation"
 version = "1.9.0"
@@ -2000,6 +2079,18 @@ version = "0.7.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
 
+[[package]]
+name = "url"
+version = "2.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "matches",
+ "percent-encoding",
+]
+
 [[package]]
 name = "valuable"
 version = "0.1.0"
@@ -2195,6 +2286,6 @@ dependencies = [
 
 [[package]]
 name = "zeroize"
-version = "1.5.2"
+version = "1.5.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7c88870063c39ee00ec285a2f8d6a966e5b6fb2becc4e8dac77ed0d370ed6006"
+checksum = "50344758e2f40e3a1fcfc8f6f91aa57b5f8ebd8d27919fe6451f15aaaf9ee608"
diff --git a/Cargo.toml b/Cargo.toml
index 8273bd281..aa3136c62 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -5,7 +5,7 @@ members = [
     "policy-controller/core",
     "policy-controller/grpc",
     "policy-controller/k8s/api",
-    "policy-controller/k8s/index"
+    "policy-controller/k8s/index",
 ]
 
 [profile.release]
diff --git a/Dockerfile-proxy b/Dockerfile-proxy
index a962eb717..90f7b8ef8 100644
--- a/Dockerfile-proxy
+++ b/Dockerfile-proxy
@@ -24,6 +24,7 @@ RUN bin/scurl -o linkerd-await https://github.com/linkerd/linkerd-await/releases
 ## compile proxy-identity agent
 FROM go-deps as golang
 WORKDIR /linkerd-build
+COPY pkg/util pkg/util
 COPY pkg/flags pkg/flags
 COPY pkg/tls pkg/tls
 COPY pkg/version pkg/version
diff --git a/charts/linkerd-control-plane/templates/destination.yaml b/charts/linkerd-control-plane/templates/destination.yaml
index 4472e342d..9ac467fb1 100644
--- a/charts/linkerd-control-plane/templates/destination.yaml
+++ b/charts/linkerd-control-plane/templates/destination.yaml
@@ -254,7 +254,9 @@ spec:
         - --admin-addr=0.0.0.0:9990
         - --control-plane-namespace={{.Release.Namespace}}
         - --grpc-addr=0.0.0.0:8090
-        - --admission-addr=0.0.0.0:9443
+        - --server-addr=0.0.0.0:9443
+        - --server-tls-key=/var/run/linkerd/tls/tls.key
+        - --server-tls-certs=/var/run/linkerd/tls/tls.crt
         - --cluster-networks={{.Values.clusterNetworks}}
         - --identity-domain={{.Values.identityTrustDomain | default .Values.clusterDomain}}
         - --default-policy={{.Values.policyController.defaultAllowPolicy}}
@@ -264,9 +266,8 @@ spec:
         imagePullPolicy: {{.Values.policyController.image.pullPolicy | default .Values.imagePullPolicy}}
         livenessProbe:
           httpGet:
-            path: /ready
+            path: /live
             port: admin
-          initialDelaySeconds: 10
         name: policy
         ports:
         - containerPort: 8090
@@ -284,6 +285,7 @@ spec:
           httpGet:
             path: /ready
             port: admin
+          initialDelaySeconds: 10
         {{- $res := .Values.policyController.resources | default .Values.destinationResources }}
         {{- if $res }}
         {{- include "partials.resources" $res | nindent 8 }}
diff --git a/cli/cmd/inject_util.go b/cli/cmd/inject_util.go
index 2c4508a3f..ecca891cc 100644
--- a/cli/cmd/inject_util.go
+++ b/cli/cmd/inject_util.go
@@ -187,13 +187,14 @@ func toURL(path string) (*url.URL, bool) {
 // walk walks the file tree rooted at path. path may be a file or a directory.
 // Creates a reader for each file found.
 func walk(path string) ([]io.Reader, error) {
-	stat, err := os.Stat(path)
+	p := filepath.Clean(path)
+	stat, err := os.Stat(p)
 	if err != nil {
 		return nil, err
 	}
 
 	if !stat.IsDir() {
-		file, err := os.Open(path)
+		file, err := os.Open(p)
 		if err != nil {
 			return nil, err
 		}
@@ -202,7 +203,7 @@ func walk(path string) ([]io.Reader, error) {
 	}
 
 	var in []io.Reader
-	werr := filepath.Walk(path, func(path string, info os.FileInfo, err error) error {
+	werr := filepath.Walk(p, func(path string, info os.FileInfo, err error) error {
 		if err != nil {
 			return err
 		}
@@ -211,7 +212,7 @@ func walk(path string) ([]io.Reader, error) {
 			return nil
 		}
 
-		file, err := os.Open(path)
+		file, err := os.Open(filepath.Clean(path))
 		if err != nil {
 			return err
 		}
diff --git a/cli/cmd/metrics_diagnostics_util.go b/cli/cmd/metrics_diagnostics_util.go
index e4c7aca7d..bbb63362d 100644
--- a/cli/cmd/metrics_diagnostics_util.go
+++ b/cli/cmd/metrics_diagnostics_util.go
@@ -37,6 +37,9 @@ func (s byResult) Less(i, j int) bool {
 
 // getResponse makes a http Get request to the passed url and returns the response/error
 func getResponse(url string) ([]byte, error) {
+	// url has been constructed by k8s.newPortForward and is not passed in by
+	// the user.
+	//nolint:gosec
 	resp, err := http.Get(url)
 	if err != nil {
 		return nil, err
@@ -177,6 +180,8 @@ func obfuscateMetrics(metrics []byte) ([]byte, error) {
 				}
 			}
 		}
+		// We'll assume MetricFamilyToText errors are insignificant
+		//nolint:errcheck
 		expfmt.MetricFamilyToText(&writer, v)
 	}
 
diff --git a/cli/cmd/options.go b/cli/cmd/options.go
index 9a7f90f13..23dd332bc 100644
--- a/cli/cmd/options.go
+++ b/cli/cmd/options.go
@@ -4,6 +4,7 @@ import (
 	"context"
 	"errors"
 	"os"
+	"path/filepath"
 
 	"fmt"
 	"io/ioutil"
@@ -144,7 +145,7 @@ func makeInstallUpgradeFlags(defaults *l5dcharts.Values) ([]flag.Flag, *pflag.Fl
 			"A path to a PEM-encoded file containing Linkerd Identity trust anchors (generated by default)",
 			func(values *l5dcharts.Values, value string) error {
 				if value != "" {
-					data, err := ioutil.ReadFile(value)
+					data, err := ioutil.ReadFile(filepath.Clean(value))
 					if err != nil {
 						return err
 					}
@@ -176,7 +177,7 @@ func makeInstallUpgradeFlags(defaults *l5dcharts.Values) ([]flag.Flag, *pflag.Fl
 }
 
 func loadCrtPEM(path string) (string, error) {
-	data, err := ioutil.ReadFile(path)
+	data, err := ioutil.ReadFile(filepath.Clean(path))
 	if err != nil {
 		return "", err
 	}
@@ -189,7 +190,7 @@ func loadCrtPEM(path string) (string, error) {
 }
 
 func loadKeyPEM(path string) (string, error) {
-	data, err := ioutil.ReadFile(path)
+	data, err := ioutil.ReadFile(filepath.Clean(path))
 	if err != nil {
 		return "", err
 	}
diff --git a/cli/main.go b/cli/main.go
index 372cc26b2..9c1730b04 100644
--- a/cli/main.go
+++ b/cli/main.go
@@ -19,6 +19,9 @@ func main() {
 		}
 		path, err := exec.LookPath(fmt.Sprintf("linkerd-%s", args[0]))
 		if err == nil {
+			// We're working with a Linkerd plugin at this point which means
+			// it's up to the plugin to cleanse the arguments if needed.
+			//nolint:gosec
 			plugin := exec.Command(path, args[1:]...)
 			plugin.Stdin = os.Stdin
 			plugin.Stdout = os.Stdout
diff --git a/controller/api/destination/endpoint_profile_translator.go b/controller/api/destination/endpoint_profile_translator.go
index cbebcf2cb..1faec212d 100644
--- a/controller/api/destination/endpoint_profile_translator.go
+++ b/controller/api/destination/endpoint_profile_translator.go
@@ -51,7 +51,9 @@ func (ept *endpointProfileTranslator) UpdateProtocol(opaqueProtocol bool) {
 	}
 	profile := ept.createDefaultProfile(opaqueProtocol)
 	ept.log.Debugf("sending protocol update: %+v", profile)
-	ept.stream.Send(profile)
+	if err := ept.stream.Send(profile); err != nil {
+		ept.log.Errorf("failed to send protocol update: %s", err)
+	}
 }
 
 func (ept *endpointProfileTranslator) createDefaultProfile(opaqueProtocol bool) *pb.DestinationProfile {
diff --git a/controller/api/destination/profile_translator.go b/controller/api/destination/profile_translator.go
index f9bd1ee82..f2c7fcccd 100644
--- a/controller/api/destination/profile_translator.go
+++ b/controller/api/destination/profile_translator.go
@@ -35,7 +35,9 @@ func newProfileTranslator(stream pb.Destination_GetProfileServer, log *logging.E
 
 func (pt *profileTranslator) Update(profile *sp.ServiceProfile) {
 	if profile == nil {
-		pt.stream.Send(pt.defaultServiceProfile())
+		if err := pt.stream.Send(pt.defaultServiceProfile()); err != nil {
+			pt.log.Errorf("failed to send default service profile: %s", err)
+		}
 		return
 	}
 	destinationProfile, err := pt.createDestinationProfile(profile)
@@ -44,7 +46,9 @@ func (pt *profileTranslator) Update(profile *sp.ServiceProfile) {
 		return
 	}
 	pt.log.Debugf("Sending profile update: %+v", destinationProfile)
-	pt.stream.Send(destinationProfile)
+	if err := pt.stream.Send(destinationProfile); err != nil {
+		pt.log.Errorf("failed to send profile update: %s", err)
+	}
 }
 
 func (pt *profileTranslator) defaultServiceProfile() *pb.DestinationProfile {
diff --git a/controller/api/destination/watcher/profile_watcher.go b/controller/api/destination/watcher/profile_watcher.go
index 66bff1e6c..859f67a81 100644
--- a/controller/api/destination/watcher/profile_watcher.go
+++ b/controller/api/destination/watcher/profile_watcher.go
@@ -1,7 +1,6 @@
 package watcher
 
 import (
-	"fmt"
 	"sync"
 
 	sp "github.com/linkerd/linkerd2/controller/gen/apis/serviceprofile/v1alpha2"
@@ -80,15 +79,14 @@ func (pw *ProfileWatcher) Subscribe(id ProfileID, listener ProfileUpdateListener
 }
 
 // Unsubscribe removes a listener from the subscribers list for this authority.
-func (pw *ProfileWatcher) Unsubscribe(id ProfileID, listener ProfileUpdateListener) error {
+func (pw *ProfileWatcher) Unsubscribe(id ProfileID, listener ProfileUpdateListener) {
 	pw.log.Debugf("Stopping watch on profile %s", id)
 
 	publisher, ok := pw.getProfilePublisher(id)
 	if !ok {
-		return fmt.Errorf("cannot unsubscribe from unknown service [%s] ", id)
+		pw.log.Errorf("cannot unsubscribe from unknown service [%s]", id)
 	}
 	publisher.unsubscribe(listener)
-	return nil
 }
 
 func (pw *ProfileWatcher) addProfile(obj interface{}) {
diff --git a/controller/cmd/destination/main.go b/controller/cmd/destination/main.go
index 4db256012..10546f435 100644
--- a/controller/cmd/destination/main.go
+++ b/controller/cmd/destination/main.go
@@ -123,14 +123,18 @@ func Main(args []string) {
 
 	go func() {
 		log.Infof("starting gRPC server on %s", *addr)
-		server.Serve(lis)
+		if err := server.Serve(lis); err != nil {
+			log.Errorf("failed to start destination gRPC server: %s", err)
+		}
 	}()
 
 	adminServer := admin.NewServer(*metricsAddr)
 
 	go func() {
 		log.Infof("starting admin server on %s", *metricsAddr)
-		adminServer.ListenAndServe()
+		if err := adminServer.ListenAndServe(); err != nil {
+			log.Errorf("failed to start destination admin server: %s", err)
+		}
 	}()
 
 	<-stop
diff --git a/controller/cmd/identity/main.go b/controller/cmd/identity/main.go
index af6b17f78..80b69854f 100644
--- a/controller/cmd/identity/main.go
+++ b/controller/cmd/identity/main.go
@@ -176,7 +176,9 @@ func Main(args []string) {
 
 	go func() {
 		log.Infof("starting admin server on %s", *adminAddr)
-		adminServer.ListenAndServe()
+		if err := adminServer.ListenAndServe(); err != nil {
+			log.Errorf("failed to start identity admin server: %s", err)
+		}
 	}()
 
 	lis, err := net.Listen("tcp", *addr)
@@ -194,7 +196,9 @@ func Main(args []string) {
 	identity.Register(srv, svc)
 	go func() {
 		log.Infof("starting gRPC server on %s", *addr)
-		srv.Serve(lis)
+		if err := srv.Serve(lis); err != nil {
+			log.Errorf("failed to start identity gRPC server: %s", err)
+		}
 	}()
 	<-stop
 	log.Infof("shutting down gRPC server on %s", *addr)
diff --git a/controller/heartbeat/heartbeat.go b/controller/heartbeat/heartbeat.go
index 0eed0db1c..8779d8066 100644
--- a/controller/heartbeat/heartbeat.go
+++ b/controller/heartbeat/heartbeat.go
@@ -3,7 +3,6 @@ package heartbeat
 import (
 	"context"
 	"fmt"
-	"io/ioutil"
 	"math"
 	"net/http"
 	"net/url"
@@ -13,6 +12,7 @@ import (
 	pkgK8s "github.com/linkerd/linkerd2/controller/k8s"
 	"github.com/linkerd/linkerd2/pkg/config"
 	"github.com/linkerd/linkerd2/pkg/k8s"
+	"github.com/linkerd/linkerd2/pkg/util"
 	"github.com/linkerd/linkerd2/pkg/version"
 	promv1 "github.com/prometheus/client_golang/api/prometheus/v1"
 	"github.com/prometheus/common/model"
@@ -235,7 +235,7 @@ func send(client *http.Client, baseURL string, v url.Values) error {
 		return fmt.Errorf("check URL [%s] request failed with: %w", req.URL.String(), err)
 	}
 	defer resp.Body.Close()
-	body, err := ioutil.ReadAll(resp.Body)
+	body, err := util.ReadAllLimit(resp.Body, util.MB)
 	if err != nil {
 		return fmt.Errorf("failed to read response body: %w", err)
 	}
diff --git a/controller/k8s/api.go b/controller/k8s/api.go
index f23a763b7..ebf63d1cb 100644
--- a/controller/k8s/api.go
+++ b/controller/k8s/api.go
@@ -164,7 +164,9 @@ func initAPI(ctx context.Context, k8sClient *k8s.KubernetesAPI, dynamicClient dy
 
 	api := NewAPI(k8sClient, dynamicClient, l5dCrdClient, resources...)
 	for _, gauge := range api.gauges {
-		prometheus.Register(gauge)
+		if err := prometheus.Register(gauge); err != nil {
+			log.Warnf("failed to register Prometheus gauge %s: %s", gauge.Desc().String(), err)
+		}
 	}
 	return api, nil
 }
diff --git a/controller/webhook/launcher.go b/controller/webhook/launcher.go
index 2e307bb5e..37d81ad82 100644
--- a/controller/webhook/launcher.go
+++ b/controller/webhook/launcher.go
@@ -47,7 +47,9 @@ func Launch(
 
 	go func() {
 		log.Infof("starting admin server on %s", metricsAddr)
-		adminServer.ListenAndServe()
+		if err := adminServer.ListenAndServe(); err != nil {
+			log.Errorf("failed to start webhook admin server: %s", err)
+		}
 	}()
 
 	<-stop
diff --git a/controller/webhook/server.go b/controller/webhook/server.go
index 3ab83eaf9..621fcaec1 100644
--- a/controller/webhook/server.go
+++ b/controller/webhook/server.go
@@ -6,13 +6,13 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
-	"io/ioutil"
 	"net/http"
 	"sync/atomic"
 
 	"github.com/linkerd/linkerd2/controller/k8s"
 	pkgk8s "github.com/linkerd/linkerd2/pkg/k8s"
 	pkgTls "github.com/linkerd/linkerd2/pkg/tls"
+	"github.com/linkerd/linkerd2/pkg/util"
 	"github.com/sirupsen/logrus"
 	log "github.com/sirupsen/logrus"
 	admissionv1beta1 "k8s.io/api/admission/v1beta1"
@@ -125,7 +125,7 @@ func (s *Server) serve(res http.ResponseWriter, req *http.Request) {
 		err  error
 	)
 	if req.Body != nil {
-		data, err = ioutil.ReadAll(req.Body)
+		data, err = util.ReadAllLimit(req.Body, 10*util.MB)
 		if err != nil {
 			http.Error(res, err.Error(), http.StatusInternalServerError)
 			return
diff --git a/deny.toml b/deny.toml
index a10a3ed77..30fe1e93e 100644
--- a/deny.toml
+++ b/deny.toml
@@ -51,6 +51,8 @@ skip = [
     { name = "parking_lot", version = "0.11" },
     { name = "parking_lot_core", version = "0.8" },
     { name = "tokio-util", version = "0.6" },
+    # Waiting on kube
+    { name = "rustls-pemfile", version = "0.2" },
 ]
 
 [sources]
diff --git a/jaeger/charts/linkerd-jaeger/README.md b/jaeger/charts/linkerd-jaeger/README.md
index 8e783d668..e14cc7188 100644
--- a/jaeger/charts/linkerd-jaeger/README.md
+++ b/jaeger/charts/linkerd-jaeger/README.md
@@ -88,6 +88,7 @@ Kubernetes: `>=1.20.0-0`
 | collector.resources.memory.request | string | `nil` | Amount of memory that the collector container requests |
 | collector.tolerations | string | `nil` | Tolerations section, See the [K8S documentation](https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/) for more information |
 | enablePSP | bool | `false` | Create Roles and RoleBindings to associate this extension's ServiceAccounts to the control plane PSP resource. This requires that `enabledPSP` is set to true on the control plane install. Note PSP has been deprecated since k8s v1.21 |
+| imagePullSecrets | list | `[]` | For Private docker registries, authentication is needed.  Registry secrets are applied to the respective service accounts |
 | jaeger.args | list | `["--query.base-path=/jaeger"]` | CLI arguments for Jaeger, See [Jaeger AIO Memory CLI reference](https://www.jaegertracing.io/docs/1.24/cli/#jaeger-all-in-one-memory) |
 | jaeger.enabled | bool | `true` | Set to false to exclude all-in-one Jaeger installation |
 | jaeger.image.name | string | `"jaegertracing/all-in-one"` |  |
diff --git a/jaeger/charts/linkerd-jaeger/templates/rbac.yaml b/jaeger/charts/linkerd-jaeger/templates/rbac.yaml
index 1c5d1ee9c..78afd7c6a 100644
--- a/jaeger/charts/linkerd-jaeger/templates/rbac.yaml
+++ b/jaeger/charts/linkerd-jaeger/templates/rbac.yaml
@@ -8,6 +8,7 @@ apiVersion: v1
 metadata:
   name: collector
   {{ include "partials.namespace" . }}
+{{- include "partials.image-pull-secrets" .Values.imagePullSecrets }}
 {{ end -}}
 ---
 ###
@@ -45,6 +46,7 @@ apiVersion: v1
 metadata:
   name: jaeger-injector
   {{ include "partials.namespace" . }}
+{{- include "partials.image-pull-secrets" .Values.imagePullSecrets }}
 ---
 {{- $host := printf "jaeger-injector.%s.svc" .Release.Namespace }}
 {{- $ca := genSelfSignedCert $host (list) (list $host) 365 }}
@@ -113,4 +115,5 @@ apiVersion: v1
 metadata:
   name: jaeger
   {{ include "partials.namespace" . }}
+{{- include "partials.image-pull-secrets" .Values.imagePullSecrets }}
 {{ end -}}
diff --git a/jaeger/charts/linkerd-jaeger/values.yaml b/jaeger/charts/linkerd-jaeger/values.yaml
index f2bf8585d..74bd33b52 100644
--- a/jaeger/charts/linkerd-jaeger/values.yaml
+++ b/jaeger/charts/linkerd-jaeger/values.yaml
@@ -6,6 +6,11 @@ linkerdNamespace: linkerd
 nodeSelector: &default_node_selector
   kubernetes.io/os: linux
 
+# -- For Private docker registries, authentication is needed.
+#  Registry secrets are applied to the respective service accounts
+imagePullSecrets: []
+# - name: my-private-docker-registry-login-secret
+
 # -- Default tolerations section, See the
 # [K8S documentation](https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/)
 # for more information
diff --git a/multicluster/cmd/link.go b/multicluster/cmd/link.go
index 79e284b5e..b838e02a0 100644
--- a/multicluster/cmd/link.go
+++ b/multicluster/cmd/link.go
@@ -61,7 +61,12 @@ func newLinkCommand() *cobra.Command {
 	cmd := &cobra.Command{
 		Use:   "link",
 		Short: "Outputs resources that allow another cluster to mirror services from this one",
-		Args:  cobra.NoArgs,
+		Long: `Outputs resources that allow another cluster to mirror services from this one.
+
+Note that the Link resource applies only in one direction. In order for two
+clusters to mirror each other, a Link resource will have to be generated for
+each cluster and applied to the other.`,
+		Args: cobra.NoArgs,
 		Example: `  # To link the west cluster to east
   linkerd --context=east multicluster link --cluster-name east | kubectl --context=west apply -f -
 
@@ -191,7 +196,8 @@ A full list of configurable values can be found at https://github.com/linkerd/li
 			}
 			if len(gwAddresses) == 0 && opts.gatewayAddresses == "" {
 				return fmt.Errorf("Gateway %s.%s has no ingress addresses", gateway.Name, gateway.Namespace)
-			} else if len(gwAddresses) > 0 {
+			}
+			if len(gwAddresses) > 0 {
 				gatewayAddresses = strings.Join(gwAddresses, ",")
 			} else {
 				gatewayAddresses = opts.gatewayAddresses
diff --git a/multicluster/cmd/service-mirror/main.go b/multicluster/cmd/service-mirror/main.go
index db266e180..bae0dca1c 100644
--- a/multicluster/cmd/service-mirror/main.go
+++ b/multicluster/cmd/service-mirror/main.go
@@ -80,7 +80,9 @@ func Main(args []string) {
 
 	go func() {
 		log.Infof("starting admin server on %s", *metricsAddr)
-		adminServer.ListenAndServe()
+		if err := adminServer.ListenAndServe(); err != nil {
+			log.Errorf("failed to start service mirror admin server: %s", err)
+		}
 	}()
 
 	controllerK8sAPI.Sync(nil)
diff --git a/multicluster/cmd/unlink.go b/multicluster/cmd/unlink.go
index 1a25a45fa..16a346180 100644
--- a/multicluster/cmd/unlink.go
+++ b/multicluster/cmd/unlink.go
@@ -10,6 +10,7 @@ import (
 	"github.com/linkerd/linkerd2/pkg/k8s"
 	"github.com/linkerd/linkerd2/pkg/k8s/resource"
 	mc "github.com/linkerd/linkerd2/pkg/multicluster"
+	log "github.com/sirupsen/logrus"
 	"github.com/spf13/cobra"
 	appsv1 "k8s.io/api/apps/v1"
 	corev1 "k8s.io/api/core/v1"
@@ -87,7 +88,9 @@ func newUnlinkCommand() *cobra.Command {
 			}
 
 			for _, r := range resources {
-				r.RenderResource(stdout)
+				if err := r.RenderResource(stdout); err != nil {
+					log.Errorf("failed to render resource %s: %s", r.Name, err)
+				}
 			}
 
 			return nil
diff --git a/multicluster/service-mirror/cluster_watcher.go b/multicluster/service-mirror/cluster_watcher.go
index 3a2b8d52a..40c038401 100644
--- a/multicluster/service-mirror/cluster_watcher.go
+++ b/multicluster/service-mirror/cluster_watcher.go
@@ -604,8 +604,9 @@ func (rcsw *RemoteClusterServiceWatcher) createGatewayEndpoints(ctx context.Cont
 
 	rcsw.log.Infof("Creating a new endpoints for %s", serviceInfo)
 	if _, err := rcsw.localAPIClient.Client.CoreV1().Endpoints(exportedService.Namespace).Create(ctx, endpointsToCreate, metav1.CreateOptions{}); err != nil {
-		// we clean up after ourselves
-		rcsw.localAPIClient.Client.CoreV1().Services(exportedService.Namespace).Delete(ctx, localServiceName, metav1.DeleteOptions{})
+		if svcErr := rcsw.localAPIClient.Client.CoreV1().Services(exportedService.Namespace).Delete(ctx, localServiceName, metav1.DeleteOptions{}); svcErr != nil {
+			rcsw.log.Errorf("failed to delete Service %s after Endpoints creation failed: %s", localServiceName, svcErr)
+		}
 		// and retry
 		return RetryableError{[]error{err}}
 	}
@@ -1150,9 +1151,9 @@ func (rcsw *RemoteClusterServiceWatcher) createEndpointMirrorService(ctx context
 
 	rcsw.log.Infof("Creating a new endpoints object for endpoint mirror service %s", endpointMirrorInfo)
 	if _, err := rcsw.localAPIClient.Client.CoreV1().Endpoints(endpointMirrorService.Namespace).Create(ctx, endpointMirrorEndpoints, metav1.CreateOptions{}); err != nil {
-		// If we cannot create an Endpoints object for the Endpoint Mirror
-		// service, then delete the Endpoint Mirror service we just created
-		rcsw.localAPIClient.Client.CoreV1().Services(endpointMirrorService.Namespace).Delete(ctx, endpointMirrorName, metav1.DeleteOptions{})
+		if svcErr := rcsw.localAPIClient.Client.CoreV1().Services(endpointMirrorService.Namespace).Delete(ctx, endpointMirrorName, metav1.DeleteOptions{}); svcErr != nil {
+			rcsw.log.Errorf("failed to delete Service %s after Endpoints creation failed: %s", endpointMirrorName, svcErr)
+		}
 		// and retry
 		return createdService, RetryableError{[]error{err}}
 	}
diff --git a/multicluster/service-mirror/cluster_watcher_headless.go b/multicluster/service-mirror/cluster_watcher_headless.go
index 6a9f1941e..9aae86bef 100644
--- a/multicluster/service-mirror/cluster_watcher_headless.go
+++ b/multicluster/service-mirror/cluster_watcher_headless.go
@@ -302,8 +302,12 @@ func (rcsw *RemoteClusterServiceWatcher) createHeadlessMirrorEndpoints(ctx conte
 
 	rcsw.log.Infof("Creating a new headless mirror endpoints object for headless mirror %s/%s", headlessMirrorServiceName, exportedService.Namespace)
 	if _, err := rcsw.localAPIClient.Client.CoreV1().Endpoints(exportedService.Namespace).Create(ctx, headlessMirrorEndpoints, metav1.CreateOptions{}); err != nil {
-		// we clean up after ourselves
-		rcsw.localAPIClient.Client.CoreV1().Services(exportedService.Namespace).Delete(ctx, headlessMirrorServiceName, metav1.DeleteOptions{})
+		// This is a precautionary deletion because creation failed, so we
+		// ignore the error if Delete also fails.
+		//nolint:errcheck
+		if svcErr := rcsw.localAPIClient.Client.CoreV1().Services(exportedService.Namespace).Delete(ctx, headlessMirrorServiceName, metav1.DeleteOptions{}); svcErr != nil {
+			rcsw.log.Errorf("failed to delete Service %s after Endpoints creation failed: %s", headlessMirrorServiceName, svcErr)
+		}
 		// and retry
 		return RetryableError{[]error{err}}
 	}
diff --git a/multicluster/service-mirror/jittered_ticker.go b/multicluster/service-mirror/jittered_ticker.go
index b57c0edd3..880158b30 100644
--- a/multicluster/service-mirror/jittered_ticker.go
+++ b/multicluster/service-mirror/jittered_ticker.go
@@ -66,6 +66,9 @@ tickLoop:
 }
 
 func (t *Ticker) calculateDelay() time.Duration {
+	// The weakness of this random number generator is not significant for
+	// calculating a delay.
+	//nolint:gosec
 	jitter := time.Duration(rand.Int63n(int64(t.MaxJitter)))
 	delay := t.MinDuration + jitter
 	return delay
diff --git a/multicluster/service-mirror/probe_worker.go b/multicluster/service-mirror/probe_worker.go
index b3146517e..cf983029c 100644
--- a/multicluster/service-mirror/probe_worker.go
+++ b/multicluster/service-mirror/probe_worker.go
@@ -98,7 +98,8 @@ func (pw *ProbeWorker) doProbe() {
 		pw.metrics.alive.Set(0)
 		pw.metrics.probes.With(notSuccessLabel).Inc()
 		return
-	} else if resp.StatusCode != 200 {
+	}
+	if resp.StatusCode != 200 {
 		pw.log.Warnf("Gateway returned unexpected status %d. Marking as unhealthy", resp.StatusCode)
 		pw.metrics.alive.Set(0)
 		pw.metrics.probes.With(notSuccessLabel).Inc()
diff --git a/pkg/config/config.go b/pkg/config/config.go
index 2dbd0c256..968d4d93f 100644
--- a/pkg/config/config.go
+++ b/pkg/config/config.go
@@ -4,6 +4,7 @@ import (
 	"context"
 	"fmt"
 	"io/ioutil"
+	"path/filepath"
 
 	"github.com/linkerd/linkerd2/pkg/k8s"
 	corev1 "k8s.io/api/core/v1"
@@ -16,16 +17,16 @@ import (
 )
 
 // Values returns the Value struct from the linkerd-config ConfigMap
-func Values(filepath string) (*l5dcharts.Values, error) {
-	values := &l5dcharts.Values{}
-	configYaml, err := ioutil.ReadFile(filepath)
+func Values(path string) (*l5dcharts.Values, error) {
+	p := filepath.Clean(path)
+	configYaml, err := ioutil.ReadFile(p)
 	if err != nil {
 		return nil, fmt.Errorf("failed to read config file: %w", err)
 	}
-
-	log.Debugf("%s config YAML: %s", filepath, configYaml)
+	log.Debugf("%s config YAML: %s", p, configYaml)
+	values := &l5dcharts.Values{}
 	if err = yaml.Unmarshal(configYaml, values); err != nil {
-		return nil, fmt.Errorf("failed to unmarshal JSON from: %s: %w", filepath, err)
+		return nil, fmt.Errorf("failed to unmarshal JSON from: %s: %w", p, err)
 	}
 	return values, err
 }
diff --git a/pkg/flags/flags.go b/pkg/flags/flags.go
index 71f66d736..8ce0c67f8 100644
--- a/pkg/flags/flags.go
+++ b/pkg/flags/flags.go
@@ -39,6 +39,9 @@ func ConfigureAndParse(cmd *flag.FlagSet, args []string) {
 		"log format, must be one of: plain, json")
 	printVersion := cmd.Bool("version", false, "print version and exit")
 
+	// We'll assume the args being passed in by calling functions have already
+	// been validated and that parsing does not have errors.
+	//nolint:errcheck
 	cmd.Parse(args)
 
 	// set log timestamps
diff --git a/pkg/healthcheck/healthcheck.go b/pkg/healthcheck/healthcheck.go
index 80c633579..ea0a653f9 100644
--- a/pkg/healthcheck/healthcheck.go
+++ b/pkg/healthcheck/healthcheck.go
@@ -153,13 +153,15 @@ const (
 
 	proxyInjectorOldTLSSecretName = "linkerd-proxy-injector-tls"
 	proxyInjectorTLSSecretName    = "linkerd-proxy-injector-k8s-tls"
-	spValidatorOldTLSSecretName   = "linkerd-sp-validator-tls"
-	spValidatorTLSSecretName      = "linkerd-sp-validator-k8s-tls"
-	policyValidatorTLSSecretName  = "linkerd-policy-validator-k8s-tls"
-	certOldKeyName                = "crt.pem"
-	certKeyName                   = "tls.crt"
-	keyOldKeyName                 = "key.pem"
-	keyKeyName                    = "tls.key"
+
+	spValidatorOldTLSSecretName = "linkerd-sp-validator-tls"
+	spValidatorTLSSecretName    = "linkerd-sp-validator-k8s-tls"
+
+	policyValidatorTLSSecretName = "linkerd-policy-validator-k8s-tls"
+	certOldKeyName               = "crt.pem"
+	certKeyName                  = "tls.crt"
+	keyOldKeyName                = "key.pem"
+	keyKeyName                   = "tls.key"
 )
 
 // AllowedClockSkew sets the allowed skew in clock synchronization
diff --git a/pkg/healthcheck/healthcheck_output.go b/pkg/healthcheck/healthcheck_output.go
index e93148378..f22bcbd73 100644
--- a/pkg/healthcheck/healthcheck_output.go
+++ b/pkg/healthcheck/healthcheck_output.go
@@ -143,6 +143,9 @@ func RunExtensionsChecks(wout io.Writer, werr io.Writer, extensions []string, fl
 				spin.Suffix = fmt.Sprintf(" Running %s extension check", extension)
 				spin.Color("bold") // this calls spin.Restart()
 			}
+			// Path is constructed from the switch statements above and will
+			// be a valid Linkerd subcommand.
+			//nolint:gosec
 			plugin := exec.Command(path, args...)
 			var stdout, stderr bytes.Buffer
 			plugin.Stdout = &stdout
diff --git a/pkg/issuercerts/issuercerts.go b/pkg/issuercerts/issuercerts.go
index 409b9315e..37298e989 100644
--- a/pkg/issuercerts/issuercerts.go
+++ b/pkg/issuercerts/issuercerts.go
@@ -6,6 +6,7 @@ import (
 	"crypto/x509"
 	"fmt"
 	"io/ioutil"
+	"path/filepath"
 	"time"
 
 	"k8s.io/client-go/kubernetes"
@@ -84,12 +85,12 @@ func FetchExternalIssuerData(ctx context.Context, api kubernetes.Interface, cont
 
 // LoadIssuerCrtAndKeyFromFiles loads the issuer certificate and key from files
 func LoadIssuerCrtAndKeyFromFiles(keyPEMFile, crtPEMFile string) (string, string, error) {
-	key, err := ioutil.ReadFile(keyPEMFile)
+	key, err := ioutil.ReadFile(filepath.Clean(keyPEMFile))
 	if err != nil {
 		return "", "", err
 	}
 
-	crt, err := ioutil.ReadFile(crtPEMFile)
+	crt, err := ioutil.ReadFile(filepath.Clean(crtPEMFile))
 	if err != nil {
 		return "", "", err
 	}
@@ -104,7 +105,7 @@ func LoadIssuerDataFromFiles(keyPEMFile, crtPEMFile, trustPEMFile string) (*Issu
 		return nil, err
 	}
 
-	anchors, err := ioutil.ReadFile(trustPEMFile)
+	anchors, err := ioutil.ReadFile(filepath.Clean(trustPEMFile))
 	if err != nil {
 		return nil, err
 	}
diff --git a/pkg/profiles/profiles.go b/pkg/profiles/profiles.go
index 99d32d308..994104d6f 100644
--- a/pkg/profiles/profiles.go
+++ b/pkg/profiles/profiles.go
@@ -6,6 +6,7 @@ import (
 	"fmt"
 	"io"
 	"os"
+	"path/filepath"
 	"regexp"
 	"text/template"
 	"time"
@@ -229,7 +230,7 @@ func readFile(fileName string) (io.Reader, error) {
 	if fileName == "-" {
 		return os.Stdin, nil
 	}
-	return os.Open(fileName)
+	return os.Open(filepath.Clean(fileName))
 }
 
 func writeProfile(profile sp.ServiceProfile, w io.Writer) error {
diff --git a/pkg/protohttp/protohttp.go b/pkg/protohttp/protohttp.go
index 76f3e8aac..944d01672 100644
--- a/pkg/protohttp/protohttp.go
+++ b/pkg/protohttp/protohttp.go
@@ -6,11 +6,11 @@ import (
 	"errors"
 	"fmt"
 	"io"
-	"io/ioutil"
 	"net/http"
 
 	"github.com/golang/protobuf/proto"
 	"github.com/linkerd/linkerd2/pkg/k8s"
+	"github.com/linkerd/linkerd2/pkg/util"
 	metricsPb "github.com/linkerd/linkerd2/viz/metrics-api/gen/viz"
 	log "github.com/sirupsen/logrus"
 	"google.golang.org/grpc/status"
@@ -45,7 +45,7 @@ func (e HTTPError) Error() string {
 
 // HTTPRequestToProto converts an HTTP Request to a protobuf request.
 func HTTPRequestToProto(req *http.Request, protoRequestOut proto.Message) error {
-	bytes, err := ioutil.ReadAll(req.Body)
+	bytes, err := util.ReadAllLimit(req.Body, 100*util.MB)
 	if err != nil {
 		return HTTPError{
 			Code:         http.StatusBadRequest,
@@ -168,7 +168,7 @@ func CheckIfResponseHasError(rsp *http.Response) error {
 	// check for JSON-encoded error
 	if rsp.StatusCode != http.StatusOK {
 		if rsp.Body != nil {
-			bytes, err := ioutil.ReadAll(rsp.Body)
+			bytes, err := util.ReadAllLimit(rsp.Body, 100*util.MB)
 			if err == nil && len(bytes) > 0 {
 				body := string(bytes)
 				obj, err := k8s.ToRuntimeObject(body)
diff --git a/pkg/tls/cred.go b/pkg/tls/cred.go
index 0916dc3ac..51372a62a 100644
--- a/pkg/tls/cred.go
+++ b/pkg/tls/cred.go
@@ -10,6 +10,7 @@ import (
 	"errors"
 	"fmt"
 	"io/ioutil"
+	"path/filepath"
 	"time"
 )
 
@@ -194,12 +195,12 @@ func ValidateAndCreateCreds(crt, key string) (*Cred, error) {
 
 // ReadPEMCreds reads PEM-encoded credentials from the named files.
 func ReadPEMCreds(keyPath, crtPath string) (*Cred, error) {
-	keyb, err := ioutil.ReadFile(keyPath)
+	keyb, err := ioutil.ReadFile(filepath.Clean(keyPath))
 	if err != nil {
 		return nil, err
 	}
 
-	crtb, err := ioutil.ReadFile(crtPath)
+	crtb, err := ioutil.ReadFile(filepath.Clean(crtPath))
 	if err != nil {
 		return nil, err
 	}
diff --git a/pkg/util/http.go b/pkg/util/http.go
index 260579342..1a4bc6862 100644
--- a/pkg/util/http.go
+++ b/pkg/util/http.go
@@ -1,11 +1,20 @@
 package util
 
 import (
+	"fmt"
+	"io"
+	"io/ioutil"
 	"strings"
 
 	httpPb "github.com/linkerd/linkerd2-proxy-api/go/http_types"
 )
 
+// KB = Kilobyte
+const KB = 1024
+
+// MB = Megabyte
+const MB = KB * 1024
+
 // ParseScheme converts a scheme string to protobuf
 // TODO: validate scheme
 func ParseScheme(scheme string) *httpPb.Scheme {
@@ -41,3 +50,17 @@ func ParseMethod(method string) *httpPb.HttpMethod {
 		},
 	}
 }
+
+// ReadAllLimit reads from r until EOF or until limit bytes are read. If EOF is
+// reached, the full bytes are returned. If the limit is reached, an error is
+// returned.
+func ReadAllLimit(r io.Reader, limit int) ([]byte, error) {
+	bytes, err := ioutil.ReadAll(io.LimitReader(r, int64(limit)))
+	if err != nil {
+		return nil, err
+	}
+	if len(bytes) == limit {
+		return nil, fmt.Errorf("limit reached while reading: %d", limit)
+	}
+	return bytes, nil
+}
diff --git a/pkg/version/channels.go b/pkg/version/channels.go
index 4b0ba6589..126f5a289 100644
--- a/pkg/version/channels.go
+++ b/pkg/version/channels.go
@@ -5,8 +5,9 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
-	"io/ioutil"
 	"net/http"
+
+	"github.com/linkerd/linkerd2/pkg/util"
 )
 
 // Channels provides an interface to interact with a set of release channels.
@@ -80,7 +81,7 @@ func getLatestVersions(ctx context.Context, client *http.Client, url string) (Ch
 		return Channels{}, fmt.Errorf("unexpected versioncheck response: %s", rsp.Status)
 	}
 
-	bytes, err := ioutil.ReadAll(rsp.Body)
+	bytes, err := util.ReadAllLimit(rsp.Body, util.MB)
 	if err != nil {
 		return Channels{}, err
 	}
diff --git a/policy-controller/Cargo.toml b/policy-controller/Cargo.toml
index 3895df8a3..a23687ff4 100644
--- a/policy-controller/Cargo.toml
+++ b/policy-controller/Cargo.toml
@@ -15,33 +15,43 @@ anyhow = "1"
 clap = { version = "3", default-features = false, features = ["derive", "env", "std"] }
 drain = "0.1"
 futures = { version = "0.3", default-features = false }
+k8s-openapi = { version = "0.14", features = ["v1_20"] }
 hyper = { version = "0.14", features = ["http1", "http2", "runtime", "server"] }
-kube = { version = "0.69", default-features = false, features = ["admission", "client"] }
 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"
+parking_lot = "0.12"
 serde_json = "1"
-tokio = { version = "1", features = [
-    "fs",
+thiserror = "1"
+tracing = "0.1"
+
+[dependencies.kube]
+version = "0.69.1"
+default-features = false
+features = [
+    "admission",
+    "derive",
+]
+
+[dependencies.kubert]
+version = "0.3.0"
+default-features = false
+features = [
+    "clap",
+    "runtime",
+    "server",
+]
+
+[dependencies.tokio]
+version = "1"
+features = [
     "macros",
     "parking_lot",
     "rt",
     "rt-multi-thread",
     "signal",
-    "sync",
-] }
-tokio-rustls = { version = "0.23", default-features = false }
-tracing = "0.1"
-tracing-subscriber = { version = "0.3", default-features = false, features = [
-    "env-filter",
-    "fmt",
-    "json",
-    "smallvec",
-    "tracing-log",
-] }
+]
 
 [target.x86_64-unknown-linux-gnu.dependencies]
 jemallocator = "0.3"
diff --git a/policy-controller/amd64.dockerfile b/policy-controller/amd64.dockerfile
index c8f763bcf..22be04857 100644
--- a/policy-controller/amd64.dockerfile
+++ b/policy-controller/amd64.dockerfile
@@ -5,10 +5,14 @@ ARG RUNTIME_IMAGE=gcr.io/distroless/cc
 FROM $RUST_IMAGE as build
 ARG TARGETARCH
 WORKDIR /build
-COPY Cargo.toml Cargo.lock policy-controller/ /build/
+COPY Cargo.toml Cargo.lock .
+COPY policy-controller policy-controller
 RUN --mount=type=cache,target=target \
     --mount=type=cache,from=rust:1.59.0,source=/usr/local/cargo,target=/usr/local/cargo \
-    cargo build --locked --target=x86_64-unknown-linux-gnu --release --package=linkerd-policy-controller && \
+    cargo fetch --locked
+RUN --mount=type=cache,target=target \
+    --mount=type=cache,from=rust:1.59.0,source=/usr/local/cargo,target=/usr/local/cargo \
+    cargo build --frozen --target=x86_64-unknown-linux-gnu --release --package=linkerd-policy-controller && \
     mv target/x86_64-unknown-linux-gnu/release/linkerd-policy-controller /tmp/
 
 # Creates a minimal runtime image with the controller binary.
diff --git a/policy-controller/arm.dockerfile b/policy-controller/arm.dockerfile
index 62280949c..675584922 100644
--- a/policy-controller/arm.dockerfile
+++ b/policy-controller/arm.dockerfile
@@ -8,11 +8,15 @@ RUN apt-get update && \
     rustup target add armv7-unknown-linux-gnueabihf
 ENV CARGO_TARGET_ARMV7_UNKNOWN_LINUX_GNUEABIHF_LINKER=arm-linux-gnueabihf-gcc
 WORKDIR /build
-COPY Cargo.toml Cargo.lock policy-controller/ /build/
+COPY Cargo.toml Cargo.lock .
+COPY policy-controller policy-controller
+RUN --mount=type=cache,target=target \
+    --mount=type=cache,from=rust:1.59.0,source=/usr/local/cargo,target=/usr/local/cargo \
+    cargo fetch --locked
 # XXX(ver) we can't easily cross-compile against openssl, so use rustls on arm.
 RUN --mount=type=cache,target=target \
     --mount=type=cache,from=rust:1.59.0,source=/usr/local/cargo,target=/usr/local/cargo \
-    cargo build --locked --release --target=armv7-unknown-linux-gnueabihf \
+    cargo build --frozen --release --target=armv7-unknown-linux-gnueabihf \
         --package=linkerd-policy-controller --no-default-features --features="rustls-tls" && \
     mv target/armv7-unknown-linux-gnueabihf/release/linkerd-policy-controller /tmp/
 
diff --git a/policy-controller/arm64.dockerfile b/policy-controller/arm64.dockerfile
index d1c1956b3..26e3e6959 100644
--- a/policy-controller/arm64.dockerfile
+++ b/policy-controller/arm64.dockerfile
@@ -8,11 +8,15 @@ RUN apt-get update && \
     rustup target add aarch64-unknown-linux-gnu
 ENV CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc
 WORKDIR /build
-COPY Cargo.toml Cargo.lock policy-controller/ /build/
+COPY Cargo.toml Cargo.lock .
+COPY policy-controller policy-controller
+RUN --mount=type=cache,target=target \
+    --mount=type=cache,from=rust:1.59.0,source=/usr/local/cargo,target=/usr/local/cargo \
+    cargo fetch --locked
 # XXX(ver) we can't easily cross-compile against openssl, so use rustls on arm.
 RUN --mount=type=cache,target=target \
     --mount=type=cache,from=rust:1.59.0,source=/usr/local/cargo,target=/usr/local/cargo \
-    cargo build --locked --release --target=aarch64-unknown-linux-gnu \
+    cargo build --frozen --release --target=aarch64-unknown-linux-gnu \
         --package=linkerd-policy-controller --no-default-features --features="rustls-tls" && \
     mv target/aarch64-unknown-linux-gnu/release/linkerd-policy-controller /tmp/
 
diff --git a/policy-controller/k8s/api/Cargo.toml b/policy-controller/k8s/api/Cargo.toml
index 2aa70deac..7d2c4b375 100644
--- a/policy-controller/k8s/api/Cargo.toml
+++ b/policy-controller/k8s/api/Cargo.toml
@@ -9,6 +9,7 @@ publish = false
 futures = { version = "0.3", default-features = false }
 k8s-openapi = { version = "0.14", default-features = false, features = ["v1_20"] }
 kube = { version = "0.69", default-features = false, features = ["client", "derive", "runtime"] }
+parking_lot = "0.12"
 schemars = "0.8"
 serde = { version = "1", features = ["derive"] }
 serde_json = "1"
diff --git a/policy-controller/k8s/api/src/lib.rs b/policy-controller/k8s/api/src/lib.rs
index 9e60bfa42..6c8464698 100644
--- a/policy-controller/k8s/api/src/lib.rs
+++ b/policy-controller/k8s/api/src/lib.rs
@@ -3,55 +3,40 @@
 
 pub mod labels;
 pub mod policy;
-mod watch;
 
-pub use self::{
-    labels::Labels,
-    watch::{Event, Watch},
-};
+pub use self::{labels::Labels, watcher::Event};
+use futures::prelude::*;
 pub use k8s_openapi::api::{
     self,
     core::v1::{Namespace, Node, NodeSpec, Pod, PodSpec, PodStatus},
 };
 pub use kube::api::{ObjectMeta, ResourceExt};
-use kube::{
-    api::{Api, ListParams},
-    runtime::watcher,
-};
-use tracing::info_span;
-
-/// Resource watches.
-pub struct ResourceWatches {
-    pub pods_rx: Watch<Pod>,
-    pub servers_rx: Watch<policy::Server>,
-    pub authorizations_rx: Watch<policy::ServerAuthorization>,
-}
-
-// === impl ResourceWatches ===
-
-impl ResourceWatches {
-    /// Limits the amount of time a watch can be idle before being reset.
-    ///
-    /// Must be less than 295 or Kubernetes throws an error.
-    const DEFAULT_TIMEOUT_SECS: u32 = 290;
-}
-
-impl From<kube::Client> for ResourceWatches {
-    fn from(client: kube::Client) -> Self {
-        let params = ListParams::default().timeout(Self::DEFAULT_TIMEOUT_SECS);
-
-        // We only need to watch pods that are injected with a Linkerd sidecar because these are the
-        // only pods that can have inbound policy. We avoid indexing information about uninjected
-        // pods.
-        let pod_params = params.clone().labels("linkerd.io/control-plane-ns");
-
-        Self {
-            pods_rx: Watch::from(watcher(Api::all(client.clone()), pod_params))
-                .instrument(info_span!("pods")),
-            servers_rx: Watch::from(watcher(Api::all(client.clone()), params.clone()))
-                .instrument(info_span!("servers")),
-            authorizations_rx: Watch::from(watcher(Api::all(client), params))
-                .instrument(info_span!("serverauthorizations")),
-        }
+use kube::runtime::watcher;
+use parking_lot::Mutex;
+use std::sync::Arc;
+
+pub type EventResult<T> = watcher::Result<watcher::Event<T>>;
+
+/// Processes an `E`-typed event stream of `T`-typed resources.
+///
+/// The `F`-typed processor is called for each event with exclusive mutable accecss to an `S`-typed
+/// store.
+///
+/// The `H`-typed initialization handle is dropped after the first event is processed to signal to
+/// the application that the index has been updated.
+///
+/// It is assumed that the event stream is infinite. If an error is encountered, the stream is
+/// immediately polled again; and if this attempt to read from the stream fails, a backoff is
+/// employed before attempting to read from the stream again.
+pub async fn index<T, E, H, S, F>(events: E, store: Arc<Mutex<S>>, process: F)
+where
+    E: Stream<Item = watcher::Event<T>>,
+    F: Fn(&mut S, watcher::Event<T>),
+{
+    tokio::pin!(events);
+    while let Some(ev) = events.next().await {
+        process(&mut *store.lock(), ev);
     }
+
+    tracing::warn!("k8s event stream terminated");
 }
diff --git a/policy-controller/k8s/api/src/watch.rs b/policy-controller/k8s/api/src/watch.rs
deleted file mode 100644
index cccffe042..000000000
--- a/policy-controller/k8s/api/src/watch.rs
+++ /dev/null
@@ -1,73 +0,0 @@
-use futures::prelude::*;
-pub use kube::runtime::watcher::{Event, Result};
-use std::pin::Pin;
-use tokio::time;
-use tracing::{info, Instrument};
-
-/// Wraps an event stream that never terminates.
-pub struct Watch<T> {
-    initialized: bool,
-    span: tracing::Span,
-    rx: Pin<Box<dyn Stream<Item = Result<Event<T>>> + Send + 'static>>,
-}
-
-// === impl Watch ===
-
-impl<T, W> From<W> for Watch<T>
-where
-    W: Stream<Item = Result<Event<T>>> + Send + 'static,
-{
-    fn from(watch: W) -> Self {
-        Self::new(watch.boxed())
-    }
-}
-
-impl<T> Watch<T> {
-    pub fn new(rx: Pin<Box<dyn Stream<Item = Result<Event<T>>> + Send + 'static>>) -> Watch<T> {
-        Self {
-            rx,
-            initialized: false,
-            span: tracing::Span::current(),
-        }
-    }
-
-    pub fn instrument(mut self, span: tracing::Span) -> Self {
-        self.span = span;
-        self
-    }
-
-    pub fn is_initialized(&self) -> bool {
-        self.initialized
-    }
-
-    /// Receive the next event in the stream.
-    ///
-    /// If the stream fails, log the error and sleep for 1s before polling for a reset event.
-    pub async fn recv(&mut self) -> Event<T> {
-        loop {
-            let ev = self
-                .rx
-                .next()
-                .instrument(self.span.clone())
-                .await
-                .expect("stream must not terminate");
-
-            match ev {
-                Ok(ev) => {
-                    self.initialized = true;
-                    return ev;
-                }
-                Err(error) => {
-                    info!(parent: &self.span, %error, "Failed");
-
-                    // TODO(ver) this backoff may not be fully honored if this call to `recv` is
-                    // canceled. Instead, we should track the backoff on the `Watch` and poll it
-                    // before the inner stream. We probably need to use pin-project-lite for this,
-                    // though.
-                    time::sleep(time::Duration::from_secs(1)).await;
-                    info!(parent: &self.span, "Restarting");
-                }
-            }
-        }
-    }
-}
diff --git a/policy-controller/k8s/index/Cargo.toml b/policy-controller/k8s/index/Cargo.toml
index a4b61bcd0..78e48acba 100644
--- a/policy-controller/k8s/index/Cargo.toml
+++ b/policy-controller/k8s/index/Cargo.toml
@@ -11,6 +11,7 @@ anyhow = "1"
 async-stream = "0.3"
 async-trait = "0.1"
 futures = { version = "0.3", default-features = false }
+kubert = { version = "0.3.0", default-features = false, features = ["initialized"] }
 linkerd-policy-controller-core = { path = "../../core" }
 linkerd-policy-controller-k8s-api = { path = "../api" }
 parking_lot = "0.12"
diff --git a/policy-controller/k8s/index/src/authz.rs b/policy-controller/k8s/index/src/authz.rs
index 636f21f5d..d6a0b08bb 100644
--- a/policy-controller/k8s/index/src/authz.rs
+++ b/policy-controller/k8s/index/src/authz.rs
@@ -1,4 +1,4 @@
-use crate::{server::ServerSelector, ClusterInfo, Errors, Index, SrvIndex};
+use crate::{server::ServerSelector, ClusterInfo, Index, SrvIndex};
 use ahash::{AHashMap as HashMap, AHashSet as HashSet};
 use anyhow::{anyhow, bail, Result};
 use linkerd_policy_controller_core::{
@@ -10,7 +10,7 @@ use linkerd_policy_controller_k8s_api::{
     ResourceExt,
 };
 use std::collections::hash_map::Entry;
-use tracing::{debug, instrument, trace};
+use tracing::{debug, instrument, trace, warn};
 
 /// Indexes `ServerAuthorization` resources within a namespace.
 #[derive(Debug, Default)]
@@ -38,7 +38,7 @@ impl Index {
             name = %authz.name(),
         )
     )]
-    pub(crate) fn apply_authz(&mut self, authz: policy::ServerAuthorization) -> Result<()> {
+    pub(crate) fn apply_serverauthorization(&mut self, authz: policy::ServerAuthorization) {
         let ns = self
             .namespaces
             .get_or_default(authz.namespace().expect("namespace required"));
@@ -53,7 +53,7 @@ impl Index {
             name = %authz.name(),
         )
     )]
-    pub(crate) fn delete_authz(&mut self, authz: policy::ServerAuthorization) {
+    pub(crate) fn delete_serverauthorization(&mut self, authz: policy::ServerAuthorization) {
         if let Some(ns) = self
             .namespaces
             .index
@@ -66,7 +66,7 @@ impl Index {
     }
 
     #[instrument(skip(self, authzs))]
-    pub(crate) fn reset_authzs(&mut self, authzs: Vec<policy::ServerAuthorization>) -> Result<()> {
+    pub(crate) fn reset_serverauthorizations(&mut self, authzs: Vec<policy::ServerAuthorization>) {
         let mut prior = self
             .namespaces
             .index
@@ -77,15 +77,12 @@ impl Index {
             })
             .collect::<HashMap<_, _>>();
 
-        let mut errors = vec![];
         for authz in authzs.into_iter() {
             if let Some(ns) = prior.get_mut(authz.namespace().unwrap().as_str()) {
                 ns.remove(authz.name().as_str());
             }
 
-            if let Err(e) = self.apply_authz(authz) {
-                errors.push(e);
-            }
+            self.apply_serverauthorization(authz);
         }
 
         for (ns_name, authzs) in prior {
@@ -96,8 +93,6 @@ impl Index {
                 }
             }
         }
-
-        Errors::ok_if_empty(errors)
     }
 }
 
@@ -138,9 +133,15 @@ impl AuthzIndex {
         authz: policy::ServerAuthorization,
         servers: &mut SrvIndex,
         cluster: &ClusterInfo,
-    ) -> Result<()> {
+    ) {
         let name = authz.name();
-        let authz = mk_authz(authz, cluster)?;
+        let authz = match mk_serverauthorization(authz, cluster) {
+            Ok(authz) => authz,
+            Err(error) => {
+                warn!(saz = %name, %error);
+                return;
+            }
+        };
 
         match self.index.entry(name) {
             Entry::Vacant(entry) => {
@@ -156,8 +157,6 @@ impl AuthzIndex {
                 }
             }
         }
-
-        Ok(())
     }
 
     fn delete(&mut self, name: &str) {
@@ -166,7 +165,10 @@ impl AuthzIndex {
     }
 }
 
-fn mk_authz(srv: policy::authz::ServerAuthorization, cluster: &ClusterInfo) -> Result<Authz> {
+fn mk_serverauthorization(
+    srv: policy::authz::ServerAuthorization,
+    cluster: &ClusterInfo,
+) -> Result<Authz> {
     let policy::authz::ServerAuthorization { metadata, spec, .. } = srv;
 
     let servers = {
diff --git a/policy-controller/k8s/index/src/defaults.rs b/policy-controller/k8s/index/src/defaults.rs
index 5f2838159..dbd4be9c5 100644
--- a/policy-controller/k8s/index/src/defaults.rs
+++ b/policy-controller/k8s/index/src/defaults.rs
@@ -6,7 +6,7 @@ use linkerd_policy_controller_core::{
     ProxyProtocol,
 };
 use linkerd_policy_controller_k8s_api as k8s;
-use std::hash::Hash;
+use std::{collections::hash_map::Entry, hash::Hash};
 use tokio::{sync::watch, time};
 
 /// Indicates the default behavior to apply when no Server is found for a port.
@@ -130,7 +130,6 @@ impl DefaultPolicyWatches {
     ///
     /// If a watch for this policy does not already exist, one is created.
     pub fn watch(&mut self, default: DefaultPolicy, config: PortDefaults) -> ServerRx {
-        use std::collections::hash_map::Entry;
         match self.watches.entry((default, config)) {
             Entry::Occupied(entry) => entry.get().1.clone(),
             Entry::Vacant(entry) => {
diff --git a/policy-controller/k8s/index/src/lib.rs b/policy-controller/k8s/index/src/lib.rs
index 67883c40e..dfdd596f3 100644
--- a/policy-controller/k8s/index/src/lib.rs
+++ b/policy-controller/k8s/index/src/lib.rs
@@ -42,11 +42,12 @@ use self::{
     namespace::{Namespace, NamespaceIndex},
     server::SrvIndex,
 };
-use anyhow::Context;
+use futures::prelude::*;
 use linkerd_policy_controller_core::{InboundServer, IpNet};
-use linkerd_policy_controller_k8s_api::{self as k8s};
+use linkerd_policy_controller_k8s_api as k8s;
+use parking_lot::Mutex;
+use std::sync::Arc;
 use tokio::{sync::watch, time};
-use tracing::{debug, warn};
 
 /// Watches a server's configuration for server/authorization changes.
 type ServerRx = watch::Receiver<InboundServer>;
@@ -92,9 +93,6 @@ pub struct Index {
     lookups: lookup::Writer,
 }
 
-#[derive(Debug)]
-struct Errors(Vec<anyhow::Error>);
-
 // === impl Index ===
 
 impl Index {
@@ -120,98 +118,43 @@ impl Index {
         };
         (reader, idx)
     }
-
-    /// Drives indexing for all resource types.
-    ///
-    /// This is all driven on a single task, so it's not necessary for any of the indexing logic to
-    /// worry about concurrent access for the internal indexing structures.
-    ///
-    /// All updates are atomically published to the shared `lookups` map after indexing occurs; but
-    /// the indexing task is solely responsible for mutating it.
-    pub async fn run(
-        mut self,
-        resources: impl Into<k8s::ResourceWatches>,
-        ready_tx: watch::Sender<bool>,
-    ) {
-        let k8s::ResourceWatches {
-            mut pods_rx,
-            mut servers_rx,
-            mut authorizations_rx,
-        } = resources.into();
-
-        let mut initialized = false;
-        loop {
-            let res = tokio::select! {
-                // Track pods against the appropriate server.
-                up = pods_rx.recv() => match up {
-                    k8s::Event::Applied(pod) => self.apply_pod(pod).context("applying a pod"),
-                    k8s::Event::Deleted(pod) => self.delete_pod(pod).context("deleting a pod"),
-                    k8s::Event::Restarted(pods) => self.reset_pods(pods).context("resetting pods"),
-                },
-
-                // Track servers and link them with pods.
-                up = servers_rx.recv() => match up {
-                    k8s::Event::Applied(srv) => {
-                        self.apply_server(srv);
-                        Ok(())
-                    }
-                    k8s::Event::Deleted(srv) => self.delete_server(srv).context("deleting a server"),
-                    k8s::Event::Restarted(srvs) => self.reset_servers(srvs).context("resetting servers"),
-                },
-
-                // Track authorizations and update relevant servers.
-                up = authorizations_rx.recv() => match up {
-                    k8s::Event::Applied(authz) => self.apply_authz(authz).context("applying an authorization"),
-                    k8s::Event::Deleted(authz) => {
-                        self.delete_authz(authz);
-                        Ok(())
-                    }
-                    k8s::Event::Restarted(authzs) => self.reset_authzs(authzs).context("resetting authorizations"),
-                },
-            };
-
-            if let Err(error) = res {
-                warn!(?error);
-            }
-
-            // Notify the readiness watch once all watches have updated.
-            if !initialized
-                && pods_rx.is_initialized()
-                && servers_rx.is_initialized()
-                && authorizations_rx.is_initialized()
-            {
-                let _ = ready_tx.send(true);
-                initialized = true;
-                debug!("Ready");
-            }
-        }
-    }
 }
 
-// === impl Errors ===
-
-impl std::fmt::Display for Errors {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        write!(f, "{}", self.0[0])?;
-        for e in &self.0[1..] {
-            write!(f, "; and {}", e)?;
+pub async fn index_pods(idx: Arc<Mutex<Index>>, events: impl Stream<Item = k8s::Event<k8s::Pod>>) {
+    tokio::pin!(events);
+    while let Some(ev) = events.next().await {
+        match ev {
+            k8s::Event::Applied(pod) => idx.lock().apply_pod(pod),
+            k8s::Event::Deleted(pod) => idx.lock().delete_pod(pod),
+            k8s::Event::Restarted(pods) => idx.lock().reset_pods(pods),
         }
-        Ok(())
     }
 }
 
-impl std::error::Error for Errors {
-    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
-        Some(&*self.0[0])
+pub async fn index_servers(
+    idx: Arc<Mutex<Index>>,
+    events: impl Stream<Item = k8s::Event<k8s::policy::Server>>,
+) {
+    tokio::pin!(events);
+    while let Some(ev) = events.next().await {
+        match ev {
+            k8s::Event::Applied(srv) => idx.lock().apply_server(srv),
+            k8s::Event::Deleted(srv) => idx.lock().delete_server(srv),
+            k8s::Event::Restarted(srvs) => idx.lock().reset_servers(srvs),
+        }
     }
 }
 
-impl Errors {
-    fn ok_if_empty(errors: Vec<anyhow::Error>) -> anyhow::Result<()> {
-        if errors.is_empty() {
-            Ok(())
-        } else {
-            Err(Self(errors).into())
+pub async fn index_serverauthorizations(
+    idx: Arc<Mutex<Index>>,
+    events: impl Stream<Item = k8s::Event<k8s::policy::ServerAuthorization>>,
+) {
+    tokio::pin!(events);
+    while let Some(ev) = events.next().await {
+        match ev {
+            k8s::Event::Applied(saz) => idx.lock().apply_serverauthorization(saz),
+            k8s::Event::Deleted(saz) => idx.lock().delete_serverauthorization(saz),
+            k8s::Event::Restarted(sazs) => idx.lock().reset_serverauthorizations(sazs),
         }
     }
 }
diff --git a/policy-controller/k8s/index/src/lookup.rs b/policy-controller/k8s/index/src/lookup.rs
index e2207d92a..0eef32556 100644
--- a/policy-controller/k8s/index/src/lookup.rs
+++ b/policy-controller/k8s/index/src/lookup.rs
@@ -5,7 +5,7 @@ use linkerd_policy_controller_core::{DiscoverInboundServer, InboundServer, Inbou
 use parking_lot::RwLock;
 use std::{collections::hash_map::Entry, sync::Arc};
 
-#[derive(Debug, Default)]
+#[derive(Clone, Debug, Default)]
 pub(crate) struct Writer(ByNs);
 
 // Supports lookups in a shared map of pod-ports.
diff --git a/policy-controller/k8s/index/src/namespace.rs b/policy-controller/k8s/index/src/namespace.rs
index a121ec767..1f90da572 100644
--- a/policy-controller/k8s/index/src/namespace.rs
+++ b/policy-controller/k8s/index/src/namespace.rs
@@ -39,7 +39,7 @@ impl NamespaceIndex {
         })
     }
 
-    pub fn iter(&self) -> impl Iterator<Item = (&String, &Namespace)> + ExactSizeIterator {
+    pub fn iter(&self) -> std::collections::hash_map::Iter<'_, String, Namespace> {
         self.index.iter()
     }
 }
diff --git a/policy-controller/k8s/index/src/pod.rs b/policy-controller/k8s/index/src/pod.rs
index a535d07e9..ea6348408 100644
--- a/policy-controller/k8s/index/src/pod.rs
+++ b/policy-controller/k8s/index/src/pod.rs
@@ -1,5 +1,5 @@
 use crate::{
-    defaults::PortDefaults, lookup, DefaultPolicy, DefaultPolicyWatches, Errors, Index, Namespace,
+    defaults::PortDefaults, lookup, DefaultPolicy, DefaultPolicyWatches, Index, Namespace,
     PodServerTx, ServerRx, SrvIndex,
 };
 use ahash::{AHashMap as HashMap, AHashSet as HashSet};
@@ -7,7 +7,7 @@ use anyhow::{anyhow, bail, Context, Result};
 use linkerd_policy_controller_k8s_api::{self as k8s, policy, ResourceExt};
 use std::collections::hash_map::Entry;
 use tokio::sync::watch;
-use tracing::{debug, instrument, trace};
+use tracing::{debug, instrument, trace, warn};
 
 /// Indexes pod state (within a namespace).
 #[derive(Debug, Default)]
@@ -75,7 +75,7 @@ impl Index {
             name = %pod.name(),
         )
     )]
-    pub(crate) fn apply_pod(&mut self, pod: k8s::Pod) -> Result<()> {
+    pub(crate) fn apply_pod(&mut self, pod: k8s::Pod) {
         let Namespace {
             default_policy,
             ref mut pods,
@@ -85,13 +85,15 @@ impl Index {
             .namespaces
             .get_or_default(pod.namespace().expect("namespace must be set"));
 
-        pods.apply(
+        if let Err(error) = pods.apply(
             pod,
             servers,
             &mut self.lookups,
             *default_policy,
             &mut self.default_policy_watches,
-        )
+        ) {
+            warn!(%error, "failed to apply pod");
+        }
     }
 
     #[instrument(
@@ -101,14 +103,15 @@ impl Index {
             name = %pod.name(),
         )
     )]
-    pub(crate) fn delete_pod(&mut self, pod: k8s::Pod) -> Result<()> {
-        let ns_name = pod.namespace().expect("namespace must be set");
-        let pod_name = pod.name();
-        self.rm_pod(ns_name.as_str(), pod_name.as_str())
+    pub(crate) fn delete_pod(&mut self, pod: k8s::Pod) {
+        self.rm_pod(
+            &*pod.namespace().expect("namespace must be set"),
+            pod.name().as_str(),
+        );
     }
 
     #[instrument(skip(self, pods))]
-    pub(crate) fn reset_pods(&mut self, pods: Vec<k8s::Pod>) -> Result<()> {
+    pub(crate) fn reset_pods(&mut self, pods: Vec<k8s::Pod>) {
         let mut prior_pods = self
             .namespaces
             .iter()
@@ -118,44 +121,37 @@ impl Index {
             })
             .collect::<HashMap<_, _>>();
 
-        let mut errors = vec![];
         for pod in pods.into_iter() {
             let ns_name = pod.namespace().unwrap();
             if let Some(ns) = prior_pods.get_mut(ns_name.as_str()) {
                 ns.remove(pod.name().as_str());
             }
 
-            if let Err(error) = self.apply_pod(pod) {
-                errors.push(error);
-            }
+            self.apply_pod(pod);
         }
 
         for (ns, pods) in prior_pods.into_iter() {
             for pod in pods.into_iter() {
-                if let Err(error) = self.rm_pod(ns.as_str(), pod.as_str()) {
-                    errors.push(error);
-                }
+                self.rm_pod(ns.as_str(), pod.as_str());
             }
         }
-
-        Errors::ok_if_empty(errors)
     }
 
-    fn rm_pod(&mut self, ns: &str, pod: &str) -> Result<()> {
-        self.namespaces
-            .index
-            .get_mut(ns)
-            .ok_or_else(|| anyhow!("namespace {} doesn't exist", ns))?
-            .pods
-            .index
-            .remove(pod)
-            .ok_or_else(|| anyhow!("pod {} doesn't exist", pod))?;
+    fn rm_pod(&mut self, ns: &str, pod: &str) {
+        match self.namespaces.index.get_mut(ns) {
+            None => warn!(%ns, "namespace doesn't exist"),
+            Some(idx) => {
+                if idx.pods.index.remove(pod).is_none() {
+                    warn!(%pod, "pod doesn't exist");
+                }
+            }
+        }
 
-        self.lookups.unset(ns, pod)?;
+        if let Err(error) = self.lookups.unset(ns, pod) {
+            warn!(%ns, %error, "failed to unset pod lookup");
+        }
 
         debug!("Removed pod");
-
-        Ok(())
     }
 }
 
@@ -350,7 +346,7 @@ impl Pod {
         if let Some(sn) = port.server_name.as_ref() {
             if sn != name {
                 debug_assert!(false, "Pod port must not match multiple servers");
-                tracing::warn!("Pod port matches multiple servers: {} and {}", sn, name);
+                warn!("Pod port matches multiple servers: {} and {}", sn, name);
             }
             // If the name matched there's no use in proceeding with a redundant update
             return;
diff --git a/policy-controller/k8s/index/src/server.rs b/policy-controller/k8s/index/src/server.rs
index c9b0c5336..600f9d920 100644
--- a/policy-controller/k8s/index/src/server.rs
+++ b/policy-controller/k8s/index/src/server.rs
@@ -1,11 +1,10 @@
-use crate::{authz::AuthzIndex, Errors, Index, Namespace, ServerRx, ServerTx};
+use crate::{authz::AuthzIndex, Index, Namespace, ServerRx, ServerTx};
 use ahash::{AHashMap as HashMap, AHashSet as HashSet};
-use anyhow::{anyhow, bail, Result};
 use linkerd_policy_controller_core::{ClientAuthorization, InboundServer, ProxyProtocol};
 use linkerd_policy_controller_k8s_api::{self as k8s, policy, ResourceExt};
 use std::{collections::hash_map::Entry, sync::Arc};
 use tokio::{sync::watch, time};
-use tracing::{debug, instrument, trace};
+use tracing::{debug, instrument, trace, warn};
 
 /// Holds the state of all `Server`s in a namespace.
 #[derive(Debug, Default)]
@@ -79,30 +78,34 @@ impl Index {
             name = %srv.name(),
         )
     )]
-    pub(crate) fn delete_server(&mut self, srv: policy::Server) -> Result<()> {
-        let ns_name = srv.namespace().expect("servers must be namespaced");
-        self.rm_server(ns_name.as_str(), srv.name().as_str())
+    pub(crate) fn delete_server(&mut self, srv: policy::Server) {
+        self.rm_server(
+            &*srv.namespace().expect("servers must be namespaced"),
+            &*srv.name(),
+        );
     }
 
-    fn rm_server(&mut self, ns_name: &str, srv_name: &str) -> Result<()> {
-        let ns =
-            self.namespaces.index.get_mut(ns_name).ok_or_else(|| {
-                anyhow!("removing server from non-existent namespace {}", ns_name)
-            })?;
+    fn rm_server(&mut self, ns_name: &str, srv_name: &str) {
+        let ns = match self.namespaces.index.get_mut(ns_name) {
+            Some(ns) => ns,
+            None => {
+                warn!(name = %ns_name, "removing server from non-existent namespace");
+                return;
+            }
+        };
 
         if ns.servers.index.remove(srv_name).is_none() {
-            bail!("removing non-existent server {}", srv_name);
+            warn!(name = %srv_name, "unknown server");
         }
 
         // Reset the server config for all pods that were using this server.
         ns.pods.reset_server(srv_name);
 
         debug!("Removed server");
-        Ok(())
     }
 
     #[instrument(skip(self, srvs))]
-    pub(crate) fn reset_servers(&mut self, srvs: Vec<policy::Server>) -> Result<()> {
+    pub(crate) fn reset_servers(&mut self, srvs: Vec<policy::Server>) {
         let mut prior_servers = self
             .namespaces
             .index
@@ -113,7 +116,6 @@ impl Index {
             })
             .collect::<HashMap<_, _>>();
 
-        let mut errors = vec![];
         for srv in srvs.into_iter() {
             let ns_name = srv.namespace().expect("namespace must be set");
             if let Some(ns) = prior_servers.get_mut(&ns_name) {
@@ -125,13 +127,9 @@ impl Index {
 
         for (ns_name, ns_servers) in prior_servers.into_iter() {
             for srv_name in ns_servers.into_iter() {
-                if let Err(e) = self.rm_server(ns_name.as_str(), &srv_name) {
-                    errors.push(e);
-                }
+                self.rm_server(ns_name.as_str(), &srv_name);
             }
         }
-
-        Errors::ok_if_empty(errors)
     }
 }
 
diff --git a/policy-controller/src/admin.rs b/policy-controller/src/admin.rs
deleted file mode 100644
index a57fdd20e..000000000
--- a/policy-controller/src/admin.rs
+++ /dev/null
@@ -1,51 +0,0 @@
-use futures::future;
-use hyper::{Body, Request, Response};
-use std::net::SocketAddr;
-use tokio::sync::watch;
-use tracing::{info, instrument};
-
-#[instrument(skip(ready))]
-pub async fn serve(addr: SocketAddr, ready: watch::Receiver<bool>) -> Result<(), hyper::Error> {
-    let server =
-        hyper::server::Server::bind(&addr).serve(hyper::service::make_service_fn(move |_conn| {
-            let ready = ready.clone();
-            future::ok::<_, hyper::Error>(hyper::service::service_fn(
-                move |req: hyper::Request<hyper::Body>| match req.uri().path() {
-                    "/ready" => future::ok(handle_ready(&ready, req)),
-                    _ => future::ok::<_, hyper::Error>(
-                        hyper::Response::builder()
-                            .status(hyper::StatusCode::NOT_FOUND)
-                            .body(hyper::Body::default())
-                            .unwrap(),
-                    ),
-                },
-            ))
-        }));
-    let addr = server.local_addr();
-    info!(%addr, "HTTP admin server listening");
-    server.await
-}
-
-fn handle_ready(ready: &watch::Receiver<bool>, req: Request<Body>) -> Response<Body> {
-    match *req.method() {
-        hyper::Method::GET | hyper::Method::HEAD => {
-            if *ready.borrow() {
-                Response::builder()
-                    .status(hyper::StatusCode::OK)
-                    .header(hyper::header::CONTENT_TYPE, "text/plain")
-                    .body("ready\n".into())
-                    .unwrap()
-            } else {
-                Response::builder()
-                    .status(hyper::StatusCode::INTERNAL_SERVER_ERROR)
-                    .header(hyper::header::CONTENT_TYPE, "text/plain")
-                    .body("not ready\n".into())
-                    .unwrap()
-            }
-        }
-        _ => Response::builder()
-            .status(hyper::StatusCode::METHOD_NOT_ALLOWED)
-            .body(Body::default())
-            .unwrap(),
-    }
-}
diff --git a/policy-controller/src/admission.rs b/policy-controller/src/admission.rs
index ec6e8dffd..55415e3de 100644
--- a/policy-controller/src/admission.rs
+++ b/policy-controller/src/admission.rs
@@ -1,11 +1,11 @@
 use crate::api;
-use anyhow::{anyhow, bail, Context, Result};
+use anyhow::{anyhow, bail, 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::TryInto;
+use kube::{core::DynamicObject, Api, ResourceExt};
 use std::task;
+use thiserror::Error;
 use tracing::{debug, info, warn};
 
 #[derive(Clone)]
@@ -13,12 +13,21 @@ pub struct Service {
     pub client: kube::Client,
 }
 
+#[derive(Debug, Error)]
+pub enum Error {
+    #[error("failed to read request body: {0}")]
+    Request(#[from] hyper::Error),
+
+    #[error("failed to encode json response: {0}")]
+    Json(#[from] serde_json::Error),
+}
+
 impl hyper::service::Service<Request<Body>> for Service {
     type Response = Response<Body>;
-    type Error = anyhow::Error;
-    type Future = future::BoxFuture<'static, Result<Response<Body>>>;
+    type Error = Error;
+    type Future = future::BoxFuture<'static, Result<Response<Body>, Error>>;
 
-    fn poll_ready(&mut self, _cx: &mut task::Context<'_>) -> task::Poll<Result<()>> {
+    fn poll_ready(&mut self, _cx: &mut task::Context<'_>) -> task::Poll<Result<(), Error>> {
         task::Poll::Ready(Ok(()))
     }
 
@@ -93,8 +102,8 @@ impl hyper::service::Service<Request<Body>> for Service {
     }
 }
 
-fn json_response(rsp: AdmissionReview) -> Result<Response<Body>> {
-    let bytes = serde_json::to_vec(&rsp).context("failed to encode admission review as JSON")?;
+fn json_response(rsp: AdmissionReview) -> Result<Response<Body>, Error> {
+    let bytes = serde_json::to_vec(&rsp)?;
     Ok(Response::builder()
         .status(http::StatusCode::OK)
         .header(http::header::CONTENT_TYPE, "application/json")
diff --git a/policy-controller/src/lib.rs b/policy-controller/src/lib.rs
index 1561dca24..a8e129af6 100644
--- a/policy-controller/src/lib.rs
+++ b/policy-controller/src/lib.rs
@@ -1,7 +1,6 @@
 #![deny(warnings, rust_2018_idioms)]
 #![forbid(unsafe_code)]
 
-pub mod admin;
 pub mod admission;
 
 pub use linkerd_policy_controller_grpc as grpc;
diff --git a/policy-controller/src/main.rs b/policy-controller/src/main.rs
index 2616e8fd8..a8a0b72da 100644
--- a/policy-controller/src/main.rs
+++ b/policy-controller/src/main.rs
@@ -1,27 +1,23 @@
 #![deny(warnings, rust_2018_idioms)]
 #![forbid(unsafe_code)]
 
-use anyhow::{anyhow, bail, Context, Error, Result};
+use anyhow::{bail, Result};
 use clap::Parser;
-use futures::{future, prelude::*};
-use linkerd_policy_controller::k8s::DefaultPolicy;
-use linkerd_policy_controller::{admin, admission};
+use futures::prelude::*;
+use kube::api::ListParams;
+use linkerd_policy_controller::{admission, k8s};
 use linkerd_policy_controller_core::IpNet;
-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};
+use parking_lot::Mutex;
+use std::{net::SocketAddr, sync::Arc};
+use tokio::time;
+use tracing::{info, info_span, instrument, Instrument};
 
 #[cfg(all(target_os = "linux", target_arch = "x86_64", target_env = "gnu"))]
 #[global_allocator]
 static GLOBAL: jemallocator::Jemalloc = jemallocator::Jemalloc;
 
+const DETECT_TIMEOUT: time::Duration = time::Duration::from_secs(10);
+
 #[derive(Debug, Parser)]
 #[clap(name = "policy", about = "A policy resource prototype")]
 struct Args {
@@ -31,19 +27,26 @@ struct Args {
         default_value = "linkerd=info,warn",
         env = "LINKERD_POLICY_CONTROLLER_LOG"
     )]
-    log_level: EnvFilter,
+    log_level: kubert::LogFilter,
 
     #[clap(long, default_value = "plain")]
-    log_format: LogFormat,
+    log_format: kubert::LogFormat,
 
-    #[clap(long, default_value = "0.0.0.0:8080")]
-    admin_addr: SocketAddr,
+    #[clap(flatten)]
+    client: kubert::ClientArgs,
 
-    #[clap(long, default_value = "0.0.0.0:8090")]
-    grpc_addr: SocketAddr,
+    #[clap(flatten)]
+    server: kubert::ServerArgs,
 
+    #[clap(flatten)]
+    admin: kubert::AdminArgs,
+
+    /// Disables the admission controller server.
     #[clap(long)]
-    admission_addr: Option<SocketAddr>,
+    admission_controller_disabled: bool,
+
+    #[clap(long, default_value = "0.0.0.0:8090")]
+    grpc_addr: SocketAddr,
 
     /// Network CIDRs of pod IPs.
     ///
@@ -58,74 +61,85 @@ struct Args {
     identity_domain: String,
 
     #[clap(long, default_value = "all-unauthenticated")]
-    default_policy: DefaultPolicy,
+    default_policy: k8s::DefaultPolicy,
 
     #[clap(long, default_value = "linkerd")]
     control_plane_namespace: String,
 }
 
-#[derive(Clone, Debug)]
-enum LogFormat {
-    Json,
-    Plain,
-}
-
 #[tokio::main]
 async fn main() -> Result<()> {
     let Args {
-        admin_addr,
+        admin,
+        client,
+        log_level,
+        log_format,
+        server,
         grpc_addr,
-        admission_addr,
+        admission_controller_disabled,
         identity_domain,
         cluster_networks: IpNets(cluster_networks),
         default_policy,
-        log_level,
-        log_format,
         control_plane_namespace,
     } = Args::parse();
 
-    log_init(log_level, log_format)?;
-
-    let (drain_tx, drain_rx) = drain::channel();
-
-    // Load a Kubernetes client from the environment (check for in-cluster configuration first).
-    //
-    // TODO support --kubeconfig and --context command-line arguments.
-    let client = kube::Client::try_default()
-        .await
-        .context("failed to initialize kubernetes client")?;
-
-    // Spawn an admin server, failing readiness checks until the index is updated.
-    let (ready_tx, ready_rx) = watch::channel(false);
-    tokio::spawn(admin::serve(admin_addr, ready_rx));
+    let server = if admission_controller_disabled {
+        None
+    } else {
+        Some(server)
+    };
 
-    // Index cluster resources, returning a handle that supports lookups for the gRPC server.
-    let handle = {
-        const DETECT_TIMEOUT: time::Duration = time::Duration::from_secs(10);
-        let cluster = linkerd_policy_controller::k8s::ClusterInfo {
+    let mut runtime = kubert::Runtime::builder()
+        .with_log(log_level, log_format)
+        .with_admin(admin)
+        .with_client(client)
+        .with_optional_server(server)
+        .build()
+        .await?;
+
+    // Build the index data structure, which will be used to process events from all watches
+    // The lookup handle is used by the gRPC server.
+    let (lookup, index) = {
+        let cluster = k8s::ClusterInfo {
             networks: cluster_networks.clone(),
             identity_domain,
             control_plane_ns: control_plane_namespace,
         };
-        let (handle, index) =
-            linkerd_policy_controller::k8s::Index::new(cluster, default_policy, DETECT_TIMEOUT);
-
-        tokio::spawn(index.run(client.clone(), ready_tx));
-        handle
+        let (lookup, idx) = k8s::Index::new(cluster, default_policy, DETECT_TIMEOUT);
+        (lookup, Arc::new(Mutex::new(idx)))
     };
 
+    // Spawn resource indexers that update the index and publish lookups for the gRPC server.
+
+    let pods = runtime.watch_all(ListParams::default().labels("linkerd.io/control-plane-ns"));
+    tokio::spawn(k8s::index_pods(index.clone(), pods).instrument(info_span!("pods")));
+
+    let servers = runtime.watch_all(ListParams::default());
+    tokio::spawn(k8s::index_servers(index.clone(), servers).instrument(info_span!("servers")));
+
+    let serverauthorizations = runtime.watch_all(ListParams::default());
+    tokio::spawn(
+        k8s::index_serverauthorizations(index, serverauthorizations)
+            .instrument(info_span!("serverauthorizations")),
+    );
+
     // Run the gRPC server, serving results by looking up against the index handle.
-    tokio::spawn(grpc(grpc_addr, cluster_networks, handle, drain_rx.clone()));
+    tokio::spawn(grpc(
+        grpc_addr,
+        cluster_networks,
+        lookup,
+        runtime.shutdown_handle(),
+    ));
 
-    if let Some(bind_addr) = admission_addr {
-        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");
-    }
+    // TODO use resource caches instead of a client
+    let client = runtime.client();
+    let runtime = runtime.spawn_server(|| admission::Service { client });
 
     // Block the main thread on the shutdown signal. Once it fires, wait for the background tasks to
     // complete before exiting.
-    shutdown(drain_tx).await;
+    if runtime.run().await.is_err() {
+        bail!("Aborted");
+    }
 
     Ok(())
 }
@@ -166,173 +180,3 @@ async fn grpc(
     }
     Ok(())
 }
-
-async fn shutdown(drain: drain::Signal) {
-    tokio::select! {
-        _ = tokio::signal::ctrl_c() => {
-            debug!("Received ctrl-c");
-        },
-        _ = sigterm() => {
-            debug!("Received SIGTERM");
-        }
-    }
-    info!("Shutting down");
-    drain.drain().await;
-}
-
-async fn sigterm() {
-    use tokio::signal::unix::{signal, SignalKind};
-    match signal(SignalKind::terminate()) {
-        Ok(mut term) => term.recv().await,
-        _ => future::pending().await,
-    };
-}
-
-fn log_init(filter: EnvFilter, format: LogFormat) -> Result<()> {
-    let registry = tracing_subscriber::registry().with(filter);
-
-    match format {
-        LogFormat::Plain => registry.with(tracing_subscriber::fmt::layer()).try_init()?,
-
-        LogFormat::Json => {
-            let event_fmt = tracing_subscriber::fmt::format()
-                // Configure the formatter to output JSON logs.
-                .json()
-                // Output the current span context as a JSON list.
-                .with_span_list(true)
-                // Don't output a field for the current span, since this
-                // would duplicate information already in the span list.
-                .with_current_span(false);
-
-            // Use the JSON event formatter and the JSON field formatter.
-            let fmt = tracing_subscriber::fmt::layer()
-                .event_format(event_fmt)
-                .fmt_fields(format::JsonFields::default());
-
-            registry.with(fmt).try_init()?
-        }
-    };
-
-    Ok(())
-}
-
-// === impl LogFormat ===
-
-impl std::str::FromStr for LogFormat {
-    type Err = Error;
-
-    fn from_str(s: &str) -> Result<Self> {
-        if s == "json" {
-            Ok(Self::Json)
-        } else if s == "plain" {
-            Ok(Self::Plain)
-        } else {
-            bail!("invalid log format: {}", s)
-        }
-    }
-}
-
-// 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/proxy-identity/main.go b/proxy-identity/main.go
index 722bd6b9a..f51e56de3 100644
--- a/proxy-identity/main.go
+++ b/proxy-identity/main.go
@@ -146,6 +146,8 @@ func generateAndStoreCSR(p, id string, key *ecdsa.PrivateKey) ([]byte, error) {
 }
 
 func runProxy() {
+	// The input arguments are static.
+	//nolint:gosec
 	err := syscall.Exec("/usr/lib/linkerd/linkerd2-proxy", []string{}, os.Environ())
 	if err != nil {
 		log.Fatalf("Failed to run proxy: %s", err)
diff --git a/viz/metrics-api/cmd/main.go b/viz/metrics-api/cmd/main.go
index 7e9da7d26..9ce3499e5 100644
--- a/viz/metrics-api/cmd/main.go
+++ b/viz/metrics-api/cmd/main.go
@@ -76,14 +76,18 @@ func main() {
 
 	go func() {
 		log.Infof("starting HTTP server on %+v", *addr)
-		server.ListenAndServe()
+		if err := server.ListenAndServe(); err != nil {
+			log.Errorf("failed to start metrics API HTTP server: %s", err)
+		}
 	}()
 
 	adminServer := admin.NewServer(*metricsAddr)
 
 	go func() {
 		log.Infof("starting admin server on %s", *metricsAddr)
-		adminServer.ListenAndServe()
+		if err := adminServer.ListenAndServe(); err != nil {
+			log.Errorf("failed to start metrics API admin server: %s", err)
+		}
 	}()
 
 	<-stop
diff --git a/viz/tap/api/grpc_server.go b/viz/tap/api/grpc_server.go
index fcaa1cc33..4be27b2b7 100644
--- a/viz/tap/api/grpc_server.go
+++ b/viz/tap/api/grpc_server.go
@@ -533,11 +533,15 @@ func NewGrpcTapServer(
 	controllerNamespace string,
 	trustDomain string,
 	k8sAPI *k8s.API,
-) *GRPCTapServer {
-	k8sAPI.Pod().Informer().AddIndexers(cache.Indexers{ipIndex: indexByIP})
-	k8sAPI.Node().Informer().AddIndexers(cache.Indexers{ipIndex: indexByIP})
+) (*GRPCTapServer, error) {
+	if err := k8sAPI.Pod().Informer().AddIndexers(cache.Indexers{ipIndex: indexByIP}); err != nil {
+		return nil, err
+	}
+	if err := k8sAPI.Node().Informer().AddIndexers(cache.Indexers{ipIndex: indexByIP}); err != nil {
+		return nil, err
+	}
 
-	return newGRPCTapServer(tapPort, controllerNamespace, trustDomain, k8sAPI)
+	return newGRPCTapServer(tapPort, controllerNamespace, trustDomain, k8sAPI), nil
 }
 
 func newGRPCTapServer(
diff --git a/viz/tap/api/main.go b/viz/tap/api/main.go
index 2799e11b1..d59045d22 100644
--- a/viz/tap/api/main.go
+++ b/viz/tap/api/main.go
@@ -56,7 +56,10 @@ func Main(args []string) {
 			log.Warnf("failed to initialize tracing: %s", err)
 		}
 	}
-	grpcTapServer := NewGrpcTapServer(*tapPort, *apiNamespace, *trustDomain, k8sAPI)
+	grpcTapServer, err := NewGrpcTapServer(*tapPort, *apiNamespace, *trustDomain, k8sAPI)
+	if err != nil {
+		log.Fatal(err.Error())
+	}
 	apiServer, err := NewServer(ctx, *apiServerAddr, k8sAPI, grpcTapServer, *disableCommonNames)
 	if err != nil {
 		log.Fatal(err.Error())
@@ -68,7 +71,9 @@ func Main(args []string) {
 
 	go func() {
 		log.Infof("starting admin server on %s", *metricsAddr)
-		adminServer.ListenAndServe()
+		if err := adminServer.ListenAndServe(); err != nil {
+			log.Errorf("failed to start tap admin server: %s", err)
+		}
 	}()
 
 	<-stop
diff --git a/web/app/js/components/NetworkGraph.jsx b/web/app/js/components/NetworkGraph.jsx
index d3ac053f0..04345dd53 100644
--- a/web/app/js/components/NetworkGraph.jsx
+++ b/web/app/js/components/NetworkGraph.jsx
@@ -210,6 +210,8 @@ NetworkGraphBase.defaultProps = {
 
 NetworkGraphBase.propTypes = {
   data: PropTypes.arrayOf(metricsPropType.isRequired).isRequired,
+  /* these objects have a lot of stuff */
+  /* eslint-disable react/forbid-prop-types */
   deployments: PropTypes.arrayOf(PropTypes.object),
 };
 
diff --git a/web/app/js/components/StatusTable.jsx b/web/app/js/components/StatusTable.jsx
index 56c961022..358fbf315 100644
--- a/web/app/js/components/StatusTable.jsx
+++ b/web/app/js/components/StatusTable.jsx
@@ -124,7 +124,12 @@ const StatusTable = function({ classes, statusColumnTitle, data }) {
 StatusTable.propTypes = {
   data: PropTypes.arrayOf(PropTypes.shape({
     name: PropTypes.string.isRequired,
-    pods: PropTypes.arrayOf(PropTypes.object).isRequired, // TODO: What's the real shape here.
+    pods: PropTypes.shape({
+      name: PropTypes.string.isRequired,
+      value: PropTypes.string.isRequired,
+      uptime: PropTypes.string.isRequired,
+      uptimeSec: PropTypes.number.isRequired,
+    }).isRequired,
     added: PropTypes.bool,
   })).isRequired,
   statusColumnTitle: PropTypes.string.isRequired,
diff --git a/web/app/package.json b/web/app/package.json
index 42455a286..fc6ee63e4 100644
--- a/web/app/package.json
+++ b/web/app/package.json
@@ -59,16 +59,16 @@
     "css-loader": "^6.6.0",
     "enzyme": "3.11.0",
     "enzyme-adapter-react-16": "^1.15.6",
-    "eslint": "^8.9.0",
+    "eslint": "^8.10.0",
     "eslint-config-airbnb": "^19.0.4",
     "eslint-plugin-import": "^2.25.4",
     "eslint-plugin-jsx-a11y": "^6.5.1",
     "eslint-plugin-promise": "^6.0.0",
-    "eslint-plugin-react": "^7.28.0",
+    "eslint-plugin-react": "^7.29.2",
     "eslint-plugin-react-hooks": "^4.3.0",
     "eslint-webpack-plugin": "^3.1.1",
     "file-loader": "^6.2.0",
-    "history": "5.2.0",
+    "history": "5.3.0",
     "html-webpack-plugin": "^5.5.0",
     "jest": "^27.5.1",
     "lodash-webpack-plugin": "^0.11.6",
diff --git a/web/app/yarn.lock b/web/app/yarn.lock
index 05fc79cba..9068cf8c6 100644
--- a/web/app/yarn.lock
+++ b/web/app/yarn.lock
@@ -1051,10 +1051,10 @@
     ts-node "^9"
     tslib "^2"
 
-"@eslint/eslintrc@^1.1.0":
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.1.0.tgz#583d12dbec5d4f22f333f9669f7d0b7c7815b4d3"
-  integrity sha512-C1DfL7XX4nPqGd6jcP01W9pVM1HYCuUkFk1432D7F0v3JSlUIeOYn9oCoi3eoLZ+iwBSb29BMFxxny0YrrEZqg==
+"@eslint/eslintrc@^1.2.0":
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.2.0.tgz#7ce1547a5c46dfe56e1e45c3c9ed18038c721c6a"
+  integrity sha512-igm9SjJHNEJRiUnecP/1R5T3wKLEJ7pL6e2P+GUSfCd0dGjPYYZve08uzw8L2J8foVHFz+NGu12JxRcU2gGo6w==
   dependencies:
     ajv "^6.12.4"
     debug "^4.3.2"
@@ -3706,22 +3706,22 @@ eslint-plugin-react-hooks@^4.3.0:
   resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.3.0.tgz#318dbf312e06fab1c835a4abef00121751ac1172"
   integrity sha512-XslZy0LnMn+84NEG9jSGR6eGqaZB3133L8xewQo3fQagbQuGt7a63gf+P1NGKZavEYEC3UXaWEAA/AqDkuN6xA==
 
-eslint-plugin-react@^7.28.0:
-  version "7.28.0"
-  resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.28.0.tgz#8f3ff450677571a659ce76efc6d80b6a525adbdf"
-  integrity sha512-IOlFIRHzWfEQQKcAD4iyYDndHwTQiCMcJVJjxempf203jnNLUnW34AXLrV33+nEXoifJE2ZEGmcjKPL8957eSw==
+eslint-plugin-react@^7.29.2:
+  version "7.29.2"
+  resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.29.2.tgz#2d4da69d30d0a736efd30890dc6826f3e91f3f7c"
+  integrity sha512-ypEBTKOy5liFQXZWMchJ3LN0JX1uPI6n7MN7OPHKacqXAxq5gYC30TdO7wqGYQyxD1OrzpobdHC3hDmlRWDg9w==
   dependencies:
     array-includes "^3.1.4"
     array.prototype.flatmap "^1.2.5"
     doctrine "^2.1.0"
     estraverse "^5.3.0"
     jsx-ast-utils "^2.4.1 || ^3.0.0"
-    minimatch "^3.0.4"
+    minimatch "^3.1.2"
     object.entries "^1.1.5"
     object.fromentries "^2.0.5"
     object.hasown "^1.1.0"
     object.values "^1.1.5"
-    prop-types "^15.7.2"
+    prop-types "^15.8.1"
     resolve "^2.0.0-next.3"
     semver "^6.3.0"
     string.prototype.matchall "^4.0.6"
@@ -3775,12 +3775,12 @@ eslint-webpack-plugin@^3.1.1:
     normalize-path "^3.0.0"
     schema-utils "^3.1.1"
 
-eslint@^8.9.0:
-  version "8.9.0"
-  resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.9.0.tgz#a2a8227a99599adc4342fd9b854cb8d8d6412fdb"
-  integrity sha512-PB09IGwv4F4b0/atrbcMFboF/giawbBLVC7fyDamk5Wtey4Jh2K+rYaBhCAbUyEI4QzB1ly09Uglc9iCtFaG2Q==
+eslint@^8.10.0:
+  version "8.10.0"
+  resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.10.0.tgz#931be395eb60f900c01658b278e05b6dae47199d"
+  integrity sha512-tcI1D9lfVec+R4LE1mNDnzoJ/f71Kl/9Cv4nG47jOueCMBrCCKYXr4AUVS7go6mWYGFD4+EoN6+eXSrEbRzXVw==
   dependencies:
-    "@eslint/eslintrc" "^1.1.0"
+    "@eslint/eslintrc" "^1.2.0"
     "@humanwhocodes/config-array" "^0.9.2"
     ajv "^6.10.0"
     chalk "^4.0.0"
@@ -4335,10 +4335,10 @@ he@^1.2.0:
   resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
   integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
 
-history@5.2.0:
-  version "5.2.0"
-  resolved "https://registry.yarnpkg.com/history/-/history-5.2.0.tgz#7cdd31cf9bac3c5d31f09c231c9928fad0007b7c"
-  integrity sha512-uPSF6lAJb3nSePJ43hN3eKj1dTWpN9gMod0ZssbFTIsen+WehTmEadgL+kg78xLJFdRfrrC//SavDzmRVdE+Ig==
+history@5.3.0:
+  version "5.3.0"
+  resolved "https://registry.yarnpkg.com/history/-/history-5.3.0.tgz#1548abaa245ba47992f063a0783db91ef201c73b"
+  integrity sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==
   dependencies:
     "@babel/runtime" "^7.7.6"
 
@@ -5876,10 +5876,10 @@ minimalistic-assert@^1.0.0:
   resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7"
   integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==
 
-minimatch@^3.0.4:
-  version "3.0.4"
-  resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
-  integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
+minimatch@^3.0.4, minimatch@^3.1.2:
+  version "3.1.2"
+  resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
+  integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
   dependencies:
     brace-expansion "^1.1.7"
 
diff --git a/web/main.go b/web/main.go
index 0ecea3b53..c6f507be1 100644
--- a/web/main.go
+++ b/web/main.go
@@ -101,14 +101,18 @@ func main() {
 
 	go func() {
 		log.Infof("starting HTTP server on %+v", *addr)
-		server.ListenAndServe()
+		if err := server.ListenAndServe(); err != nil {
+			log.Errorf("failed to start web HTTP server: %s", err)
+		}
 	}()
 
 	adminServer := admin.NewServer(*metricsAddr)
 
 	go func() {
 		log.Infof("starting admin server on %s", *metricsAddr)
-		adminServer.ListenAndServe()
+		if err := adminServer.ListenAndServe(); err != nil {
+			log.Errorf("failed to start web admin server: %s", err)
+		}
 	}()
 
 	<-stop
diff --git a/web/srv/api_handlers.go b/web/srv/api_handlers.go
index 91198e993..727a45a84 100644
--- a/web/srv/api_handlers.go
+++ b/web/srv/api_handlers.go
@@ -76,6 +76,8 @@ func renderJSON(w http.ResponseWriter, resp interface{}) {
 
 func renderJSONPb(w http.ResponseWriter, msg proto.Message) {
 	w.Header().Set("Content-Type", "application/json")
+	// Callers of renderJSONPb pass in msgs that can be serialized into JSON.
+	//nolint:errcheck
 	pbMarshaler.Marshal(w, msg)
 }
 


Test patch:
diff --git a/.github/workflows/integration_tests.yml b/.github/workflows/integration_tests.yml
index 43450dfeb..617bb2c6f 100644
--- a/.github/workflows/integration_tests.yml
+++ b/.github/workflows/integration_tests.yml
@@ -34,7 +34,7 @@ jobs:
     timeout-minutes: 30
     steps:
     - name: Checkout code
-      uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
+      uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
     - uses: ./.github/actions/docker-build
       with:
         docker-registry: ${{ env.DOCKER_REGISTRY }}
@@ -77,15 +77,15 @@ jobs:
     timeout-minutes: 60
     steps:
     - name: Checkout code
-      uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
-    - uses: actions/setup-go@bfdd3570ce990073878bf10f6b2d79082de49492
+      uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
+    - uses: actions/setup-go@f6164bd8c8acb4a71fb2791a8b6c4024ff038dab
       with:
         go-version: '1.17'
     - name: Set environment variables from scripts
       run: |
         echo "TAG=$(CI_FORCE_CLEAN=1 bin/root-tag)" >> $GITHUB_ENV
     - name: Download image archives
-      uses: actions/download-artifact@f023be2c48cc18debc3bacd34cb396e0295e2869
+      uses: actions/download-artifact@fb598a63ae348fa914e94cd0ff38f362e927b741
       with:
         name: image-archives
         path: image-archives
diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml
index 68b5128c9..7848f6125 100644
--- a/.github/workflows/unit_tests.yml
+++ b/.github/workflows/unit_tests.yml
@@ -18,7 +18,7 @@ jobs:
       image: golang:1.17.3
     steps:
     - name: Checkout code
-      uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
+      uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
     - name: Go unit tests
       run: |
         go get gotest.tools/gotestsum@v0.4.2
@@ -32,7 +32,7 @@ jobs:
       image: node:14-stretch
     steps:
     - name: Checkout code
-      uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
+      uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
     - name: Yarn setup
       run: bin/scurl --retry=2 https://yarnpkg.com/install.sh | bash -s -- --version 1.21.1 --network-concurrency 1
     - name: JS unit tests
diff --git a/cli/cmd/inject_test.go b/cli/cmd/inject_test.go
index f3b56372d..5dd072871 100644
--- a/cli/cmd/inject_test.go
+++ b/cli/cmd/inject_test.go
@@ -624,10 +624,10 @@ func TestWalk(t *testing.T) {
 		file1 = filepath.Join(tmpFolderRoot, "root.txt")
 		file2 = filepath.Join(tmpFolderData, "data.txt")
 	)
-	if err := ioutil.WriteFile(file1, data, 0644); err != nil {
+	if err := ioutil.WriteFile(file1, data, 0600); err != nil {
 		t.Fatal("Unexpected error: ", err)
 	}
-	if err := ioutil.WriteFile(file2, data, 0644); err != nil {
+	if err := ioutil.WriteFile(file2, data, 0600); err != nil {
 		t.Fatal("Unexpected error: ", err)
 	}
 
diff --git a/cli/cmd/testdata/install_controlplane_tracing_output.golden b/cli/cmd/testdata/install_controlplane_tracing_output.golden
index 992f361c3..65d7ae034 100644
--- a/cli/cmd/testdata/install_controlplane_tracing_output.golden
+++ b/cli/cmd/testdata/install_controlplane_tracing_output.golden
@@ -1937,7 +1937,9 @@ spec:
         - --admin-addr=0.0.0.0:9990
         - --control-plane-namespace=linkerd
         - --grpc-addr=0.0.0.0:8090
-        - --admission-addr=0.0.0.0:9443
+        - --server-addr=0.0.0.0:9443
+        - --server-tls-key=/var/run/linkerd/tls/tls.key
+        - --server-tls-certs=/var/run/linkerd/tls/tls.crt
         - --cluster-networks=10.0.0.0/8,100.64.0.0/10,172.16.0.0/12,192.168.0.0/16
         - --identity-domain=cluster.local
         - --default-policy=all-unauthenticated
@@ -1947,9 +1949,8 @@ spec:
         imagePullPolicy: IfNotPresent
         livenessProbe:
           httpGet:
-            path: /ready
+            path: /live
             port: admin
-          initialDelaySeconds: 10
         name: policy
         ports:
         - containerPort: 8090
@@ -1963,6 +1964,7 @@ spec:
           httpGet:
             path: /ready
             port: admin
+          initialDelaySeconds: 10
         resources:
         securityContext:
           runAsUser: 2103
diff --git a/cli/cmd/testdata/install_custom_domain.golden b/cli/cmd/testdata/install_custom_domain.golden
index c6b15ff9f..bdb7d24ce 100644
--- a/cli/cmd/testdata/install_custom_domain.golden
+++ b/cli/cmd/testdata/install_custom_domain.golden
@@ -1935,7 +1935,9 @@ spec:
         - --admin-addr=0.0.0.0:9990
         - --control-plane-namespace=linkerd
         - --grpc-addr=0.0.0.0:8090
-        - --admission-addr=0.0.0.0:9443
+        - --server-addr=0.0.0.0:9443
+        - --server-tls-key=/var/run/linkerd/tls/tls.key
+        - --server-tls-certs=/var/run/linkerd/tls/tls.crt
         - --cluster-networks=10.0.0.0/8,100.64.0.0/10,172.16.0.0/12,192.168.0.0/16
         - --identity-domain=cluster.local
         - --default-policy=all-unauthenticated
@@ -1945,9 +1947,8 @@ spec:
         imagePullPolicy: IfNotPresent
         livenessProbe:
           httpGet:
-            path: /ready
+            path: /live
             port: admin
-          initialDelaySeconds: 10
         name: policy
         ports:
         - containerPort: 8090
@@ -1961,6 +1962,7 @@ spec:
           httpGet:
             path: /ready
             port: admin
+          initialDelaySeconds: 10
         resources:
         securityContext:
           runAsUser: 2103
diff --git a/cli/cmd/testdata/install_custom_registry.golden b/cli/cmd/testdata/install_custom_registry.golden
index 6ab8dec5e..45f32796d 100644
--- a/cli/cmd/testdata/install_custom_registry.golden
+++ b/cli/cmd/testdata/install_custom_registry.golden
@@ -1935,7 +1935,9 @@ spec:
         - --admin-addr=0.0.0.0:9990
         - --control-plane-namespace=linkerd
         - --grpc-addr=0.0.0.0:8090
-        - --admission-addr=0.0.0.0:9443
+        - --server-addr=0.0.0.0:9443
+        - --server-tls-key=/var/run/linkerd/tls/tls.key
+        - --server-tls-certs=/var/run/linkerd/tls/tls.crt
         - --cluster-networks=10.0.0.0/8,100.64.0.0/10,172.16.0.0/12,192.168.0.0/16
         - --identity-domain=cluster.local
         - --default-policy=all-unauthenticated
@@ -1945,9 +1947,8 @@ spec:
         imagePullPolicy: IfNotPresent
         livenessProbe:
           httpGet:
-            path: /ready
+            path: /live
             port: admin
-          initialDelaySeconds: 10
         name: policy
         ports:
         - containerPort: 8090
@@ -1961,6 +1962,7 @@ spec:
           httpGet:
             path: /ready
             port: admin
+          initialDelaySeconds: 10
         resources:
         securityContext:
           runAsUser: 2103
diff --git a/cli/cmd/testdata/install_default.golden b/cli/cmd/testdata/install_default.golden
index c6b15ff9f..bdb7d24ce 100644
--- a/cli/cmd/testdata/install_default.golden
+++ b/cli/cmd/testdata/install_default.golden
@@ -1935,7 +1935,9 @@ spec:
         - --admin-addr=0.0.0.0:9990
         - --control-plane-namespace=linkerd
         - --grpc-addr=0.0.0.0:8090
-        - --admission-addr=0.0.0.0:9443
+        - --server-addr=0.0.0.0:9443
+        - --server-tls-key=/var/run/linkerd/tls/tls.key
+        - --server-tls-certs=/var/run/linkerd/tls/tls.crt
         - --cluster-networks=10.0.0.0/8,100.64.0.0/10,172.16.0.0/12,192.168.0.0/16
         - --identity-domain=cluster.local
         - --default-policy=all-unauthenticated
@@ -1945,9 +1947,8 @@ spec:
         imagePullPolicy: IfNotPresent
         livenessProbe:
           httpGet:
-            path: /ready
+            path: /live
             port: admin
-          initialDelaySeconds: 10
         name: policy
         ports:
         - containerPort: 8090
@@ -1961,6 +1962,7 @@ spec:
           httpGet:
             path: /ready
             port: admin
+          initialDelaySeconds: 10
         resources:
         securityContext:
           runAsUser: 2103
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 c46118241..dbb6ac969 100644
--- a/cli/cmd/testdata/install_default_override_dst_get_nets.golden
+++ b/cli/cmd/testdata/install_default_override_dst_get_nets.golden
@@ -1935,7 +1935,9 @@ spec:
         - --admin-addr=0.0.0.0:9990
         - --control-plane-namespace=linkerd
         - --grpc-addr=0.0.0.0:8090
-        - --admission-addr=0.0.0.0:9443
+        - --server-addr=0.0.0.0:9443
+        - --server-tls-key=/var/run/linkerd/tls/tls.key
+        - --server-tls-certs=/var/run/linkerd/tls/tls.crt
         - --cluster-networks=10.0.0.0/8,100.64.0.0/10,172.0.0.0/8
         - --identity-domain=cluster.local
         - --default-policy=all-unauthenticated
@@ -1945,9 +1947,8 @@ spec:
         imagePullPolicy: IfNotPresent
         livenessProbe:
           httpGet:
-            path: /ready
+            path: /live
             port: admin
-          initialDelaySeconds: 10
         name: policy
         ports:
         - containerPort: 8090
@@ -1961,6 +1962,7 @@ spec:
           httpGet:
             path: /ready
             port: admin
+          initialDelaySeconds: 10
         resources:
         securityContext:
           runAsUser: 2103
diff --git a/cli/cmd/testdata/install_default_token.golden b/cli/cmd/testdata/install_default_token.golden
index a81a767aa..4c3079d50 100644
--- a/cli/cmd/testdata/install_default_token.golden
+++ b/cli/cmd/testdata/install_default_token.golden
@@ -1924,7 +1924,9 @@ spec:
         - --admin-addr=0.0.0.0:9990
         - --control-plane-namespace=linkerd
         - --grpc-addr=0.0.0.0:8090
-        - --admission-addr=0.0.0.0:9443
+        - --server-addr=0.0.0.0:9443
+        - --server-tls-key=/var/run/linkerd/tls/tls.key
+        - --server-tls-certs=/var/run/linkerd/tls/tls.crt
         - --cluster-networks=10.0.0.0/8,100.64.0.0/10,172.16.0.0/12,192.168.0.0/16
         - --identity-domain=cluster.local
         - --default-policy=all-unauthenticated
@@ -1934,9 +1936,8 @@ spec:
         imagePullPolicy: IfNotPresent
         livenessProbe:
           httpGet:
-            path: /ready
+            path: /live
             port: admin
-          initialDelaySeconds: 10
         name: policy
         ports:
         - containerPort: 8090
@@ -1950,6 +1951,7 @@ spec:
           httpGet:
             path: /ready
             port: admin
+          initialDelaySeconds: 10
         resources:
         securityContext:
           runAsUser: 2103
diff --git a/cli/cmd/testdata/install_ha_output.golden b/cli/cmd/testdata/install_ha_output.golden
index c73579780..c4edfb7bc 100644
--- a/cli/cmd/testdata/install_ha_output.golden
+++ b/cli/cmd/testdata/install_ha_output.golden
@@ -2071,7 +2071,9 @@ spec:
         - --admin-addr=0.0.0.0:9990
         - --control-plane-namespace=linkerd
         - --grpc-addr=0.0.0.0:8090
-        - --admission-addr=0.0.0.0:9443
+        - --server-addr=0.0.0.0:9443
+        - --server-tls-key=/var/run/linkerd/tls/tls.key
+        - --server-tls-certs=/var/run/linkerd/tls/tls.crt
         - --cluster-networks=10.0.0.0/8,100.64.0.0/10,172.16.0.0/12,192.168.0.0/16
         - --identity-domain=cluster.local
         - --default-policy=all-unauthenticated
@@ -2081,9 +2083,8 @@ spec:
         imagePullPolicy: IfNotPresent
         livenessProbe:
           httpGet:
-            path: /ready
+            path: /live
             port: admin
-          initialDelaySeconds: 10
         name: policy
         ports:
         - containerPort: 8090
@@ -2097,6 +2098,7 @@ spec:
           httpGet:
             path: /ready
             port: admin
+          initialDelaySeconds: 10
         resources:
         securityContext:
           runAsUser: 2103
diff --git a/cli/cmd/testdata/install_ha_with_overrides_output.golden b/cli/cmd/testdata/install_ha_with_overrides_output.golden
index a23f05517..5a19443b3 100644
--- a/cli/cmd/testdata/install_ha_with_overrides_output.golden
+++ b/cli/cmd/testdata/install_ha_with_overrides_output.golden
@@ -2071,7 +2071,9 @@ spec:
         - --admin-addr=0.0.0.0:9990
         - --control-plane-namespace=linkerd
         - --grpc-addr=0.0.0.0:8090
-        - --admission-addr=0.0.0.0:9443
+        - --server-addr=0.0.0.0:9443
+        - --server-tls-key=/var/run/linkerd/tls/tls.key
+        - --server-tls-certs=/var/run/linkerd/tls/tls.crt
         - --cluster-networks=10.0.0.0/8,100.64.0.0/10,172.16.0.0/12,192.168.0.0/16
         - --identity-domain=cluster.local
         - --default-policy=all-unauthenticated
@@ -2081,9 +2083,8 @@ spec:
         imagePullPolicy: IfNotPresent
         livenessProbe:
           httpGet:
-            path: /ready
+            path: /live
             port: admin
-          initialDelaySeconds: 10
         name: policy
         ports:
         - containerPort: 8090
@@ -2097,6 +2098,7 @@ spec:
           httpGet:
             path: /ready
             port: admin
+          initialDelaySeconds: 10
         resources:
         securityContext:
           runAsUser: 2103
diff --git a/cli/cmd/testdata/install_heartbeat_disabled_output.golden b/cli/cmd/testdata/install_heartbeat_disabled_output.golden
index dc6040da2..51ff6a691 100644
--- a/cli/cmd/testdata/install_heartbeat_disabled_output.golden
+++ b/cli/cmd/testdata/install_heartbeat_disabled_output.golden
@@ -1866,7 +1866,9 @@ spec:
         - --admin-addr=0.0.0.0:9990
         - --control-plane-namespace=linkerd
         - --grpc-addr=0.0.0.0:8090
-        - --admission-addr=0.0.0.0:9443
+        - --server-addr=0.0.0.0:9443
+        - --server-tls-key=/var/run/linkerd/tls/tls.key
+        - --server-tls-certs=/var/run/linkerd/tls/tls.crt
         - --cluster-networks=10.0.0.0/8,100.64.0.0/10,172.16.0.0/12,192.168.0.0/16
         - --identity-domain=cluster.local
         - --default-policy=all-unauthenticated
@@ -1876,9 +1878,8 @@ spec:
         imagePullPolicy: IfNotPresent
         livenessProbe:
           httpGet:
-            path: /ready
+            path: /live
             port: admin
-          initialDelaySeconds: 10
         name: policy
         ports:
         - containerPort: 8090
@@ -1892,6 +1893,7 @@ spec:
           httpGet:
             path: /ready
             port: admin
+          initialDelaySeconds: 10
         resources:
         securityContext:
           runAsUser: 2103
diff --git a/cli/cmd/testdata/install_helm_control_plane_output.golden b/cli/cmd/testdata/install_helm_control_plane_output.golden
index e41a5cfef..4a60df10d 100644
--- a/cli/cmd/testdata/install_helm_control_plane_output.golden
+++ b/cli/cmd/testdata/install_helm_control_plane_output.golden
@@ -1198,7 +1198,9 @@ spec:
         - --admin-addr=0.0.0.0:9990
         - --control-plane-namespace=linkerd-dev
         - --grpc-addr=0.0.0.0:8090
-        - --admission-addr=0.0.0.0:9443
+        - --server-addr=0.0.0.0:9443
+        - --server-tls-key=/var/run/linkerd/tls/tls.key
+        - --server-tls-certs=/var/run/linkerd/tls/tls.crt
         - --cluster-networks=10.0.0.0/8,100.64.0.0/10,172.16.0.0/12,192.168.0.0/16
         - --identity-domain=test.trust.domain
         - --default-policy=all-unauthenticated
@@ -1208,9 +1210,8 @@ spec:
         imagePullPolicy: IfNotPresent
         livenessProbe:
           httpGet:
-            path: /ready
+            path: /live
             port: admin
-          initialDelaySeconds: 10
         name: policy
         ports:
         - containerPort: 8090
@@ -1224,6 +1225,7 @@ spec:
           httpGet:
             path: /ready
             port: admin
+          initialDelaySeconds: 10
         resources:
         securityContext:
           runAsUser: 2103
diff --git a/cli/cmd/testdata/install_helm_control_plane_output_ha.golden b/cli/cmd/testdata/install_helm_control_plane_output_ha.golden
index 81fc25df7..92b3dfde8 100644
--- a/cli/cmd/testdata/install_helm_control_plane_output_ha.golden
+++ b/cli/cmd/testdata/install_helm_control_plane_output_ha.golden
@@ -1334,7 +1334,9 @@ spec:
         - --admin-addr=0.0.0.0:9990
         - --control-plane-namespace=linkerd-dev
         - --grpc-addr=0.0.0.0:8090
-        - --admission-addr=0.0.0.0:9443
+        - --server-addr=0.0.0.0:9443
+        - --server-tls-key=/var/run/linkerd/tls/tls.key
+        - --server-tls-certs=/var/run/linkerd/tls/tls.crt
         - --cluster-networks=10.0.0.0/8,100.64.0.0/10,172.16.0.0/12,192.168.0.0/16
         - --identity-domain=test.trust.domain
         - --default-policy=all-unauthenticated
@@ -1344,9 +1346,8 @@ spec:
         imagePullPolicy: IfNotPresent
         livenessProbe:
           httpGet:
-            path: /ready
+            path: /live
             port: admin
-          initialDelaySeconds: 10
         name: policy
         ports:
         - containerPort: 8090
@@ -1360,6 +1361,7 @@ spec:
           httpGet:
             path: /ready
             port: admin
+          initialDelaySeconds: 10
         resources:
         securityContext:
           runAsUser: 2103
diff --git a/cli/cmd/testdata/install_helm_output_ha_labels.golden b/cli/cmd/testdata/install_helm_output_ha_labels.golden
index 245b3265c..f5edfe7f7 100644
--- a/cli/cmd/testdata/install_helm_output_ha_labels.golden
+++ b/cli/cmd/testdata/install_helm_output_ha_labels.golden
@@ -1346,7 +1346,9 @@ spec:
         - --admin-addr=0.0.0.0:9990
         - --control-plane-namespace=linkerd-dev
         - --grpc-addr=0.0.0.0:8090
-        - --admission-addr=0.0.0.0:9443
+        - --server-addr=0.0.0.0:9443
+        - --server-tls-key=/var/run/linkerd/tls/tls.key
+        - --server-tls-certs=/var/run/linkerd/tls/tls.crt
         - --cluster-networks=10.0.0.0/8,100.64.0.0/10,172.16.0.0/12,192.168.0.0/16
         - --identity-domain=test.trust.domain
         - --default-policy=all-unauthenticated
@@ -1356,9 +1358,8 @@ spec:
         imagePullPolicy: IfNotPresent
         livenessProbe:
           httpGet:
-            path: /ready
+            path: /live
             port: admin
-          initialDelaySeconds: 10
         name: policy
         ports:
         - containerPort: 8090
@@ -1372,6 +1373,7 @@ spec:
           httpGet:
             path: /ready
             port: admin
+          initialDelaySeconds: 10
         resources:
         securityContext:
           runAsUser: 2103
diff --git a/cli/cmd/testdata/install_helm_output_ha_namespace_selector.golden b/cli/cmd/testdata/install_helm_output_ha_namespace_selector.golden
index 2e8b39721..df70e29cd 100644
--- a/cli/cmd/testdata/install_helm_output_ha_namespace_selector.golden
+++ b/cli/cmd/testdata/install_helm_output_ha_namespace_selector.golden
@@ -1324,7 +1324,9 @@ spec:
         - --admin-addr=0.0.0.0:9990
         - --control-plane-namespace=linkerd-dev
         - --grpc-addr=0.0.0.0:8090
-        - --admission-addr=0.0.0.0:9443
+        - --server-addr=0.0.0.0:9443
+        - --server-tls-key=/var/run/linkerd/tls/tls.key
+        - --server-tls-certs=/var/run/linkerd/tls/tls.crt
         - --cluster-networks=10.0.0.0/8,100.64.0.0/10,172.16.0.0/12,192.168.0.0/16
         - --identity-domain=test.trust.domain
         - --default-policy=all-unauthenticated
@@ -1334,9 +1336,8 @@ spec:
         imagePullPolicy: IfNotPresent
         livenessProbe:
           httpGet:
-            path: /ready
+            path: /live
             port: admin
-          initialDelaySeconds: 10
         name: policy
         ports:
         - containerPort: 8090
@@ -1350,6 +1351,7 @@ spec:
           httpGet:
             path: /ready
             port: admin
+          initialDelaySeconds: 10
         resources:
         securityContext:
           runAsUser: 2103
diff --git a/cli/cmd/testdata/install_no_init_container.golden b/cli/cmd/testdata/install_no_init_container.golden
index 6da6a5d56..8f98b8f36 100644
--- a/cli/cmd/testdata/install_no_init_container.golden
+++ b/cli/cmd/testdata/install_no_init_container.golden
@@ -1898,7 +1898,9 @@ spec:
         - --admin-addr=0.0.0.0:9990
         - --control-plane-namespace=linkerd
         - --grpc-addr=0.0.0.0:8090
-        - --admission-addr=0.0.0.0:9443
+        - --server-addr=0.0.0.0:9443
+        - --server-tls-key=/var/run/linkerd/tls/tls.key
+        - --server-tls-certs=/var/run/linkerd/tls/tls.crt
         - --cluster-networks=10.0.0.0/8,100.64.0.0/10,172.16.0.0/12,192.168.0.0/16
         - --identity-domain=cluster.local
         - --default-policy=all-unauthenticated
@@ -1908,9 +1910,8 @@ spec:
         imagePullPolicy: IfNotPresent
         livenessProbe:
           httpGet:
-            path: /ready
+            path: /live
             port: admin
-          initialDelaySeconds: 10
         name: policy
         ports:
         - containerPort: 8090
@@ -1924,6 +1925,7 @@ spec:
           httpGet:
             path: /ready
             port: admin
+          initialDelaySeconds: 10
         resources:
         securityContext:
           runAsUser: 2103
diff --git a/cli/cmd/testdata/install_output.golden b/cli/cmd/testdata/install_output.golden
index 99ce526f3..df8a60848 100644
--- a/cli/cmd/testdata/install_output.golden
+++ b/cli/cmd/testdata/install_output.golden
@@ -1937,7 +1937,9 @@ spec:
         - --admin-addr=0.0.0.0:9990
         - --control-plane-namespace=linkerd
         - --grpc-addr=0.0.0.0:8090
-        - --admission-addr=0.0.0.0:9443
+        - --server-addr=0.0.0.0:9443
+        - --server-tls-key=/var/run/linkerd/tls/tls.key
+        - --server-tls-certs=/var/run/linkerd/tls/tls.crt
         - --cluster-networks=ClusterNetworks
         - --identity-domain=cluster.local
         - --default-policy=default-allow-policy
@@ -1947,9 +1949,8 @@ spec:
         imagePullPolicy: ImagePullPolicy
         livenessProbe:
           httpGet:
-            path: /ready
+            path: /live
             port: admin
-          initialDelaySeconds: 10
         name: policy
         ports:
         - containerPort: 8090
@@ -1963,6 +1964,7 @@ spec:
           httpGet:
             path: /ready
             port: admin
+          initialDelaySeconds: 10
         resources:
           limits:
             cpu: "cpu-limit"
diff --git a/cli/cmd/testdata/install_proxy_ignores.golden b/cli/cmd/testdata/install_proxy_ignores.golden
index e0d54d34d..ecff71681 100644
--- a/cli/cmd/testdata/install_proxy_ignores.golden
+++ b/cli/cmd/testdata/install_proxy_ignores.golden
@@ -1935,7 +1935,9 @@ spec:
         - --admin-addr=0.0.0.0:9990
         - --control-plane-namespace=linkerd
         - --grpc-addr=0.0.0.0:8090
-        - --admission-addr=0.0.0.0:9443
+        - --server-addr=0.0.0.0:9443
+        - --server-tls-key=/var/run/linkerd/tls/tls.key
+        - --server-tls-certs=/var/run/linkerd/tls/tls.crt
         - --cluster-networks=10.0.0.0/8,100.64.0.0/10,172.16.0.0/12,192.168.0.0/16
         - --identity-domain=cluster.local
         - --default-policy=all-unauthenticated
@@ -1945,9 +1947,8 @@ spec:
         imagePullPolicy: IfNotPresent
         livenessProbe:
           httpGet:
-            path: /ready
+            path: /live
             port: admin
-          initialDelaySeconds: 10
         name: policy
         ports:
         - containerPort: 8090
@@ -1961,6 +1962,7 @@ spec:
           httpGet:
             path: /ready
             port: admin
+          initialDelaySeconds: 10
         resources:
         securityContext:
           runAsUser: 2103
diff --git a/cli/cmd/testdata/install_values_file.golden b/cli/cmd/testdata/install_values_file.golden
index badc5f06c..50244b7d7 100644
--- a/cli/cmd/testdata/install_values_file.golden
+++ b/cli/cmd/testdata/install_values_file.golden
@@ -1935,7 +1935,9 @@ spec:
         - --admin-addr=0.0.0.0:9990
         - --control-plane-namespace=linkerd
         - --grpc-addr=0.0.0.0:8090
-        - --admission-addr=0.0.0.0:9443
+        - --server-addr=0.0.0.0:9443
+        - --server-tls-key=/var/run/linkerd/tls/tls.key
+        - --server-tls-certs=/var/run/linkerd/tls/tls.crt
         - --cluster-networks=10.0.0.0/8,100.64.0.0/10,172.16.0.0/12,192.168.0.0/16
         - --identity-domain=example.com
         - --default-policy=all-unauthenticated
@@ -1945,9 +1947,8 @@ spec:
         imagePullPolicy: IfNotPresent
         livenessProbe:
           httpGet:
-            path: /ready
+            path: /live
             port: admin
-          initialDelaySeconds: 10
         name: policy
         ports:
         - containerPort: 8090
@@ -1961,6 +1962,7 @@ spec:
           httpGet:
             path: /ready
             port: admin
+          initialDelaySeconds: 10
         resources:
         securityContext:
           runAsUser: 2103
diff --git a/cni-plugin/test/install-cni_test.go b/cni-plugin/test/install-cni_test.go
index 94fb68265..fe4ec00cf 100644
--- a/cni-plugin/test/install-cni_test.go
+++ b/cni-plugin/test/install-cni_test.go
@@ -95,7 +95,7 @@ func cp(src, dest string, t *testing.T) {
 		testutil.AnnotatedFatalf(t, fmt.Sprintf("failed to read file %v", src),
 			"failed to read file %v: %v", src, err)
 	}
-	if err = ioutil.WriteFile(dest, data, 0644); err != nil {
+	if err = ioutil.WriteFile(dest, data, 0600); err != nil {
 		testutil.AnnotatedFatalf(t, fmt.Sprintf("failed to write file %v", dest),
 			"failed to write file %v: %v", dest, err)
 	}
diff --git a/policy-controller/k8s/index/src/tests.rs b/policy-controller/k8s/index/src/tests.rs
index e9d1620cc..93451701b 100644
--- a/policy-controller/k8s/index/src/tests.rs
+++ b/policy-controller/k8s/index/src/tests.rs
@@ -37,7 +37,7 @@ async fn incrementally_configure_server() {
         pod_net.hosts().next().unwrap(),
         Some(("container-0", vec![2222, 9999])),
     );
-    idx.apply_pod(pod.clone()).unwrap();
+    idx.apply_pod(pod.clone());
 
     let default = DefaultPolicy::Allow {
         authenticated_only: false,
@@ -94,7 +94,7 @@ async fn incrementally_configure_server() {
             ..Default::default()
         },
     );
-    idx.apply_authz(authz.clone()).unwrap();
+    idx.apply_serverauthorization(authz.clone());
 
     // Check that the watch now has authorized traffic as described above.
     let mut rx = port2222.into_stream();
@@ -116,21 +116,21 @@ async fn incrementally_configure_server() {
     );
 
     // Delete the authorization and check that the watch has reverted to its prior state.
-    idx.delete_authz(authz);
+    idx.delete_serverauthorization(authz);
     assert_eq!(
         time::timeout(time::Duration::from_secs(1), rx.next()).await,
         Ok(Some(basic_config)),
     );
 
     // Delete the server and check that the watch has reverted the default state.
-    idx.delete_server(srv).unwrap();
+    idx.delete_server(srv);
     assert_eq!(
         time::timeout(time::Duration::from_secs(1), rx.next()).await,
         Ok(Some(default_config))
     );
 
     // Delete the pod and check that the watch recognizes that the watch has been closed.
-    idx.delete_pod(pod).unwrap();
+    idx.delete_pod(pod);
     assert_eq!(
         time::timeout(time::Duration::from_secs(1), rx.next()).await,
         Ok(None)
@@ -160,7 +160,7 @@ fn server_update_deselects_pod() {
         pod_net.hosts().next().unwrap(),
         Some(("container-0", vec![2222])),
     );
-    idx.apply_pod(p).unwrap();
+    idx.apply_pod(p);
 
     let srv = {
         let mut srv = mk_server("ns-0", "srv-0", Port::Number(2222), None, None);
@@ -221,7 +221,7 @@ fn default_policy_global() {
             pod_net.hosts().next().unwrap(),
             Some(("container-0", vec![2222])),
         );
-        idx.reset_pods(vec![p]).unwrap();
+        idx.reset_pods(vec![p]);
 
         let config = InboundServer {
             name: format!("default:{}", default),
@@ -277,7 +277,7 @@ fn default_policy_annotated() {
         );
         p.annotations_mut()
             .insert(DefaultPolicy::ANNOTATION.into(), default.to_string());
-        idx.reset_pods(vec![p]).unwrap();
+        idx.reset_pods(vec![p]);
 
         let config = InboundServer {
             name: format!("default:{}", default),
@@ -320,7 +320,7 @@ fn default_policy_annotated_invalid() {
     );
     p.annotations_mut()
         .insert(DefaultPolicy::ANNOTATION.into(), "bogus".into());
-    idx.reset_pods(vec![p]).unwrap();
+    idx.reset_pods(vec![p]);
 
     // Lookup port 2222 -> default config.
     let port2222 = lookup_rx
@@ -367,7 +367,7 @@ fn opaque_annotated() {
         );
         p.annotations_mut()
             .insert("config.linkerd.io/opaque-ports".into(), "2222".into());
-        idx.reset_pods(vec![p]).unwrap();
+        idx.reset_pods(vec![p]);
 
         let config = InboundServer {
             name: format!("default:{}", default),
@@ -407,7 +407,7 @@ fn authenticated_annotated() {
             "config.linkerd.io/proxy-require-identity-inbound-ports".into(),
             "2222".into(),
         );
-        idx.reset_pods(vec![p]).unwrap();
+        idx.reset_pods(vec![p]);
 
         let config = {
             let policy = match *default {
diff --git a/test/integration/viz/tracing/tracing_test.go b/test/integration/viz/tracing/tracing_test.go
index 366ad25b8..9c40010ae 100644
--- a/test/integration/viz/tracing/tracing_test.go
+++ b/test/integration/viz/tracing/tracing_test.go
@@ -147,6 +147,7 @@ func TestTracing(t *testing.T) {
 				testutil.AnnotatedWarn(t, "Failed to get pods", err)
 			}
 			for _, pod := range pods {
+				pod := pod
 				if !labels.IsTracingEnabled(&pod) {
 					testutil.AnnotatedWarn(t, "Tracing annotation not found on pod", pod.Namespace, pod.Name)
 					// XXX This test is super duper flakey, so for now we ignore failures when the
diff --git a/testutil/test_data_diff.go b/testutil/test_data_diff.go
index ec3be8c6b..a60eab53a 100644
--- a/testutil/test_data_diff.go
+++ b/testutil/test_data_diff.go
@@ -59,14 +59,14 @@ func ReadTestdata(t *testing.T, fileName string) string {
 
 func writeTestdata(t *testing.T, fileName string, data []byte) {
 	p := filepath.Join("testdata", fileName)
-	if err := ioutil.WriteFile(p, data, 0644); err != nil {
+	if err := ioutil.WriteFile(p, data, 0600); err != nil {
 		t.Fatal(err)
 	}
 }
 
 func writeRejects(t *testing.T, origFileName string, data []byte, rejectPath string) {
 	p := filepath.Join(rejectPath, origFileName+".rej")
-	if err := ioutil.WriteFile(p, data, 0644); err != nil {
+	if err := ioutil.WriteFile(p, data, 0600); err != nil {
 		t.Fatal(err)
 	}
 }
diff --git a/testutil/test_helper.go b/testutil/test_helper.go
index 01c5d79fd..49fd45d6e 100644
--- a/testutil/test_helper.go
+++ b/testutil/test_helper.go
@@ -706,10 +706,12 @@ func (h *TestHelper) DownloadCLIBinary(filepath, version string) error {
 	defer resp.Body.Close()
 
 	// Create if it doesn't already exist
-	out, err := os.OpenFile(filepath, os.O_RDWR|os.O_CREATE, 755)
+	out, err := os.OpenFile(filepath, os.O_RDWR|os.O_CREATE, 0555)
 	if err != nil {
 		return err
 	}
+	// This is a test and we don't care about errors that can occur here.
+	//nolint:gosec
 	defer out.Close()
 
 	_, err = io.Copy(out, resp.Body)
diff --git a/viz/tap/api/grpc_server_test.go b/viz/tap/api/grpc_server_test.go
index e8099b759..a7120968f 100644
--- a/viz/tap/api/grpc_server_test.go
+++ b/viz/tap/api/grpc_server_test.go
@@ -396,7 +396,7 @@ status:
 			}
 			proxy.RegisterTapServer(s, &mockProxyTapServer)
 
-			lis, err := net.Listen("tcp", ":0")
+			lis, err := net.Listen("tcp", "localhost:0")
 			if err != nil {
 				t.Fatalf("Failed to listen")
 			}
@@ -664,7 +664,7 @@ status:
 			if err != nil {
 				t.Fatalf("NewFakeAPI returned an error: %s", err)
 			}
-			s := NewGrpcTapServer(4190, "controller-ns", "cluster.local", k8sAPI)
+			s, _ := NewGrpcTapServer(4190, "controller-ns", "cluster.local", k8sAPI)
 			k8sAPI.Sync(nil)
 
 			labels := make(map[string]string)