├── .dockerignore ├── .github └── workflows │ ├── daily-scanning.yaml │ ├── on-commit.yaml │ ├── release-publish-to-ghcr.yaml │ └── testing-PRs.yml ├── .gitignore ├── Dockerfile-alpine ├── Dockerfile-distroless ├── LICENSE ├── README.md ├── SECURITY.md ├── cmd └── kube-audit-rest │ └── main.go ├── docs └── ForDevelopers.md ├── examples ├── full-elastic-stack │ ├── README.md │ ├── certs │ │ ├── ca.cnf │ │ ├── certs.sh │ │ └── server.csr.cnf │ └── k8s │ │ ├── ValidatingWebhookConfiguration.yaml │ │ ├── elastic-cluster.yaml │ │ └── kube-audit-rest.yaml ├── live-demo │ └── demo.sh └── seeing-audit-log-locally │ └── README.md ├── go.mod ├── go.sum ├── internal ├── audit_writer │ ├── common_writer │ │ └── common_writer.go │ ├── disk_writer │ │ ├── disk_writer.go │ │ └── disk_writer_test.go │ ├── iaudit_writer.go │ └── stderr_writer │ │ ├── stderr_writer.go │ │ └── stderr_writer_test.go ├── common │ ├── logger.go │ └── logger_test.go ├── event_processor │ ├── event_processor_impl │ │ ├── event_processor_impl.go │ │ └── event_processor_impl_test.go │ └── ievent_processor.go ├── http_listener │ ├── ihttp_listener.go │ └── log_request_listener │ │ ├── log_request_listener.go │ │ └── log_request_listener_test.go └── metrics │ ├── imetrics.go │ └── prometheus_metrics │ ├── prometheus_metrics.go │ └── prometheus_metrics_test.go ├── k8s ├── deployment.yaml ├── namespace.yaml ├── service.yaml └── webhook.yaml ├── mocks ├── audit_writer_mock.go ├── event_processor_mock.go ├── http_listener_mock.go └── metrics_mock.go ├── renovate.json └── testing ├── ca.cnf ├── certs.sh ├── cleanup.sh ├── locally ├── data │ ├── kube-audit-rest-sorted.log │ └── kube-audit-rest.log ├── local-testing.sh └── main.go ├── server.csr.cnf └── setup.sh /.dockerignore: -------------------------------------------------------------------------------- 1 | # Ignore files not built in docker image 2 | kube-audit-rest 3 | 4 | # Ignore git directories 5 | .git 6 | .gitignore 7 | 8 | # Ignore github directories 9 | .github 10 | 11 | # Ignore tmp so certs/etc not leaked 12 | tmp/ 13 | 14 | # Ignore circleci 15 | .circleci 16 | 17 | # Docker 18 | Dockerfile* 19 | 20 | # various documentation 21 | examples/ 22 | docs/ -------------------------------------------------------------------------------- /.github/workflows/daily-scanning.yaml: -------------------------------------------------------------------------------- 1 | name: Daily vulnerability scan 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: "43 14 * * *" 7 | 8 | jobs: 9 | build-and-publish-result-to-ghcr: 10 | # Explicitly grant the `secrets.GITHUB_TOKEN` permissions. 11 | permissions: 12 | packages: read 13 | # Needed to upload the code scanning results to code-scanning dashboard. 14 | security-events: write 15 | name: Build container images 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout main 19 | uses: actions/checkout@v4.2.2 20 | with: 21 | fetch-depth: 0 22 | - name: Detect latest release 23 | id: generate-container-name 24 | run: | 25 | echo "latest_version=ghcr.io/richardoc/kube-audit-rest:$(git tag --sort=-version:refname | head -n1)" >> "$GITHUB_OUTPUT" 26 | 27 | - name: Set up Docker Buildx 28 | uses: docker/setup-buildx-action@v3 29 | 30 | - name: Login to GitHub Container Registry 31 | uses: docker/login-action@v3 32 | with: 33 | registry: ghcr.io 34 | # This is the user that triggered the Workflow. In this case, it will 35 | # either be the user whom created the Release or manually triggered 36 | # the workflow_dispatch. 37 | username: ${{ github.actor }} 38 | # `secrets.GITHUB_TOKEN` is a secret that's automatically generated by 39 | # GitHub Actions at the start of a workflow run to identify the job. 40 | # This is used to authenticate against GitHub Container Registry. 41 | # See https://docs.github.com/en/actions/security-guides/automatic-token-authentication#about-the-github_token-secret 42 | # for more detailed information. 43 | password: ${{ secrets.GITHUB_TOKEN }} 44 | 45 | - name: Run Trivy vulnerability scanner for distroless container 46 | uses: aquasecurity/trivy-action@0.30.0 47 | with: 48 | image-ref: '${{ steps.generate-container-name.outputs.latest_version }}-distroless' 49 | scan-ref: "daily scan - ${{ steps.generate-container-name.outputs.latest_version }}-distroless" 50 | format: 'sarif' 51 | output: 'distroless-results.sarif' 52 | github-pat: '${{ secrets.GITHUB_TOKEN }}' 53 | - name: Upload Trivy distroless scan results to GitHub Security tab 54 | uses: github/codeql-action/upload-sarif@v3.28.18 55 | with: 56 | sarif_file: 'distroless-results.sarif' 57 | category: 'daily-trivy-distroless-AMD64-release' 58 | - name: Run Trivy vulnerability scanner for alpine container 59 | uses: aquasecurity/trivy-action@0.30.0 60 | with: 61 | image-ref: '${{ steps.generate-container-name.outputs.latest_version }}-alpine' 62 | scan-ref: "daily scan - ${{ steps.generate-container-name.outputs.latest_version }}-alpine" 63 | format: 'sarif' 64 | output: 'alpine-results.sarif' 65 | github-pat: '${{ secrets.GITHUB_TOKEN }}' 66 | - name: Upload Trivy alpine scan results to GitHub Security tab 67 | uses: github/codeql-action/upload-sarif@v3.28.18 68 | with: 69 | sarif_file: 'alpine-results.sarif' 70 | category: 'daily-trivy-alpine-AMD64-on-latest-release' 71 | static-scan-with-trivy: 72 | # Explicitly grant the `secrets.GITHUB_TOKEN` permissions. 73 | permissions: 74 | packages: read 75 | # Needed to upload the code scanning results to code-scanning dashboard. 76 | security-events: write 77 | # Needed to upload dependency graph 78 | contents: write 79 | 80 | name: Run trivy on the repo 81 | runs-on: ubuntu-latest 82 | steps: 83 | - name: Checkout main 84 | uses: actions/checkout@v4.2.2 85 | with: 86 | fetch-depth: 0 87 | - name: Run Trivy vulnerability scanner in fs mode 88 | uses: aquasecurity/trivy-action@master 89 | with: 90 | scan-type: 'fs' 91 | scan-ref: '.' 92 | format: 'sarif' 93 | output: 'repo-results.sarif' 94 | github-pat: '${{ secrets.GITHUB_TOKEN }}' 95 | - name: Upload Trivy repo scan results to GitHub Security tab 96 | uses: github/codeql-action/upload-sarif@v3.28.18 97 | with: 98 | sarif_file: 'repo-results.sarif' 99 | category: 'daily-trivy-repo' 100 | 101 | - name: Run Trivy in GitHub SBOM mode and submit results to Dependency Graph 102 | uses: aquasecurity/trivy-action@master 103 | with: 104 | scan-type: 'fs' 105 | format: 'github' 106 | output: 'dependency-results.sbom.json' 107 | image-ref: '.' 108 | github-pat: ${{ secrets.GITHUB_TOKEN }} 109 | -------------------------------------------------------------------------------- /.github/workflows/on-commit.yaml: -------------------------------------------------------------------------------- 1 | name: Run per commit workflows 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'main' 7 | 8 | jobs: 9 | AMD64-build-and-publish-to-ghcr: 10 | # Explicitly grant the `secrets.GITHUB_TOKEN` permissions. 11 | permissions: 12 | # Grant the ability to write to GitHub Packages (push Docker images to 13 | # GitHub Container Registry). 14 | packages: write 15 | # Needed to upload the code scanning results to code-scanning dashboard. 16 | security-events: write 17 | # For uploading attestations 18 | attestations: write 19 | # For OICD? https://github.com/actions/deploy-pages/issues/329 20 | id-token: write 21 | 22 | name: Build and publish AMD64 and ARM64 container images 📦 to GitHub Container Registry 23 | runs-on: ubuntu-latest 24 | steps: 25 | - name: Checkout main 26 | uses: actions/checkout@v4.2.2 27 | with: 28 | fetch-depth: 1 29 | fetch-tags: true 30 | - name: Extract metadata (tags, labels) for Docker 31 | id: meta 32 | uses: docker/metadata-action@v5.7.0 33 | with: 34 | images: ghcr.io/${{ github.repository }} 35 | - name: Set up QEMU 36 | uses: docker/setup-qemu-action@v3.6.0 37 | with: 38 | platforms: 'arm64' 39 | - name: Set up Docker Buildx 40 | uses: docker/setup-buildx-action@v3.10.0 41 | - name: Login to GitHub Container Registry 42 | uses: docker/login-action@v3.4.0 43 | with: 44 | registry: ghcr.io 45 | # This is the user that triggered the Workflow. In this case, it will 46 | # either be the user whom created the Release or manually triggered 47 | # the workflow_dispatch. 48 | username: ${{ github.actor }} 49 | # `secrets.GITHUB_TOKEN` is a secret that's automatically generated by 50 | # GitHub Actions at the start of a workflow run to identify the job. 51 | # This is used to authenticate against GitHub Container Registry. 52 | # See https://docs.github.com/en/actions/security-guides/automatic-token-authentication#about-the-github_token-secret 53 | # for more detailed information. 54 | password: ${{ secrets.GITHUB_TOKEN }} 55 | 56 | - name: Build and push alpine based image 57 | id: alpine-image 58 | uses: docker/build-push-action@v6.18.0 59 | with: 60 | file: Dockerfile-alpine 61 | context: . 62 | push: true # push the image to ghcr 63 | tags: | 64 | ghcr.io/richardoc/kube-audit-rest:${{github.sha}}-alpine 65 | labels: ${{ steps.meta.outputs.labels }} 66 | platforms: linux/amd64,linux/arm64 67 | cache-from: type=gha 68 | cache-to: type=gha,mode=max 69 | provenance: true 70 | sbom: true 71 | - name: Attest alpine based image 72 | id: attest-alpine 73 | uses: actions/attest-build-provenance@v2 74 | with: 75 | subject-name: ghcr.io/richardoc/kube-audit-rest 76 | subject-digest: ${{ steps.alpine-image.outputs.digest }} 77 | push-to-registry: true 78 | - name: Build and push distroless image 79 | id: distroless-image 80 | uses: docker/build-push-action@v6.18.0 81 | with: 82 | file: Dockerfile-distroless 83 | context: . 84 | push: true # push the image to ghcr 85 | tags: | 86 | ghcr.io/richardoc/kube-audit-rest:${{github.sha}}-distroless 87 | labels: ${{ steps.meta.outputs.labels }} 88 | platforms: linux/amd64,linux/arm64 89 | cache-from: type=gha 90 | cache-to: type=gha,mode=max 91 | provenance: true 92 | sbom: true 93 | - name: Attest distroless based image 94 | id: attest-distroless 95 | uses: actions/attest-build-provenance@v2 96 | with: 97 | subject-name: ghcr.io/richardoc/kube-audit-rest 98 | subject-digest: ${{ steps.distroless-image.outputs.digest }} 99 | push-to-registry: true 100 | - name: Run Trivy vulnerability scanner for distroless AMD64 container 101 | uses: aquasecurity/trivy-action@0.30.0 102 | with: 103 | image-ref: 'ghcr.io/richardoc/kube-audit-rest:${{github.sha}}-distroless' 104 | scan-ref: "ghcr.io/richardoc/kube-audit-rest:${{github.sha}}-distroless" 105 | format: 'sarif' 106 | output: 'distroless-results.sarif' 107 | github-pat: '${{ secrets.GITHUB_TOKEN }}' 108 | - name: Upload Trivy distroless AMD64 scan results to GitHub Security tab 109 | uses: github/codeql-action/upload-sarif@v3.28.18 110 | with: 111 | sarif_file: 'distroless-results.sarif' 112 | category: 'trivy-distroless-AMD64' 113 | - name: Run Trivy vulnerability scanner for alpine container AMD64 114 | uses: aquasecurity/trivy-action@0.30.0 115 | with: 116 | image-ref: 'ghcr.io/richardoc/kube-audit-rest:${{github.sha}}-alpine' 117 | scan-ref: "ghcr.io/richardoc/kube-audit-rest:${{github.sha}}-alpine" 118 | format: 'sarif' 119 | output: 'alpine-results.sarif' 120 | github-pat: '${{ secrets.GITHUB_TOKEN }}' 121 | - name: Upload Trivy alpine AMD64 scan results to GitHub Security tab 122 | uses: github/codeql-action/upload-sarif@v3.28.18 123 | with: 124 | sarif_file: 'alpine-results.sarif' 125 | category: 'trivy-alpine-AMD64-per-commit' 126 | 127 | static-scan-with-trivy: 128 | # Explicitly grant the `secrets.GITHUB_TOKEN` permissions. 129 | permissions: 130 | # Grant the ability to write to GitHub Packages (push Docker images to 131 | # GitHub Container Registry). 132 | packages: write 133 | # Needed to upload the code scanning results to code-scanning dashboard. 134 | security-events: write 135 | # Needed to upload dependency graph 136 | contents: write 137 | 138 | name: Run trivy on the repo 139 | runs-on: ubuntu-latest 140 | steps: 141 | - name: Checkout main 142 | uses: actions/checkout@v4.2.2 143 | - name: Run Trivy vulnerability scanner in fs mode 144 | uses: aquasecurity/trivy-action@master 145 | with: 146 | scan-type: 'fs' 147 | scan-ref: '.' 148 | format: 'sarif' 149 | output: 'repo-results.sarif' 150 | github-pat: '${{ secrets.GITHUB_TOKEN }}' 151 | - name: Upload Trivy repo scan results to GitHub Security tab 152 | uses: github/codeql-action/upload-sarif@v3.28.18 153 | with: 154 | sarif_file: 'repo-results.sarif' 155 | category: 'trivy-repo' 156 | 157 | - name: Run Trivy in GitHub SBOM mode and submit results to Dependency Graph 158 | uses: aquasecurity/trivy-action@master 159 | with: 160 | scan-type: 'fs' 161 | format: 'github' 162 | output: 'dependency-results.sbom.json' 163 | image-ref: '.' 164 | github-pat: ${{ secrets.GITHUB_TOKEN }} 165 | -------------------------------------------------------------------------------- /.github/workflows/release-publish-to-ghcr.yaml: -------------------------------------------------------------------------------- 1 | name: Publish release's container images 📦 to GitHub Container Registry 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | build-and-publish-to-ghcr: 9 | # Explicitly grant the `secrets.GITHUB_TOKEN` permissions. 10 | permissions: 11 | # Grant the ability to write to GitHub Packages (push Docker images to 12 | # GitHub Container Registry). 13 | packages: write 14 | # Needed to upload the code scanning results to code-scanning dashboard. 15 | security-events: write 16 | # For uploading attestations 17 | attestations: write 18 | # For OICD? https://github.com/actions/deploy-pages/issues/329 19 | id-token: write 20 | name: Build and publish container images 📦 to GitHub Container Registry 21 | runs-on: ubuntu-latest 22 | steps: 23 | - name: Checkout main 24 | uses: actions/checkout@v4.2.2 25 | with: 26 | fetch-depth: 0 27 | fetch-tags: true 28 | - name: Extract metadata (tags, labels) for Docker 29 | id: meta 30 | uses: docker/metadata-action@v5.7.0 31 | with: 32 | images: ghcr.io/${{ github.repository }} 33 | - name: Set up QEMU 34 | uses: docker/setup-qemu-action@v3.6.0 35 | with: 36 | platforms: "arm64" 37 | - name: Set up Docker Buildx 38 | uses: docker/setup-buildx-action@v3 39 | - name: Login to GitHub Container Registry 40 | uses: docker/login-action@v3.4.0 41 | with: 42 | registry: ghcr.io 43 | # This is the user that triggered the Workflow. In this case, it will 44 | # either be the user whom created the Release or manually triggered 45 | # the workflow_dispatch. 46 | username: ${{ github.actor }} 47 | # `secrets.GITHUB_TOKEN` is a secret that's automatically generated by 48 | # GitHub Actions at the start of a workflow run to identify the job. 49 | # This is used to authenticate against GitHub Container Registry. 50 | # See https://docs.github.com/en/actions/security-guides/automatic-token-authentication#about-the-github_token-secret 51 | # for more detailed information. 52 | password: ${{ secrets.GITHUB_TOKEN }} 53 | 54 | - name: Build and push alpine based image 55 | id: alpine-image 56 | uses: docker/build-push-action@v6.18.0 57 | with: 58 | file: Dockerfile-alpine 59 | context: . 60 | push: true # push the image to ghcr 61 | tags: | 62 | ghcr.io/richardoc/kube-audit-rest:${{github.ref_name}}-alpine 63 | labels: ${{ steps.meta.outputs.labels }} 64 | platforms: linux/amd64,linux/arm64 65 | cache-from: type=gha 66 | cache-to: type=gha,mode=max 67 | provenance: true 68 | sbom: true 69 | - name: Attest alpine based image 70 | id: attest-alpine 71 | uses: actions/attest-build-provenance@v2 72 | with: 73 | subject-name: ghcr.io/richardoc/kube-audit-rest 74 | subject-digest: ${{ steps.alpine-image.outputs.digest }} 75 | push-to-registry: true 76 | - name: Build and push distroless image 77 | id: distroless-image 78 | uses: docker/build-push-action@v6 79 | with: 80 | file: Dockerfile-distroless 81 | context: . 82 | push: true # push the image to ghcr 83 | tags: | 84 | ghcr.io/richardoc/kube-audit-rest:${{github.ref_name}}-distroless 85 | ghcr.io/richardoc/kube-audit-rest:latest 86 | labels: ${{ steps.meta.outputs.labels }} 87 | platforms: linux/amd64,linux/arm64 88 | cache-from: type=gha 89 | cache-to: type=gha,mode=max 90 | provenance: true 91 | sbom: true 92 | - name: Attest distroless based image 93 | id: attest-distroless 94 | uses: actions/attest-build-provenance@v2 95 | with: 96 | subject-name: ghcr.io/richardoc/kube-audit-rest 97 | subject-digest: ${{ steps.distroless-image.outputs.digest }} 98 | push-to-registry: true 99 | - name: Run Trivy vulnerability scanner for distroless container 100 | uses: aquasecurity/trivy-action@0.30.0 101 | with: 102 | image-ref: "ghcr.io/richardoc/kube-audit-rest:${{github.ref_name}}-distroless" 103 | scan-ref: "ghcr.io/richardoc/kube-audit-rest:${{github.ref_name}}-distroless" 104 | format: "sarif" 105 | output: "distroless-results.sarif" 106 | github-pat: "${{ secrets.GITHUB_TOKEN }}" 107 | - name: Upload Trivy distroless scan results to GitHub Security tab 108 | uses: github/codeql-action/upload-sarif@v3.28.18 109 | with: 110 | sarif_file: "distroless-results.sarif" 111 | category: "trivy-distroless-AMD64-release" 112 | - name: Run Trivy vulnerability scanner for alpine container 113 | uses: aquasecurity/trivy-action@0.30.0 114 | with: 115 | image-ref: "ghcr.io/richardoc/kube-audit-rest:${{github.ref_name}}-alpine" 116 | scan-ref: "ghcr.io/richardoc/kube-audit-rest:${{github.ref_name}}-alpine" 117 | format: "sarif" 118 | output: "alpine-results.sarif" 119 | github-pat: "${{ secrets.GITHUB_TOKEN }}" 120 | - name: Upload Trivy alpine scan results to GitHub Security tab 121 | uses: github/codeql-action/upload-sarif@v3.28.18 122 | with: 123 | sarif_file: "alpine-results.sarif" 124 | category: "trivy-alpine-AMD64-on-release" 125 | static-scan-with-trivy: 126 | # Explicitly grant the `secrets.GITHUB_TOKEN` permissions. 127 | permissions: 128 | # Grant the ability to write to GitHub Packages (push Docker images to 129 | # GitHub Container Registry). 130 | packages: write 131 | # Needed to upload the code scanning results to code-scanning dashboard. 132 | security-events: write 133 | # Needed to upload dependency graph 134 | contents: write 135 | 136 | name: Run trivy on the repo 137 | runs-on: ubuntu-latest 138 | steps: 139 | - name: Checkout main 140 | uses: actions/checkout@v4.2.2 141 | with: 142 | fetch-depth: 0 143 | - name: Run Trivy vulnerability scanner in fs mode 144 | uses: aquasecurity/trivy-action@master 145 | with: 146 | scan-type: "fs" 147 | scan-ref: "." 148 | format: "sarif" 149 | output: "repo-results.sarif" 150 | github-pat: "${{ secrets.GITHUB_TOKEN }}" 151 | - name: Upload Trivy repo scan results to GitHub Security tab 152 | uses: github/codeql-action/upload-sarif@v3.28.18 153 | with: 154 | sarif_file: "repo-results.sarif" 155 | category: "trivy-repo-release" 156 | 157 | - name: Run Trivy in GitHub SBOM mode and submit results to Dependency Graph 158 | uses: aquasecurity/trivy-action@master 159 | with: 160 | scan-type: "fs" 161 | format: "github" 162 | output: "dependency-results.sbom.json" 163 | image-ref: "." 164 | github-pat: ${{ secrets.GITHUB_TOKEN }} 165 | -------------------------------------------------------------------------------- /.github/workflows/testing-PRs.yml: -------------------------------------------------------------------------------- 1 | name: Test PRs 2 | on: 3 | pull_request: 4 | types: 5 | - labeled 6 | - opened 7 | - edited 8 | - reopened 9 | - synchronize 10 | - ready_for_review 11 | 12 | # Explicitly grant the `secrets.GITHUB_TOKEN` no permissions. 13 | permissions: {} 14 | jobs: 15 | AMD-test-via-docker: 16 | name: AMD64 - Build and test via Docker 🐳 images 📦 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Setup Action 20 | uses: actions/checkout@v4.2.2 21 | - name: Set up Docker Buildx 22 | uses: docker/setup-buildx-action@v3.10.0 23 | - name: Build alpine based image 24 | uses: docker/build-push-action@v6.18.0 25 | with: 26 | file: Dockerfile-alpine 27 | context: . 28 | push: false # DO NOT PUSH AS UNTRUSTED 29 | tags: | 30 | ghcr.io/richardoc/kube-audit-rest:test-alpine 31 | platforms: linux/amd64 32 | cache-from: type=gha 33 | cache-to: type=gha,mode=max 34 | - name: Build distroless image 35 | uses: docker/build-push-action@v6.18.0 36 | with: 37 | file: Dockerfile-distroless 38 | context: . 39 | push: false # DO NOT PUSH AS UNTRUSTED 40 | tags: | 41 | ghcr.io/richardoc/kube-audit-rest:test-distroless 42 | ghcr.io/richardoc/kube-audit-rest:latest 43 | platforms: linux/amd64 44 | cache-from: type=gha 45 | cache-to: type=gha,mode=max 46 | ARM-test-via-docker: 47 | name: ARM64 - Build and test via Docker 🐳 images 📦 48 | runs-on: ubuntu-24.04-arm 49 | steps: 50 | - name: Setup Action 51 | uses: actions/checkout@v4.2.2 52 | - name: Set up Docker Buildx 53 | uses: docker/setup-buildx-action@v3.10.0 54 | - name: Build alpine based image 55 | uses: docker/build-push-action@v6.18.0 56 | with: 57 | file: Dockerfile-alpine 58 | context: . 59 | push: false # DO NOT PUSH AS UNTRUSTED 60 | tags: | 61 | ghcr.io/richardoc/kube-audit-rest:test-alpine 62 | platforms: linux/arm64 63 | cache-from: type=gha 64 | cache-to: type=gha,mode=max 65 | - name: Build distroless image 66 | uses: docker/build-push-action@v6.18.0 67 | with: 68 | file: Dockerfile-distroless 69 | context: . 70 | push: false # DO NOT PUSH AS UNTRUSTED 71 | tags: | 72 | ghcr.io/richardoc/kube-audit-rest:test-distroless 73 | ghcr.io/richardoc/kube-audit-rest:latest 74 | platforms: linux/arm64 75 | cache-from: type=gha 76 | cache-to: type=gha,mode=max 77 | execute-unittests: 78 | name: Execute the unittests 79 | runs-on: ubuntu-latest 80 | steps: 81 | - name: Setup Action 82 | uses: actions/checkout@v4.2.2 83 | 84 | - name: Set up Go 85 | uses: actions/setup-go@v5.5.0 86 | with: 87 | go-version: 1.22 88 | 89 | - name: Build 90 | run: go build -v ./... 91 | 92 | - name: Test 93 | run: go test -v ./... 94 | run-trivy: 95 | name: Run trivy 96 | runs-on: ubuntu-latest 97 | steps: 98 | - name: Setup Action 99 | uses: actions/checkout@v4.2.2 100 | - name: Run Trivy vulnerability scanner in fs mode 101 | uses: aquasecurity/trivy-action@master 102 | with: 103 | scan-type: "fs" 104 | scan-ref: "." 105 | format: "sarif" 106 | output: "repo-results.sarif" 107 | exit-code: "1" 108 | severity: "CRITICAL,HIGH" 109 | run-semgrep: 110 | name: Run Semgrep 111 | runs-on: ubuntu-latest 112 | steps: 113 | - name: Setup Action 114 | uses: actions/checkout@v4.2.2 115 | - name: Set up Docker Buildx 116 | uses: docker/setup-buildx-action@v3.10.0 117 | - name: Run semgrep 118 | # excluding yamls due to false positives with the elasticsearch example for now 119 | run: docker run --rm -v "${PWD}:/src" returntocorp/semgrep:1.48.0-nonroot@sha256:572b06425becea5b9b26bcd01f78553383ab052debfeb2c57720cebd6999d964 semgrep ci --config auto --exclude=*.yaml 120 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | tmp/* 2 | kube-audit-rest 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /Dockerfile-alpine: -------------------------------------------------------------------------------- 1 | FROM golang:1.24.3-alpine3.21@sha256:ef18ee7117463ac1055f5a370ed18b8750f01589f13ea0b48642f5792b234044 AS builder 2 | 3 | # Can be removed once testing done from go rather than bash 4 | # gcc and libc-dev needed for go vet 5 | RUN apk add --no-cache bash diffutils gcc libc-dev git jq openssl 6 | 7 | WORKDIR /src/github.com/RichardoC/kube-audit-rest 8 | 9 | COPY ./go.mod ./go.sum ./ 10 | 11 | RUN go mod download 12 | 13 | COPY . . 14 | 15 | 16 | RUN go vet ./... 17 | 18 | # Do simple local testing 19 | RUN ./testing/locally/local-testing.sh 20 | 21 | # CGO_ENABLED forces a static binary 22 | RUN CGO_ENABLED=0 GOOS=linux go build -o kube-audit-rest ./cmd/kube-audit-rest/main.go 23 | 24 | 25 | FROM alpine:3.22.0@sha256:8a1f59ffb675680d47db6337b49d22281a139e9d709335b492be023728e11715 26 | 27 | LABEL org.opencontainers.image.source="https://github.com/RichardoC/kube-audit-rest" 28 | LABEL org.opencontainers.image.description="Kubernetes audit logging, when you don't control the control plane" 29 | LABEL org.opencontainers.image.licenses="Apache-2.0" 30 | LABEL org.opencontainers.image.documentation="https://github.com/RichardoC/kube-audit-rest" 31 | LABEL org.opencontainers.image.title="kube-audit-rest" 32 | 33 | # Application port 34 | EXPOSE 9090 35 | # Metrics port 36 | EXPOSE 55555 37 | 38 | COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ 39 | 40 | COPY --from=builder /src/github.com/RichardoC/kube-audit-rest/kube-audit-rest /kube-audit-rest 41 | 42 | USER 255999 43 | 44 | ENTRYPOINT ["/bin/sh", "-c"] 45 | 46 | CMD ["/kube-audit-rest"] 47 | -------------------------------------------------------------------------------- /Dockerfile-distroless: -------------------------------------------------------------------------------- 1 | FROM golang:1.24.3-alpine3.21@sha256:ef18ee7117463ac1055f5a370ed18b8750f01589f13ea0b48642f5792b234044 AS builder 2 | 3 | # Can be removed once testing done from go rather than bash 4 | # gcc and libc-dev needed for go vet 5 | RUN apk add --no-cache bash diffutils gcc libc-dev git jq openssl 6 | 7 | WORKDIR /src/github.com/RichardoC/kube-audit-rest 8 | 9 | COPY ./go.mod ./go.sum ./ 10 | 11 | RUN go mod download 12 | 13 | COPY . . 14 | 15 | RUN go vet ./... 16 | 17 | # Do simple local testing 18 | RUN ./testing/locally/local-testing.sh 19 | 20 | # CGO_ENABLED forces a static binary 21 | RUN CGO_ENABLED=0 GOOS=linux go build -o kube-audit-rest ./cmd/kube-audit-rest/main.go 22 | 23 | RUN mkdir /new-tmp 24 | 25 | FROM scratch 26 | 27 | LABEL org.opencontainers.image.source="https://github.com/RichardoC/kube-audit-rest" 28 | LABEL org.opencontainers.image.description="Kubernetes audit logging, when you don't control the control plane" 29 | LABEL org.opencontainers.image.licenses="Apache-2.0" 30 | LABEL org.opencontainers.image.documentation="https://github.com/RichardoC/kube-audit-rest" 31 | LABEL org.opencontainers.image.title="kube-audit-rest" 32 | 33 | # Application port 34 | EXPOSE 9090 35 | # Metrics port 36 | EXPOSE 55555 37 | 38 | COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ 39 | 40 | COPY --from=builder /src/github.com/RichardoC/kube-audit-rest/kube-audit-rest /kube-audit-rest 41 | 42 | # Required as distroless doesn't contain a tmp dir 43 | COPY --from=builder /new-tmp /tmp 44 | 45 | USER 255999 46 | 47 | 48 | CMD ["/kube-audit-rest"] 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | 204 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # kube-audit-rest 2 | 3 | Want to get a kubernetes audit log without having the ability to configure the kube-api-server such as with [EKS](https://docs.aws.amazon.com/eks/latest/userguide/control-plane-logs.html), [GKE](https://issuetracker.google.com/issues/185868707) or [AKS](https://learn.microsoft.com/en-us/azure/aks/monitor-aks#collect-resource-logs)? 4 | Use kube-audit-rest to capture all mutation/creation API calls to disk, before exporting those to your logging infrastructure. 5 | This should be much cheaper than the Cloud Service Provider managed offerings which charges ~ per API call and don't support ingestion filtering. 6 | 7 | If you do control the kube-api-server then use the [built in audit logging](https://kubernetes.io/docs/tasks/debug/debug-cluster/audit/), not kube-audit-rest 8 | 9 | This tool is maintained and originally created by [Richard Tweed](https://github.com/RichardoC) 10 | 11 | 12 | ## What this is 13 | 14 | A simple logger of mutation/creation requests to the k8s api. 15 | 16 | 17 | ## What this isn't 18 | 19 | A filtering/redaction/forwarder system. This can be done with many different tools, so this tool doesn't rely on any specific tooling. Examples of using kube-audit-rest with Elastic Search can be found in 20 | 21 | ## Why should I care? 22 | 23 | You can use kube-audit-rest to avoid bills of thousands of dollars per cluster per year for non-filterable Kubernetes audit logs. 24 | 25 | With kube-audit-rest you can configure exactly which events are recorded and directly send them to your SIEM, dramatically reducing storage and ingestion charges compared with only on/off configurations such as [EKS](https://docs.aws.amazon.com/eks/latest/userguide/control-plane-logs.html) 26 | 27 | 28 | ## Kubernetes distribution compatibility 29 | 30 | Unknown but likely to work with all distributions due to how fundamental the ValidatingWebhook API is to Kubernetes operators. At worst the MutatingAdmissionWebhook API can be used instead, though that does mean that subverting this binary could lead to a cluster takeover and that it may not log the final version of the object. 31 | 32 | 33 | ## Usage 34 | 35 | An example of how to deploy this service can be found within `./k8s` and steps to actually deploy it in `testing/setup.sh` 36 | You could either run this centrally (though it would be difficult to tell which API calls are from which clusters) or running in each cluster. 37 | At minimum you require 38 | * Ability to create ValidatingWebhookConfiguration on the target k8s cluster. 39 | * A CA, and a TLS certificate signed for the address the kubernetes control plane is connecting to for connections to kube-audit-rest 40 | * kube-audit-rest running somewhere connectable by the kubernetes control plane. 41 | * some disk space for kube-audit-rest to write to. Defaults to `/tmp` which is a ramfs on most linux distributions, though not on Kubernetes. 42 | * Either to build a copy of the binary yourself, or download a copy of the docker image via the steps on the [packages page](https://github.com/RichardoC/kube-audit-rest/pkgs/container/kube-audit-rest) which is available as a distroless image (default, and `latest`) with suffix -distroless and a -alpine image based on the alpine docker image. 43 | 44 | If you are running kube-audit-rest within the kubernetes cluster it is auditing you also require 45 | * a deployment of kube-audit-rest running 46 | * a service targeting the kube-audit-rest pods 47 | 48 | 49 | ## Image variants 50 | 51 | kube-audit-rest images come in many flavors, each designed for a specific use case. 52 | 53 | These are all available for linux/amd64 and linux/arm64 54 | 55 | VERSION refers to github release versions 56 | 57 | COMMIT refers to the commits to main 58 | 59 | 60 | ### Available container tags 61 | 62 | While the tags below exist, it is recommended to pin to the digest of the container image you wish to use, these can be found on 63 | 64 | 65 | #### VERSION-distroless 66 | 67 | This is the preferred image for production usage, it only contains the required kube-audit-rest binary, and nothing else. 68 | 69 | This means it has the minimum size (~ 14 MB) and number of container layers which decreases image pull time. 70 | 71 | Since it doesn't contain an OS, or any other packages this will contain the minimum possible (reported) vulnerabilities. 72 | 73 | 74 | #### VERSION-alpine 75 | 76 | This is the preferred image for development usage, it contains the required kube-audit-rest binary, and uses a default alpine image containing a shell. 77 | 78 | This is larger, but since it contains a shell and other useful utilities it's the best one for experimenting with kube-audit-rest and diagnosing issues. 79 | 80 | 81 | #### COMMIT-distroless 82 | 83 | Generated container images will be pushed for every commit, so you can experiment with new functionality or bug fixes while waiting for an official release to be genenerated. Otherwise this is the same as `VERSION-distroless` 84 | 85 | 86 | #### COMMIT-alpine 87 | 88 | Generated container images will be pushed for every commit, so you can experiment with new functionality or bug fixes while waiting for an official release to be genenerated. Otherwise this is the same as `VERSION-alpine` 89 | 90 | 91 | #### latest 92 | 93 | This points to the latest `VERSION-distroless` image and is only recommended for demo purposes. Use versioned images for stable usage. 94 | 95 | 96 | ### Binary options 97 | 98 | ```bash 99 | $ kube-audit-rest --help 100 | Usage: 101 | kube-audit-rest [OPTIONS] 102 | 103 | Application Options: 104 | --logger-filename= Location to log audit log to (default: /tmp/kube-audit-rest.log) 105 | --audit-to-std-log Not recommended - log to stderr/stdout rather than a file 106 | --logger-max-size= Maximum size for each log file in megabytes (default: 500) 107 | --logger-max-backups= Maximum number of rolled log files to store, 0 means store all rolled files (default: 1) 108 | --cert-filename= Location of certificate for TLS (default: /etc/tls/tls.crt) 109 | --cert-key-filename= Location of certificate key for TLS (default: /etc/tls/tls.key) 110 | --server-port= Port to run https server on (default: 9090) 111 | -v, --verbosity Uses zap Development default verbose mode rather than production 112 | 113 | Help Options: 114 | -h, --help Show this help message 115 | ``` 116 | 117 | 118 | ### Example usage 119 | 120 | These can be found in the <./examples> directory, and documented in this readme. 121 | 122 | 123 | ### Resource requirements 124 | 125 | Unknown, if anyone performs benchmarks please open a pull request with your findings. These can be set by following the instructions [here](https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/). 126 | 127 | Current values seem to deal with > 12 requests per second. 128 | 129 | 130 | ### Limiting which requests are logged 131 | 132 | In your `ValidatingWebhookConfiguration` use the limited amount of resources and verbs you wish to log, rather than the `*`s in `./k8s/webhook.yaml` using the [Kubernetes documentation](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#webhook-configuration) 133 | 134 | 135 | ## API spec for kube-audit-rest output 136 | 137 | This is the [AdmissionRequest](https://kubernetes.io/docs/reference/config-api/apiserver-admission.v1/#admission-k8s-io-v1-AdmissionRequest) request with requestReceivedTimestamp injected in RFC3339 format (see #26 for why). 138 | 139 | kube-audit-rest will log one request per line, in compacted json. 140 | 141 | ### Example 142 | 143 | ```console 144 | {"kind":"AdmissionReview","apiVersion":"admission.k8s.io/v1","request":{"uid":"f452d444-9782-45ce-8eea-85e7c5a2801b","kind":{"group":"authorization.k8s.io","version":"v1","kind":"SelfSubjectAccessReview"},"resource":{"group":"authorization.k8s.io","version":"v1","resource":"selfsubjectaccessreviews"},"requestKind":{"group":"authorization.k8s.io","version":"v1","kind":"SelfSubjectAccessReview"},"requestResource":{"group":"authorization.k8s.io","version":"v1","resource":"selfsubjectaccessreviews"},"operation":"CREATE","userInfo":{"username":"system:admin","groups":["system:masters","system:authenticated"]},"object":{"kind":"SelfSubjectAccessReview","apiVersion":"authorization.k8s.io/v1","metadata":{"creationTimestamp":null,"managedFields":[{"manager":"steve","operation":"Update","apiVersion":"authorization.k8s.io/v1","time":"2022-11-30T17:46:52Z","fieldsType":"FieldsV1","fieldsV1":{"f:spec":{"f:resourceAttributes":{".":{},"f:group":{},"f:resource":{},"f:verb":{},"f:version":{}}}}}]},"spec":{"resourceAttributes":{"verb":"list","group":"helm.cattle.io","version":"v1","resource":"helmchartconfigs"}},"status":{"allowed":false}},"oldObject":null,"dryRun":false,"options":{"kind":"CreateOptions","apiVersion":"meta.k8s.io/v1"}},"requestReceivedTimestamp":"2023-02-04T21:56:41.610688981Z"} 145 | {"kind":"AdmissionReview","apiVersion":"admission.k8s.io/v1","request":{"uid":"f3491090-1952-4c4f-8825-6a1d1738e709","kind":{"group":"authorization.k8s.io","version":"v1","kind":"SelfSubjectAccessReview"},"resource":{"group":"authorization.k8s.io","version":"v1","resource":"selfsubjectaccessreviews"},"requestKind":{"group":"authorization.k8s.io","version":"v1","kind":"SelfSubjectAccessReview"},"requestResource":{"group":"authorization.k8s.io","version":"v1","resource":"selfsubjectaccessreviews"},"operation":"CREATE","userInfo":{"username":"system:admin","groups":["system:masters","system:authenticated"]},"object":{"kind":"SelfSubjectAccessReview","apiVersion":"authorization.k8s.io/v1","metadata":{"creationTimestamp":null,"managedFields":[{"manager":"steve","operation":"Update","apiVersion":"authorization.k8s.io/v1","time":"2022-11-30T17:46:51Z","fieldsType":"FieldsV1","fieldsV1":{"f:spec":{"f:resourceAttributes":{".":{},"f:group":{},"f:resource":{},"f:verb":{},"f:version":{}}}}}]},"spec":{"resourceAttributes":{"verb":"list","group":"batch","version":"v1","resource":"jobs"}},"status":{"allowed":false}},"oldObject":null,"dryRun":false,"options":{"kind":"CreateOptions","apiVersion":"meta.k8s.io/v1"}},"requestReceivedTimestamp":"2023-02-04T21:56:41.409906164Z"} 146 | ``` 147 | 148 | 149 | ## Metrics 150 | kube-audit-rest provide some metrics describing its own operations, both as an application specifically and as a go binary. . 151 | 152 | All the specific application metrics are prefixed with `kube_audit_rest_`. 153 | 154 | | Metric name | Metric type | Labels | Description | 155 | | ----------- | ----------- | ------ | ----------- | 156 | | kube_audit_rest_valid_requests_processed_total | Counter | | Total number of valid requests processed | 157 | | kube_audit_rest_http_requests_total | Counter | | Total number of requests to kube-audit-rest | 158 | 159 | kube-audit-rest also exposes all default go metrics from the (Prometheus Go collector)[https://github.com/prometheus/client_golang/blob/main/prometheus/go_collector.go] 160 | 161 | 162 | ## Building 163 | 164 | Requires nerdctl and rancher desktop as a way of building/testing locally with k8s. 165 | 166 | ```bash 167 | ./testing/setup.sh 168 | 169 | # To cleanup 170 | ./testing/cleanup.sh 171 | ``` 172 | 173 | 174 | ### Testing 175 | 176 | Run via the Building commands, then the following should contain various admission requests 177 | 178 | ```bash 179 | kubectl -n kube-audit-rest logs -l app=kube-audit-rest 180 | ``` 181 | 182 | Confirm that the k8s API is happy with this webhook (log location may vary, check Rancher Desktop docs) 183 | If it's working there should be no mention of this webook. 184 | 185 | ```bash 186 | vim $HOME/.local/share/rancher-desktop/logs/k3s.log 187 | ``` 188 | 189 | Example failures 190 | 191 | ``` 192 | W1127 13:26:10.911971 3402 dispatcher.go:142] Failed calling webhook, failing open kube-audit-rest.kube-audit-rest.svc.cluster.local: failed calling webhook "kube-audit-rest.kube-audit-rest.svc.cluster.local": failed to call webhook: Post "https://kube-audit-rest.kube-audit-rest.svc:443/log-request?timeout=1s": x509: certificate signed by unknown authority (possibly because of "crypto/rsa: verification error" while trying to verify candidate authority certificate "ca.local") 193 | W1127 13:35:04.936121 3402 dispatcher.go:142] Failed calling webhook, failing open kube-audit-rest.kube-audit-rest.svc.cluster.local: failed calling webhook "kube-audit-rest.kube-audit-rest.svc.cluster.local": failed to call webhook: Post "https://kube-audit-rest.kube-audit-rest.svc:443/log-request?timeout=1s": no endpoints available for service "kube-audit-rest" 194 | E1127 13:35:04.936459 3402 dispatcher.go:149] failed calling webhook "kube-audit-rest.kube-audit-rest.svc.cluster.local": failed to call webhook: Post "https://kube-audit-rest.kube-audit-rest.svc:443/log-request?timeout=1s": no endpoints available for service "kube-audit-rest" 195 | 196 | ``` 197 | 198 | 199 | ### Local testing 200 | 201 | This requires the [go toolchain for Go 1.21+](https://go.dev/doc/install), openssl and bash installed. 202 | 203 | ```bash 204 | testing/locally/local-testing.sh 205 | ... 206 | Test passed 207 | {"level":"info","msg":"Server is shutting down...","time":"2022-12-01T19:43:01Z"} 208 | {"level":"info","msg":"Server stopped","time":"2022-12-01T19:43:01Z"} 209 | Terminated 210 | ``` 211 | 212 | If this failed, you will see `output not as expected` 213 | 214 | 215 | ### Unit tests 216 | 217 | The individual components that create this application have unittests. To run them 218 | 219 | ``` 220 | go test ./... 221 | ``` 222 | 223 | ## Guide for developers 224 | 225 | If you want to know how the project is structured and/or you want to colaborate in it, you can read the following 226 | document: [ForDevelopers](docs/ForDevelopers.md) 227 | 228 | ## Known limitations and warnings 229 | 230 | From the k8s documentation [see rules](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.25/#validatingwebhook-v1-admissionregistration-k8s-io) 231 | 232 | ```text 233 | Rules describes what operations on what resources/subresources the webhook cares about. The webhook cares about an operation if it matches _any_ Rule. However, in order to prevent ValidatingAdmissionWebhooks and MutatingAdmissionWebhooks from putting the cluster in a state which cannot be recovered from without completely disabling the plugin, ValidatingAdmissionWebhooks and MutatingAdmissionWebhooks are never called on admission requests for ValidatingWebhookConfiguration and MutatingWebhookConfiguration objects. 234 | ``` 235 | 236 | This webhook also cannot know that all other validating webhooks passed so may log requests that were failed by other validating webhooks afterwards. 237 | 238 | Due to the `failure: ignore` in the example webhook configurations there may be missing requests that were not logged in the interests of availability of the kubernetes API.. 239 | 240 | WARNING: This will log all details of the request! This namespace should be very locked down to prevent privilege escalation! 241 | 242 | This webhook will also record dry-run requests. 243 | 244 | The audit log files will only exist if valid API calls are sent to the webhook binary. 245 | 246 | API calls can be logged repeatedly due to Kubernetes repeatedly re-calling the [webhook](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#reinvocation-policy) and thus may not be in chronological order. 247 | 248 | WARNING: This can only log mutation/creation requests. Read Only requests are *not* sent to mutating or validating webhooks unfortunately. 249 | 250 | Due to how dynamic admisson control works on managed Kubernetes clusters, kube-audit-rest is unable to verfiy that the requests are coming from the control plane, so there is the risk that an attacker could flood the logs with nonsense. This could be rectified by registering kube-audit-rest as a custom API (with https://github.com/kubernetes-sigs/apiserver-runtime https://kubernetes.io/docs/concepts/extend-kubernetes/) and using that custom API as the webhook, as the api server uses MTLS to authenticate but this risks causing denial of service if the kube-audit-rest server is down, so won't be done for now. 251 | 252 | kube-audit-rest will not be able to write audit logs if the host disks are full. 253 | 254 | ### Certificate expires/invalid 255 | 256 | The application logs will be full of the following error, and you will *not* get any more audit logs until this is fixed. 257 | ```2022/11/27 15:36:42 http: TLS handshake error from 10.42.0.1:46380: EOF``` 258 | 259 | Kubernetes may not load balance between replicas of kube-audit-rest in the way you expect as this behaviour appears to be undocumented. 260 | 261 | 262 | ### Read only actions not logged 263 | 264 | Unfortunately the admission controllers are only called for mutations and creations, so this cannot be used to capture read only API calls. 265 | 266 | Some well behaved API clients will create a `SubjectAccessReview` before making any API call. These can be used to spot all API calls made by those clients, but this is not required (and most clients don't.) 267 | 268 | 269 | ## Next steps 270 | 271 | * explain zero stability guarantees until above completed 272 | * follow GH best practises for workflows/etc 273 | * add prometheus metrics, particularly for total requests dealt with/request latency/invalid certificate refusal from client as this probably needs an alert as the cert needs replaced... 274 | * make it clear just how bad an idea stdout is, preferably with a PoC exploit of using that to take over a cluster via logs... 275 | * have the testing main.go spin up/shut down the binaries rather than using bash and make it clearer that diff is required. 276 | 277 | 278 | ## Completed next steps 279 | 280 | * Use flags for certs locations 281 | * write to a file rather than STDOUT with rotation and/or a max size 282 | * Use structured logging 283 | * rename to kube-audit-rest from kube-rest-audit 284 | * Add examples folder 285 | * Add local testing 286 | * use zap for logging rather than logrus for prettier http error logs 287 | * despite the issues, make it possible to log to stdout/stderr, as useful for capturing less sensitive info directly without infra 288 | * upload images on git commit 289 | * make a distroless version 290 | * clarify logs are not guaranteed to be ordered because there aren't guarantees from k8s that the requests would arrive in order. 291 | * explain how to limit resources it's logging via the webhook resource (just a link to the k8s docs) 292 | * make it clear log file only exists if requests are sent 293 | * clarify log file format is the raw response with no newlines in the json, with one response per line. 294 | * clarify that kubernetes may not loadbalance between replicas as expected. 295 | * document that image defaults to distroless 296 | * add simple metrics 297 | * test properly rather than use sleeps to manage async things... 298 | * Correctly configure GOMAXPROCS to reduce unhelpful throttling 299 | * have workflow to test that docker image can be created once a maintainer adds a label to the PR. 300 | * Make it clear this only tracks mutations due to limitations of the k8s api. 301 | * Show kube-audit-rest working with a full elastic stack 302 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | TBC - this is a placeholder 6 | 7 | Use this section to tell people about which versions of your project are 8 | currently being supported with security updates. 9 | 10 | | Version | Supported | 11 | | ------- | ------------------ | 12 | | 1.0.x | :white_check_mark: | 13 | | < 1.0.0 | :x: | 14 | 15 | ## Reporting a Vulnerability 16 | 17 | Please send an email to and give me 14 days to respond and we can discuss time to fix/responsible disclosure. 18 | -------------------------------------------------------------------------------- /cmd/kube-audit-rest/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2023 Richard Tweed. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package main 15 | 16 | import ( 17 | "log" 18 | "os" 19 | "os/signal" 20 | "syscall" 21 | 22 | auditwritter "github.com/RichardoC/kube-audit-rest/internal/audit_writer" 23 | diskwriter "github.com/RichardoC/kube-audit-rest/internal/audit_writer/disk_writer" 24 | stderrwriter "github.com/RichardoC/kube-audit-rest/internal/audit_writer/stderr_writer" 25 | "github.com/RichardoC/kube-audit-rest/internal/common" 26 | eventprocessorimpl "github.com/RichardoC/kube-audit-rest/internal/event_processor/event_processor_impl" 27 | logrequestlistener "github.com/RichardoC/kube-audit-rest/internal/http_listener/log_request_listener" 28 | prometheusmetrics "github.com/RichardoC/kube-audit-rest/internal/metrics/prometheus_metrics" 29 | "github.com/thought-machine/go-flags" 30 | 31 | "go.uber.org/automaxprocs/maxprocs" 32 | ) 33 | 34 | type Options struct { 35 | LoggerFilename string `long:"logger-filename" description:"Location to log audit log to" default:"/tmp/kube-audit-rest.log"` 36 | AuditToStdErr bool `long:"audit-to-std-log" description:"Not recommended - log to stderr/stdout rather than a file"` 37 | LoggerMaxSize int `long:"logger-max-size" description:"Maximum size for each log file in megabytes" default:"500"` 38 | LoggerMaxBackups int `long:"logger-max-backups" description:"Maximum number of rolled log files to store, 0 means store all rolled files" default:"1"` 39 | CertFilename string `long:"cert-filename" description:"Location of certificate for TLS" default:"/etc/tls/tls.crt"` 40 | CertKeyFilename string `long:"cert-key-filename" description:"Location of certificate key for TLS" default:"/etc/tls/tls.key"` 41 | ServerPort int `long:"server-port" description:"Port to run https server on" default:"9090"` 42 | MetricsPort int `long:"metrics-port" description:"Port to run http metrics server on" default:"55555"` 43 | Verbose bool `long:"verbosity" short:"v" description:"Uses zap Development default verbose mode rather than production"` 44 | } 45 | 46 | func main() { 47 | // Set and parse command line options 48 | var opts Options 49 | parser := flags.NewParser(&opts, flags.Default) 50 | _, err := parser.Parse() 51 | if err != nil { 52 | log.Fatalf("can't parse flags: %v", err) 53 | } 54 | 55 | // Configure the logger 56 | if opts.Verbose { 57 | common.ConfigGlobalLogger(common.Dbg) 58 | } else { 59 | common.ConfigGlobalLogger(common.Prod) 60 | } 61 | defer common.Logger.Sync() 62 | 63 | common.Logger.Infow("Got config", "config", opts) 64 | 65 | // Set maxprocs and have it use our nice logger 66 | maxprocs.Set(maxprocs.Logger(common.Logger.Infof)) 67 | 68 | // Create the components. In the future we can consider using containers 69 | metricsServer := prometheusmetrics.New(opts.MetricsPort) 70 | var auditWriter auditwritter.AuditWritter 71 | if opts.AuditToStdErr { 72 | auditWriter = stderrwriter.New() 73 | } else { 74 | auditWriter = diskwriter.New(opts.LoggerFilename, opts.LoggerMaxSize, opts.LoggerMaxBackups) 75 | } 76 | eventProcessor, err := eventprocessorimpl.New(auditWriter, metricsServer) 77 | 78 | if err != nil { 79 | common.Logger.Fatalf("failed to start audit eventProcessor with: %s", err.Error()) 80 | } 81 | 82 | httpListener := logrequestlistener.New(opts.ServerPort, opts.CertFilename, opts.CertKeyFilename, eventProcessor) 83 | 84 | go metricsServer.Start() 85 | go httpListener.Start() 86 | 87 | // Logic to capture SIGTERM and ctrl+c, so we can do a graceful shutdown 88 | done := make(chan bool) 89 | quit := make(chan os.Signal, 1) 90 | signal.Notify(quit, os.Interrupt) 91 | signal.Notify(quit, syscall.SIGTERM) 92 | 93 | go func() { 94 | <-quit 95 | httpListener.Stop() 96 | metricsServer.Stop() 97 | close(done) 98 | }() 99 | 100 | <-done 101 | common.Logger.Infow("Server stopped") 102 | } 103 | -------------------------------------------------------------------------------- /docs/ForDevelopers.md: -------------------------------------------------------------------------------- 1 | ## Project structure 2 | 3 | The go code is placed in two directories: 4 | - **cmd**: Where we define the `main` entrypoint. At this moment we have a single `main`, `cmd/kube-audit-rest/main.go` 5 | (we are generating a single executable) but this structure allows us to add other `main` programs in the future 6 | - **internal**: Where we have the components that build our application 7 | 8 | Within `internal`, we have a folder for each component. The only exception is `common`, 9 | which contains some functions, singletons and constants that are used across most of the components. 10 | 11 | Each directory within `internal` (except `common`), has a file called `i.go`, 12 | which defines the interface of a component. Then, we have a directory for each implementation of this interface. 13 | Let's see `audit_writer` as an example. 14 | 15 | ``` 16 | iaudit_writer.go # defines the interface AuditWriter 17 | disk_writer 18 | |- disk_writer.go # implementation of AuditWriter. It writes stuff on a file on disk 19 | \_ disk_writer_test.go # unittests for disk_writer.go 20 | stderr_writer 21 | |- stderr_writer.go # implementation of AuditWriter. It write stuff on stderr 22 | \_ stderr_writer_test.go # unittests for stderr_writer.go 23 | common_writer 24 | \_ common_writer.go # common functions that can be used in all the AuditWriter implementations 25 | ``` 26 | 27 | ## Application architecture 28 | 29 | This project follows a modular architecture. Components can only talk between them using the component interface. 30 | They should never use anything specific of the implementation of such interface. The advantages are: 31 | 32 | - We can easily change the implementation of a component without affecting the other components. 33 | - Cyclic dependencies are not a problem. With this layout, the implementation of interface A could use interface 34 | B and the implementation of interface B could use interface A. 35 | - We can generate mocks for each interface, so we can unittest all the components. 36 | 37 | ## Graph of dependencies 38 | 39 | ``` 40 | ┌─────────────┐ 41 | │http_listener│ 42 | └──────┬──────┘ 43 | │ 44 | ┌───────▼───────┐ 45 | │event_processor│ 46 | └─┬───────────┬─┘ 47 | │ │ 48 | ┌─────▼─┐ ┌─────▼──────┐ 49 | │metrics│ │audit_writer│ 50 | └───────┘ └────────────┘ 51 | ``` 52 | 53 | ## Unittests 54 | 55 | Each implementation of an interface has unittests. To help isolating the component that we are testing 56 | we need to mock the other components that it interacts with. The mock generation is done using the 57 | `mockgen` package. This generation process is automated using the following approach. 58 | 59 | We write something similar to the following code snippet in each file that defines a component 60 | interface, like `iaudit_writer.go` 61 | 62 | ```go 63 | package 64 | 65 | //go:generate mockgen -package mymock -destination ../../mocks/_mock.go github.com/RichardoC/kube-audit-rest/internal/ , 66 | ``` 67 | 68 | Then, the following command regenerates all the mocks. 69 | 70 | ``` 71 | go generate ./... 72 | ``` 73 | 74 | As usual, the execution of the unittests is done by 75 | 76 | ``` 77 | go test ./... 78 | ``` 79 | -------------------------------------------------------------------------------- /examples/full-elastic-stack/README.md: -------------------------------------------------------------------------------- 1 | # Example - Running kube-audit-rest and ingesting events into elastic search 2 | 3 | After following this example, you will have an elastic search cluster running, with all creation/mutation events (except the limitations listed in the readme of this repo, and some spammy ones) being ingested into that cluster using vector. You'll also have configuration that drops the data field of secrets so that they aren't logged and filter out a variety of low signal to noise objects. This can be seen in the `vector-config` configmap in `examples/full-elastic-stack/k8s/kube-audit-rest.yaml` 4 | 5 | You'll be able to navigate around in kibana and get a feel for the schema used, and what is available form this tool. 6 | 7 | ## Prerequisites 8 | 9 | * Internet access 10 | 11 | * A kubernetes cluster 12 | * which you have admin level privileges to 13 | * that you don't mind having to recreate 14 | * that doesn't already have elastic search operator running 15 | 16 | * openssl 17 | * kubectl 18 | * bash 19 | * envsubst 20 | * base64 21 | * echo 22 | 23 | A good example would be Rancher Desktop, or minikube. 24 | 25 | ## How to follow the guide 26 | 27 | Run all commands in the ```bash ``` blocks, and run them from a terminal at the root of this repo. 28 | 29 | Warning, this is designed to be run on a local cluster which can be destroyed afterwards. 30 | 31 | ## Setting up elastic search 32 | 33 | Largely following 34 | 35 | Set up the custom resources eck requires, then run the operator and lastly start an elastic search and kibana. 36 | 37 | ```bash 38 | kubectl create -f https://download.elastic.co/downloads/eck/2.9.0/crds.yaml 39 | 40 | kubectl apply -f https://download.elastic.co/downloads/eck/2.9.0/operator.yaml 41 | 42 | kubectl apply -f examples/full-elastic-stack/k8s/elastic-cluster.yaml 43 | 44 | ``` 45 | 46 | Check that an operator pod is running in elastic-system 47 | 48 | ```bash 49 | kubectl -n elastic-system get po 50 | ``` 51 | 52 | Then check that the elastic cluster, and kibana is running in example-kube-audit-rest 53 | 54 | ```bash 55 | kubectl -n example-kube-audit-rest get po 56 | NAME READY STATUS RESTARTS AGE 57 | elasticsearch-kube-audit-rest-es-default-0 1/1 Running 0 23m 58 | kibana-kube-audit-rest-kb-868975c597-4r9nj 1/1 Running 0 23m 59 | ``` 60 | 61 | ## Accessing Kibana 62 | Port forward kibana in a terminal, this will keep running until you terminate it with `ctrl+c` 63 | 64 | ```bash 65 | kubectl -n example-kube-audit-rest port-forward svc/kibana-kube-audit-rest-kb-http 60443:https 66 | ``` 67 | 68 | To see that kibana is working navigate to from your browser, and click to ignore the invalid certificate 69 | 70 | Use another terminal to get the password to access 71 | 72 | ```bash 73 | echo "username is elastic" 74 | echo "password is $(kubectl -n example-kube-audit-rest get secret elasticsearch-kube-audit-rest-es-elastic-user -o=jsonpath='{.data.elastic}' | base64 --decode; echo)" 75 | ``` 76 | 77 | ## Set up kube-audit-rest 78 | 79 | Using a locked version from 2023-10-05 80 | 81 | This configuration is deliberately non-HA, and will allow API calls to keep running if the wehook isn't running (failurePolicy:Ignore rather than Fail) 82 | 83 | It will record all create/mutation/deletion API calls, which can leak service account tokens via secrets. This is to show to maximum capabilities. 84 | 85 | In production limit this only to resources you want to capture. 86 | 87 | ## Create required certificates and upload them 88 | 89 | Webhooks are required to serve TLS, so creating a the certificate authority and tls certificates 90 | 91 | ```bash 92 | ./examples/full-elastic-stack/certs/certs.sh 93 | ``` 94 | 95 | Upload the TLS certificate for use by the kube-audit-rest workload. 96 | 97 | ```bash 98 | kubectl -n example-kube-audit-rest create secret tls kube-audit-rest --cert=./tmp/full-elastic-stack/server.crt --key=tmp/full-elastic-stack/server.key --dry-run=client -oyaml | kubectl -n example-kube-audit-rest apply -f - 99 | ``` 100 | 101 | ## Deploy kube-audit-rest 102 | 103 | ```bash 104 | kubectl -n example-kube-audit-rest apply -f examples/full-elastic-stack/k8s/kube-audit-rest.yaml 105 | ``` 106 | 107 | ## Deploy the validation webhook 108 | 109 | Warning, this is set to apply to every API call, and block the call if the webhook doesn't respond with success. 110 | 111 | Webhooks are required to serve TLS, so the templating is including the certificate authority so kubernetes trusts our certificate 112 | 113 | ```bash 114 | export CABUNDLEB64="$(cat tmp/full-elastic-stack/rootCA.pem | base64 | tr -d '\n')" 115 | cat examples/full-elastic-stack/k8s/ValidatingWebhookConfiguration.yaml | envsubst | kubectl apply -f - 116 | unset CABUNDLEB64 117 | ``` 118 | 119 | If you have any issues, delete the webhook with the following command, and change the failurePolicy to Ignore rather than Fail 120 | 121 | ## Do some api calls so you have something to look at 122 | ```bash 123 | kubectl create ns test-namespace 124 | kubectl -n test-namespace create serviceaccount abc 125 | kubectl -n test-namespace create secret generic example-secret --from-literal=VerySecret=topsecret 126 | kubectl -n test-namespace delete secret example-secret 127 | kubectl delete namespace test-namespace 128 | ``` 129 | 130 | ## View the data in elastic search via kibana 131 | Navigate to provided the port forward from earlier is still running, or restart it if required. 132 | 133 | Create a data view 134 | ``` 135 | Name: example-kube-audit-rest-audit-events 136 | Index pattern: example-kube-audit-rest-audit-events 137 | Timestamp field: timestamp 138 | ``` 139 | 140 | Then click "Save data view to Kibana" 141 | 142 | You'll be able to see that the API calls regarding the secret were redacted 143 | 144 | ## Install in another namespace 145 | If you would like to deploy to another namespace, recursively replace all occurences of `example-kube-audit-rest` 146 | with your desired namespace name. Then just follow the guide from the local README.md - the commands will have 147 | been adjusted. If you don't feel secure about just replacing everything, go through the matches 148 | manually. Use at your own risk! 149 | 150 | ## Attach to an already deployed Elasticsearch-instance. 151 | After testing it in a separate namespace, you can also use this example to connect kube-audit-rest with an existing 152 | Elasticsearch instance. Adjust the namespace as indicated above, then recursively replace `elasticsearch-kube-audit-rest` 153 | the name of your Elasticsearch instance. If you don't feel secure about just replacing everything, go through the matches 154 | manually. 155 | 156 | Then follow this guide, but skip deploying the Elastic CRD's, Operator and 157 | `examples/full-elastic-stack/k8s/elastic-cluster.yaml` as you already have them. On your own risk :) 158 | 159 | ## Tidyup 160 | 161 | WARNING this *will* delete the elastic operator, if it's already running in this cluster 162 | 163 | ```bash 164 | 165 | export CABUNDLEB64="$(cat tmp/full-elastic-stack/rootCA.pem | base64 | tr -d '\n')" 166 | cat examples/full-elastic-stack/k8s/ValidatingWebhookConfiguration.yaml | envsubst | kubectl delete -f - 167 | unset CABUNDLEB64 168 | 169 | kubectl delete namespace example-kube-audit-rest 170 | 171 | kubectl delete -f https://download.elastic.co/downloads/eck/2.9.0/crds.yaml 172 | 173 | kubectl delete -f https://download.elastic.co/downloads/eck/2.9.0/operator.yaml 174 | 175 | kubectl delete -f examples/full-elastic-stack/k8s/elastic-cluster.yaml 176 | 177 | ``` 178 | -------------------------------------------------------------------------------- /examples/full-elastic-stack/certs/ca.cnf: -------------------------------------------------------------------------------- 1 | [req] 2 | default_bits = 2048 3 | prompt = no 4 | default_md = sha256 5 | distinguished_name = dn 6 | x509_extensions = v3_req 7 | 8 | CA_default = [ca] 9 | 10 | [dn] 11 | C=GB 12 | OU=Engineering 13 | emailAddress=admin@localhost 14 | CN = ca.local 15 | 16 | [ca] 17 | copy_extensions = copy 18 | 19 | 20 | [ v3_req ] 21 | # Extensions for a typical CA (`man x509v3_config`). 22 | subjectKeyIdentifier = hash 23 | authorityKeyIdentifier = keyid:always,issuer 24 | basicConstraints = critical, CA:true 25 | keyUsage = critical, digitalSignature, cRLSign, keyCertSign -------------------------------------------------------------------------------- /examples/full-elastic-stack/certs/certs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eux 4 | 5 | # Default to root if no .git missing 6 | ROOT=$(git rev-parse --show-toplevel || echo '.' ) 7 | cd "$ROOT" 8 | 9 | CONF_DIR="$(pwd)/examples/full-elastic-stack/certs" 10 | 11 | TMP="$(pwd)/tmp/full-elastic-stack" 12 | echo "$TMP" 13 | mkdir -p "$TMP" 14 | cd "$TMP" 15 | 16 | openssl genrsa -out rootCA.key 2048 &> /dev/null 17 | 18 | openssl req -x509 -new -nodes -key rootCA.key -sha256 -days 1460 -out rootCA.pem -config "$CONF_DIR/ca.cnf" &> /dev/null 19 | 20 | openssl req -new -nodes -sha256 -out server.csr -newkey rsa:2048 -keyout server.key -config "$CONF_DIR/server.csr.cnf" &> /dev/null 21 | 22 | openssl x509 -req -in server.csr -CA rootCA.pem -CAkey rootCA.key -CAcreateserial -out server.crt -days 500 -sha256 -extensions v3_req -extfile "$CONF_DIR/server.csr.cnf" &> /dev/null -------------------------------------------------------------------------------- /examples/full-elastic-stack/certs/server.csr.cnf: -------------------------------------------------------------------------------- 1 | [req] 2 | default_bits = 2048 3 | prompt = no 4 | default_md = sha256 5 | distinguished_name = dn 6 | req_extensions = v3_req 7 | 8 | [dn] 9 | C=GB 10 | OU=Engineering 11 | emailAddress=admin@localhost 12 | CN = kube-audit-rest 13 | 14 | 15 | [v3_req] 16 | #authorityKeyIdentifier=keyid,issuer 17 | basicConstraints=CA:FALSE 18 | keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment 19 | 20 | 21 | subjectAltName = @alt_names 22 | 23 | [alt_names] 24 | DNS.1 = kube-audit-rest 25 | DNS.2 = *.example-kube-audit-rest 26 | DNS.3 = *.example-kube-audit-rest.svc 27 | DNS.4 = *.example-kube-audit-rest.svc.cluster.local -------------------------------------------------------------------------------- /examples/full-elastic-stack/k8s/ValidatingWebhookConfiguration.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: admissionregistration.k8s.io/v1 3 | kind: ValidatingWebhookConfiguration # Can also be a MutatingWebhookConfiguration if required 4 | metadata: 5 | name: kube-audit-rest 6 | labels: 7 | app: kube-audit-rest 8 | webhooks: 9 | - name: kube-audit-rest.example-kube-audit-rest.svc.cluster.local 10 | failurePolicy: Ignore 11 | timeoutSeconds: 1 12 | sideEffects: None 13 | clientConfig: 14 | service: 15 | name: kube-audit-rest 16 | namespace: example-kube-audit-rest 17 | path: "/log-request" 18 | caBundle: "$CABUNDLEB64" # To be replaced 19 | rules: # To be reduced as needed 20 | - operations: [ "*" ] 21 | apiGroups: ["*"] 22 | apiVersions: ["*"] 23 | resources: ["*/*"] 24 | scope: "*" 25 | admissionReviewVersions: ["v1"] 26 | -------------------------------------------------------------------------------- /examples/full-elastic-stack/k8s/elastic-cluster.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Namespace 4 | metadata: 5 | name: example-kube-audit-rest 6 | --- 7 | apiVersion: elasticsearch.k8s.elastic.co/v1 8 | kind: Elasticsearch 9 | metadata: 10 | name: elasticsearch-kube-audit-rest 11 | namespace: example-kube-audit-rest 12 | spec: 13 | version: 8.10.4 14 | nodeSets: 15 | - name: default 16 | count: 1 17 | config: 18 | node.store.allow_mmap: false 19 | --- 20 | apiVersion: kibana.k8s.elastic.co/v1 21 | kind: Kibana 22 | metadata: 23 | name: kibana-kube-audit-rest 24 | namespace: example-kube-audit-rest 25 | spec: 26 | version: 8.10.4 27 | count: 1 28 | elasticsearchRef: 29 | name: elasticsearch-kube-audit-rest 30 | config: 31 | telemetry: 32 | optIn: false 33 | -------------------------------------------------------------------------------- /examples/full-elastic-stack/k8s/kube-audit-rest.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: ConfigMap 4 | metadata: 5 | name: vector-config 6 | data: 7 | vector.yaml: | 8 | sources: 9 | audit-files: 10 | type: file 11 | include: 12 | - /tmp/* 13 | transforms: 14 | audit-files-json-parser-and-redaction: 15 | inputs: 16 | - audit-files 17 | type: remap 18 | reroute_dropped: true 19 | source: |- 20 | # Ensure that the message is recognised as json 21 | # Setting a max_depth because elastic can get very unhappy with how nested k8s objects are 22 | # The depth is low because elastic requires all messages to have the same fields, and deeply 23 | # nested objects like pods don't match the schema of things like configmaps 24 | . = parse_json!(.message, max_depth: 2) 25 | # Remove managedFields as they often cause elastic search ingestion issues 26 | del(.request.object.metadata.managedFields) 27 | # remove any empty objects as elastic search gets unhappy with them, 28 | # managedFields are a bad example of this 29 | . = compact!(., recursive:true, array:true, object:true, null: true) 30 | # remove objects where the key is a dot, as elasticsearch refuses to ingest anything containing these 31 | # again, managedFields are terrible for this 32 | . = remove!(., path: ["."]) 33 | # set the timestamp in a kibana friendly way from the custom field kube-audit-rest uses 34 | ."@timestamp" = .requestReceivedTimestamp 35 | # redact the actual value of a secret 36 | 37 | if .request.requestKind.kind == "Secret" { 38 | # Redact the secret data 39 | del(.request.object.data) 40 | .request.object.data.redacted = "REDACTED" 41 | del(.request.oldObject.data) 42 | .request.oldObject.data.redacted = "REDACTED" 43 | 44 | # Remove the previously set secret data - Not bothering to parse it as this annotation shouldn't ever be needed 45 | del(.request.object.metadata.annotations."kubectl.kubernetes.io/last-applied-configuration") 46 | del(.request.oldObject.metadata.annotations."kubectl.kubernetes.io/last-applied-configuration") 47 | } 48 | filter-spam: 49 | inputs: 50 | - audit-files-json-parser-and-redaction 51 | type: filter 52 | condition: 53 | type: "vrl" 54 | source: |- 55 | # It's unlikely that anyone needs to care about leases and they're very very chatty 56 | # TokenReviews are only requested by well behaved kubernetes clients, so can be ignored as they're low value to noise 57 | .request.kind.group != "coordination.k8s.io" && .request.kind.kind != "TokenReview" 58 | sinks: 59 | elastic-sink: 60 | type: elasticsearch 61 | api_version: v8 62 | bulk: 63 | index: kube-audit-rest-example-audit-events 64 | inputs: 65 | - filter-spam 66 | endpoints: 67 | - https://elasticsearch-kube-audit-rest-es-http.example-kube-audit-rest:9200 68 | auth: 69 | strategy: basic 70 | user: elastic 71 | password: "${ESP:?err}" 72 | mode: 73 | bulk 74 | tls: # TODO: fix this to actually accept the ES self signed certificates 75 | verify_certificate: false 76 | --- 77 | apiVersion: apps/v1 78 | kind: Deployment 79 | metadata: 80 | labels: 81 | app: kube-audit-rest 82 | name: kube-audit-rest 83 | spec: 84 | progressDeadlineSeconds: 600 85 | replicas: 1 # Can be HA 86 | revisionHistoryLimit: 10 87 | selector: 88 | matchLabels: 89 | app: kube-audit-rest 90 | strategy: 91 | rollingUpdate: 92 | maxSurge: 25% 93 | maxUnavailable: 25% 94 | type: RollingUpdate 95 | template: 96 | metadata: 97 | labels: 98 | app: kube-audit-rest 99 | spec: 100 | automountServiceAccountToken: false 101 | containers: 102 | - image: ghcr.io/richardoc/kube-audit-rest:ad68f71978e8cd610b5b06769fab301cf9ee74d0-distroless@sha256:2444c1207156681c4ed04e7bb02662820c9bfb31b50e8fe5b0112b3f8f577d42 103 | imagePullPolicy: IfNotPresent 104 | name: kube-audit-rest 105 | resources: 106 | requests: 107 | cpu: "2m" 108 | memory: "10Mi" 109 | limits: 110 | cpu: "1" 111 | memory: "32Mi" 112 | ports: 113 | - containerPort: 9090 114 | protocol: TCP 115 | name: https 116 | - containerPort: 55555 117 | protocol: TCP 118 | name: metrics 119 | volumeMounts: 120 | - name: certs 121 | mountPath: "/etc/tls" 122 | readOnly: true 123 | - name: tmp 124 | mountPath: "/tmp" 125 | securityContext: 126 | allowPrivilegeEscalation: false 127 | readOnlyRootFilesystem: true 128 | capabilities: 129 | drop: 130 | - ALL 131 | - name: vector 132 | image: docker.io/timberio/vector:0.33.0-distroless-static@sha256:90e14483720ea7dfa5c39812a30f37d3bf3a94b6611787a0d14055b8ac31eb1f 133 | resources: 134 | requests: 135 | cpu: "2m" 136 | memory: "10Mi" 137 | limits: 138 | cpu: "2" 139 | memory: "512Mi" 140 | env: 141 | - name: ESP 142 | valueFrom: 143 | secretKeyRef: 144 | name: elasticsearch-kube-audit-rest-es-elastic-user 145 | key: elastic 146 | volumeMounts: 147 | - name: tmp 148 | mountPath: "/tmp" 149 | readOnly: true 150 | - name: vector-config 151 | mountPath: "/etc/vector/" 152 | readOnly: true 153 | restartPolicy: Always 154 | terminationGracePeriodSeconds: 30 155 | volumes: 156 | - name: certs 157 | secret: 158 | secretName: kube-audit-rest 159 | - name: tmp 160 | emptyDir: 161 | sizeLimit: 2Gi # Based on default of 3 files at 500Mi 162 | - name: vector-config 163 | configMap: 164 | name: vector-config 165 | --- 166 | apiVersion: v1 167 | kind: Service 168 | metadata: 169 | labels: 170 | app: kube-audit-rest 171 | name: kube-audit-rest 172 | spec: 173 | ports: 174 | - name: https 175 | port: 443 176 | protocol: TCP 177 | targetPort: https 178 | - name: metrics 179 | port: 55555 180 | protocol: TCP 181 | targetPort: metrics 182 | selector: 183 | app: kube-audit-rest 184 | sessionAffinity: None 185 | type: ClusterIP 186 | -------------------------------------------------------------------------------- /examples/live-demo/demo.sh: -------------------------------------------------------------------------------- 1 | 2 | testing/setup.sh 3 | 4 | # Force auditing to stdout 5 | kubectl -n kube-audit-rest patch deployment kube-audit-rest --patch='{"spec":{"template":{"spec":{"$setElementOrder/containers":[{"name":"kube-audit-rest"}],"containers":[{"args":["--audit-to-std-log"],"name":"kube-audit-rest"}]}}}}' 6 | 7 | kubectl delete ns hacker --ignore-not-found=true 8 | kubectl create ns hacker 9 | 10 | 11 | kubectl -n hacker create secret generic hacking-creds --from-literal="DB_PASSWORD"="V3rySecr3t" 12 | 13 | kubectl -n hacker run monero-miner --force=true --image alpine -- tail -f /dev/null 14 | 15 | kubectl -n kube-audit-rest logs kube-audit-rest-5c6bc76b4c-7kd82 | grep zapio| grep hacker | jq '.msg | fromjson ' 16 | -------------------------------------------------------------------------------- /examples/seeing-audit-log-locally/README.md: -------------------------------------------------------------------------------- 1 | # Seeing the audit log locally, if using rancher desktop 2 | 3 | The following one liner will output the generated audit log to standard out, which can then be piped to a file and used for testing your parser etc. 4 | 5 | ```bash 6 | rdctl shell sudo cat "/var/lib/kubelet/pods/$(kubectl -n kube-audit-rest get po -l app=kube-audit-rest -ojsonpath='{.items[0].metadata.uid}' )/volumes/kubernetes.io~empty-dir/tmp/kube-audit-rest.log" 7 | ``` 8 | 9 | ## Warnings 10 | 11 | This is relying on k3s putting this at specific paths, which may not be true in future versions. This was tested with Rancher Desktop 1.7.0 and Kubernetes version 1.26 12 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/RichardoC/kube-audit-rest 2 | 3 | go 1.22 4 | 5 | require ( 6 | github.com/golang/mock v1.6.0 7 | github.com/prometheus/client_golang v1.22.0 8 | github.com/stretchr/testify v1.10.0 9 | github.com/thought-machine/go-flags v1.7.0 10 | github.com/tidwall/gjson v1.18.0 11 | github.com/tidwall/sjson v1.2.5 12 | go.uber.org/automaxprocs v1.6.0 13 | go.uber.org/zap v1.27.0 14 | gopkg.in/natefinch/lumberjack.v2 v2.2.1 15 | ) 16 | 17 | require ( 18 | github.com/beorn7/perks v1.0.1 // indirect 19 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 20 | github.com/davecgh/go-spew v1.1.1 // indirect 21 | github.com/klauspost/compress v1.18.0 // indirect 22 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 23 | github.com/pmezard/go-difflib v1.0.0 // indirect 24 | github.com/prometheus/client_model v0.6.1 // indirect 25 | github.com/prometheus/common v0.62.0 // indirect 26 | github.com/prometheus/procfs v0.15.1 // indirect 27 | github.com/tidwall/match v1.1.1 // indirect 28 | github.com/tidwall/pretty v1.2.1 // indirect 29 | go.uber.org/multierr v1.11.0 // indirect 30 | golang.org/x/sys v0.30.0 // indirect 31 | google.golang.org/protobuf v1.36.5 // indirect 32 | gopkg.in/yaml.v3 v3.0.1 // indirect 33 | ) 34 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 2 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 3 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 4 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= 8 | github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= 9 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 10 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 11 | github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= 12 | github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= 13 | github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= 14 | github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= 15 | github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= 16 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 17 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 18 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 19 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 20 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 21 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 22 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 23 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 24 | github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= 25 | github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= 26 | github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU= 27 | github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k= 28 | github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= 29 | github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= 30 | github.com/prometheus/client_golang v1.20.0 h1:jBzTZ7B099Rg24tny+qngoynol8LtVYlA2bqx3vEloI= 31 | github.com/prometheus/client_golang v1.20.0/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= 32 | github.com/prometheus/client_golang v1.20.1 h1:IMJXHOD6eARkQpxo8KkhgEVFlBNm+nkrFUyGlIu7Na8= 33 | github.com/prometheus/client_golang v1.20.1/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= 34 | github.com/prometheus/client_golang v1.20.2 h1:5ctymQzZlyOON1666svgwn3s6IKWgfbjsejTMiXIyjg= 35 | github.com/prometheus/client_golang v1.20.2/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= 36 | github.com/prometheus/client_golang v1.20.3 h1:oPksm4K8B+Vt35tUhw6GbSNSgVlVSBH0qELP/7u83l4= 37 | github.com/prometheus/client_golang v1.20.3/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= 38 | github.com/prometheus/client_golang v1.20.4 h1:Tgh3Yr67PaOv/uTqloMsCEdeuFTatm5zIq5+qNN23vI= 39 | github.com/prometheus/client_golang v1.20.4/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= 40 | github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= 41 | github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= 42 | github.com/prometheus/client_golang v1.21.0 h1:DIsaGmiaBkSangBgMtWdNfxbMNdku5IK6iNhrEqWvdA= 43 | github.com/prometheus/client_golang v1.21.0/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg= 44 | github.com/prometheus/client_golang v1.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGCQ8YOmu+Ibk= 45 | github.com/prometheus/client_golang v1.21.1/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg= 46 | github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= 47 | github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= 48 | github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= 49 | github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= 50 | github.com/prometheus/common v0.53.0 h1:U2pL9w9nmJwJDa4qqLQ3ZaePJ6ZTwt7cMD3AG3+aLCE= 51 | github.com/prometheus/common v0.53.0/go.mod h1:BrxBKv3FWBIGXw89Mg1AeBq7FSyRzXWI3l3e7W3RN5U= 52 | github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= 53 | github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= 54 | github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= 55 | github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= 56 | github.com/prometheus/procfs v0.14.0 h1:Lw4VdGGoKEZilJsayHf0B+9YgLGREba2C6xr+Fdfq6s= 57 | github.com/prometheus/procfs v0.14.0/go.mod h1:XL+Iwz8k8ZabyZfMFHPiilCniixqQarAy5Mu67pHlNQ= 58 | github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= 59 | github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= 60 | github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= 61 | github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= 62 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 63 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 64 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 65 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 66 | github.com/thought-machine/go-flags v1.6.3 h1:AGA+iy7EP7ia/e46jzrmJV3oJhznESq7kNEILunmP8w= 67 | github.com/thought-machine/go-flags v1.6.3/go.mod h1:+r2g8uGwgGM7IGZzmMS97mKBFLDbW6vgFO1jxp0rDmg= 68 | github.com/thought-machine/go-flags v1.7.0 h1:BcZvT1pH6UQTythJ8s+k0K31N3ScHPOLIaREnAemZH8= 69 | github.com/thought-machine/go-flags v1.7.0/go.mod h1:+r2g8uGwgGM7IGZzmMS97mKBFLDbW6vgFO1jxp0rDmg= 70 | github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= 71 | github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U= 72 | github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= 73 | github.com/tidwall/gjson v1.17.2 h1:YlBFFaxZdSXKP8zhqh5CRbk0wL7oCAU3D+JJLU5pE7U= 74 | github.com/tidwall/gjson v1.17.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= 75 | github.com/tidwall/gjson v1.17.3 h1:bwWLZU7icoKRG+C+0PNwIKC6FCJO/Q3p2pZvuP0jN94= 76 | github.com/tidwall/gjson v1.17.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= 77 | github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= 78 | github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= 79 | github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= 80 | github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= 81 | github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= 82 | github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= 83 | github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= 84 | github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= 85 | github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= 86 | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 87 | go.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8= 88 | go.uber.org/automaxprocs v1.5.3/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0= 89 | go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= 90 | go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= 91 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 92 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 93 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 94 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 95 | go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= 96 | go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= 97 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 98 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 99 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 100 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 101 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 102 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 103 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 104 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 105 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 106 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 107 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 108 | golang.org/x/sys v0.0.0-20201207223542-d4d67f95c62d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 109 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 110 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 111 | golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= 112 | golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 113 | golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= 114 | golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 115 | golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= 116 | golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 117 | golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= 118 | golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 119 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 120 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 121 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 122 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 123 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 124 | golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 125 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 126 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 127 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 128 | google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= 129 | google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 130 | google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= 131 | google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= 132 | google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk= 133 | google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 134 | google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= 135 | google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 136 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 137 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 138 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 139 | gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= 140 | gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= 141 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 142 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 143 | -------------------------------------------------------------------------------- /internal/audit_writer/common_writer/common_writer.go: -------------------------------------------------------------------------------- 1 | package commonwriter 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "time" 9 | 10 | "github.com/RichardoC/kube-audit-rest/internal/common" 11 | "github.com/tidwall/sjson" 12 | ) 13 | 14 | func LogEvent(body []byte, writer io.Writer) { 15 | requestStr := string(body) 16 | updatedObj, err := addTimestamp(requestStr) 17 | if err != nil { 18 | common.Logger.Debugw("failed to add timestamp", "error", err) 19 | } 20 | 21 | // Compact the json for single line use regardless of request prettiness 22 | dst := &bytes.Buffer{} 23 | json.Compact(dst, []byte(updatedObj)) 24 | 25 | _, err = fmt.Fprintln(writer, dst) 26 | if err != nil { 27 | common.Logger.Error(err) 28 | } 29 | } 30 | 31 | func addTimestamp(requestBody string) (string, error) { 32 | currentTime := time.Now().Format(time.RFC3339Nano) 33 | return sjson.Set(requestBody, "requestReceivedTimestamp", currentTime) 34 | } 35 | -------------------------------------------------------------------------------- /internal/audit_writer/disk_writer/disk_writer.go: -------------------------------------------------------------------------------- 1 | package diskwriter 2 | 3 | import ( 4 | auditwritter "github.com/RichardoC/kube-audit-rest/internal/audit_writer" 5 | commonwriter "github.com/RichardoC/kube-audit-rest/internal/audit_writer/common_writer" 6 | "gopkg.in/natefinch/lumberjack.v2" 7 | ) 8 | 9 | type diskWritter struct { 10 | lumberjackLogger *lumberjack.Logger 11 | } 12 | 13 | func New(filename string, loggerMaxSize int, loggerMaxBackups int) auditwritter.AuditWritter { 14 | lumberjackLogger := &lumberjack.Logger{ 15 | Filename: filename, 16 | MaxSize: loggerMaxSize, 17 | MaxBackups: loggerMaxBackups, 18 | } 19 | return &diskWritter{lumberjackLogger: lumberjackLogger} 20 | } 21 | 22 | func (dw *diskWritter) LogEvent(body []byte) { 23 | commonwriter.LogEvent(body, dw.lumberjackLogger) 24 | } 25 | 26 | func (dw *diskWritter) Sync() {} 27 | -------------------------------------------------------------------------------- /internal/audit_writer/disk_writer/disk_writer_test.go: -------------------------------------------------------------------------------- 1 | package diskwriter_test 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | "os" 7 | "path" 8 | "testing" 9 | 10 | diskwriter "github.com/RichardoC/kube-audit-rest/internal/audit_writer/disk_writer" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func Test_WhenWritingEvent_ThenEventWritten(t *testing.T) { 15 | // Create a tmp dir where we'll put the log file for this test 16 | // This directory is automatically destroyed after the test has finished 17 | tmpDir := t.TempDir() 18 | fileLog := path.Join(tmpDir, "test_file.log") 19 | dw := diskwriter.New(fileLog, 1, 1) 20 | 21 | event := "{\"testEvent\": \"test\"}" 22 | dw.LogEvent([]byte(event)) 23 | 24 | // Check we can read the event we've just written 25 | byteContent, err := os.ReadFile(fileLog) 26 | if err != nil { 27 | log.Fatal(err) 28 | } 29 | var content map[string]string 30 | json.Unmarshal(byteContent, &content) 31 | assert.Equal(t, content["testEvent"], "test") 32 | } 33 | -------------------------------------------------------------------------------- /internal/audit_writer/iaudit_writer.go: -------------------------------------------------------------------------------- 1 | // Package auditwritter implements the interfaces to write events 2 | // to some medium (disk, stdout, etc.) 3 | package auditwritter 4 | 5 | //go:generate mockgen -package mymock -destination ../../mocks/audit_writer_mock.go github.com/RichardoC/kube-audit-rest/internal/audit_writer AuditWritter 6 | 7 | type AuditWritter interface { 8 | LogEvent(body []byte) 9 | Sync() 10 | } 11 | -------------------------------------------------------------------------------- /internal/audit_writer/stderr_writer/stderr_writer.go: -------------------------------------------------------------------------------- 1 | package stderrwriter 2 | 3 | import ( 4 | "log" 5 | 6 | auditwritter "github.com/RichardoC/kube-audit-rest/internal/audit_writer" 7 | commonwriter "github.com/RichardoC/kube-audit-rest/internal/audit_writer/common_writer" 8 | "go.uber.org/zap" 9 | "go.uber.org/zap/zapio" 10 | ) 11 | 12 | type stderrWritter struct { 13 | writer *zapio.Writer 14 | } 15 | 16 | func New() auditwritter.AuditWritter { 17 | lg, err := zap.NewProduction() 18 | if err != nil { 19 | log.Fatalf("can't initialize zap logger: %v", err) 20 | } 21 | writer := &zapio.Writer{Log: lg, Level: zap.InfoLevel} 22 | return &stderrWritter{writer: writer} 23 | } 24 | 25 | func (w *stderrWritter) LogEvent(body []byte) { 26 | commonwriter.LogEvent(body, w.writer) 27 | } 28 | 29 | func (w *stderrWritter) Sync() { 30 | w.writer.Sync() 31 | } 32 | -------------------------------------------------------------------------------- /internal/audit_writer/stderr_writer/stderr_writer_test.go: -------------------------------------------------------------------------------- 1 | package stderrwriter_test 2 | 3 | import ( 4 | "testing" 5 | 6 | stderrwriter "github.com/RichardoC/kube-audit-rest/internal/audit_writer/stderr_writer" 7 | ) 8 | 9 | func Test_WhenWritingEvent_ThenSucceeds(t *testing.T) { 10 | writer := stderrwriter.New() 11 | event := "{\"testEvent\": \"test\"}" 12 | writer.LogEvent([]byte(event)) 13 | } 14 | -------------------------------------------------------------------------------- /internal/common/logger.go: -------------------------------------------------------------------------------- 1 | // Package common provides some useful functions and objects that 2 | // may be used across all the components implemented in this project 3 | // One example is the default logger 4 | package common 5 | 6 | import ( 7 | "log" 8 | 9 | "go.uber.org/zap" 10 | ) 11 | 12 | type LogCfg int 13 | 14 | const ( 15 | Dbg LogCfg = iota 16 | Prod 17 | ) 18 | 19 | var Logger *zap.SugaredLogger = NewLogger(Dbg) 20 | 21 | func NewLogger(cfg LogCfg) *zap.SugaredLogger { 22 | var logger *zap.Logger 23 | var err error 24 | 25 | switch cfg { 26 | case Dbg: 27 | logger, err = zap.NewDevelopment() 28 | case Prod: 29 | logger, err = zap.NewProduction() 30 | default: 31 | log.Fatalf("Invalid log config %d", cfg) 32 | } 33 | 34 | if err != nil { 35 | log.Fatalf("Failed to initialize logger: %v", err) 36 | } 37 | 38 | return logger.Sugar() 39 | } 40 | 41 | func ConfigGlobalLogger(cfg LogCfg) { 42 | Logger = NewLogger(cfg) 43 | // Send standard logging to zap 44 | // ignoring undo as we don't want to undo this 45 | _ = zap.RedirectStdLog(Logger.Desugar()) 46 | } 47 | -------------------------------------------------------------------------------- /internal/common/logger_test.go: -------------------------------------------------------------------------------- 1 | package common_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/RichardoC/kube-audit-rest/internal/common" 7 | "github.com/stretchr/testify/assert" 8 | "go.uber.org/zap/zapcore" 9 | ) 10 | 11 | func TestConfigGlobalLogger(t *testing.T) { 12 | testCases := []struct { 13 | name string 14 | desiredLevel common.LogCfg 15 | expectedLevel zapcore.Level 16 | }{ 17 | {"dbg", common.Dbg, zapcore.DebugLevel}, 18 | {"prod", common.Prod, zapcore.InfoLevel}, 19 | } 20 | for _, tc := range testCases { 21 | t.Run(tc.name, func(t *testing.T) { 22 | common.ConfigGlobalLogger(tc.desiredLevel) 23 | assert.Equal(t, common.Logger.Level(), tc.expectedLevel) 24 | }) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /internal/event_processor/event_processor_impl/event_processor_impl.go: -------------------------------------------------------------------------------- 1 | package eventprocessorimpl 2 | 3 | import ( 4 | "html/template" 5 | "io" 6 | "net/http" 7 | 8 | auditwriter "github.com/RichardoC/kube-audit-rest/internal/audit_writer" 9 | "github.com/RichardoC/kube-audit-rest/internal/common" 10 | eventprocessor "github.com/RichardoC/kube-audit-rest/internal/event_processor" 11 | "github.com/RichardoC/kube-audit-rest/internal/metrics" 12 | "github.com/tidwall/gjson" 13 | ) 14 | 15 | // minimum viable response 16 | // https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#response 17 | const responseTemplate = `{ 18 | "apiVersion": "admission.k8s.io/v1", 19 | "kind": "AdmissionReview", 20 | "response": { 21 | "uid": "{{.}}", 22 | "allowed": true 23 | } 24 | }` 25 | 26 | type eventProcImpl struct { 27 | validReqProc metrics.Counter 28 | totalReq metrics.Counter 29 | eventWritter auditwriter.AuditWritter 30 | responseTemplate template.Template 31 | } 32 | 33 | func New(eventWritter auditwriter.AuditWritter, metricsServer metrics.MetricsServer) (eventprocessor.EventProcessor, error) { 34 | validReqProc := metricsServer.CreateAndRegisterCounter( 35 | "kube_audit_rest_valid_requests_processed_total", 36 | "Total number of valid requests processed", 37 | ) 38 | totalReq := metricsServer.CreateAndRegisterCounter( 39 | "kube_audit_rest_http_requests_total", 40 | "Total number of requests to kube-audit-rest", 41 | ) 42 | tmpl, err := template.New("name").Parse(responseTemplate) 43 | 44 | if err != nil { 45 | return &eventProcImpl{}, err 46 | } 47 | 48 | return &eventProcImpl{ 49 | validReqProc: validReqProc, 50 | totalReq: totalReq, 51 | eventWritter: eventWritter, 52 | responseTemplate: *tmpl, 53 | }, nil 54 | } 55 | 56 | func (ep *eventProcImpl) ProcessEvent(w http.ResponseWriter, r *http.Request) { 57 | ep.totalReq.Inc() 58 | common.Logger.Debugw("Got request", "request", r) 59 | var body []byte 60 | // Don't bother with any logic if there is no request 61 | if r.Body != nil { 62 | if data, err := io.ReadAll(r.Body); err == nil { 63 | common.Logger.Debugw("Got this body", "body", string(data)) 64 | body = data 65 | } else { 66 | common.Logger.Debugw(err.Error(), "body", r.Body) 67 | w.Header().Set("error", "Failed to read body") 68 | w.WriteHeader(http.StatusBadRequest) 69 | return 70 | } 71 | } else { 72 | common.Logger.Debugw("No body provided") 73 | w.WriteHeader(http.StatusBadRequest) 74 | w.Header().Set("error", "No body provided") 75 | return 76 | } 77 | 78 | // verify the content type is accurate 79 | contentType := r.Header.Get("Content-Type") 80 | if contentType != "application/json" { 81 | common.Logger.Debugw("expect application/json", "contentType", contentType) 82 | w.Header().Set("error", "expect contentType application/json") 83 | w.WriteHeader(http.StatusBadRequest) 84 | return 85 | } 86 | 87 | if !gjson.ValidBytes(body) { 88 | common.Logger.Debugw("invalid json", "body", body) 89 | w.Header().Set("error", "invalid json") 90 | w.WriteHeader(http.StatusBadRequest) 91 | return 92 | } 93 | requestUid := gjson.GetBytes(body, "request.uid").Str 94 | if requestUid == "" { 95 | common.Logger.Debugln("failed to find request uid") 96 | w.Header().Set("error", "uid not provided") 97 | w.WriteHeader(http.StatusBadRequest) 98 | return 99 | } 100 | 101 | // Sychronous so that slower writes *do* slow our responses 102 | ep.eventWritter.LogEvent(body) 103 | 104 | // Record we processed a valid request 105 | ep.validReqProc.Inc() 106 | 107 | // Template the uid into our default approval and finish up 108 | 109 | ep.responseTemplate.Execute(w, requestUid) 110 | 111 | // fmt.Fprintf(w, responseTemplate, requestUid) 112 | } 113 | -------------------------------------------------------------------------------- /internal/event_processor/event_processor_impl/event_processor_impl_test.go: -------------------------------------------------------------------------------- 1 | package eventprocessorimpl_test 2 | 3 | import ( 4 | "net/http" 5 | "strings" 6 | "testing" 7 | 8 | eventprocessor "github.com/RichardoC/kube-audit-rest/internal/event_processor" 9 | eventprocessorimpl "github.com/RichardoC/kube-audit-rest/internal/event_processor/event_processor_impl" 10 | mymock "github.com/RichardoC/kube-audit-rest/mocks" 11 | "github.com/golang/mock/gomock" 12 | ) 13 | 14 | type mockResponseWriter struct{} 15 | 16 | func (mrw *mockResponseWriter) Header() http.Header { 17 | return http.Header{} 18 | } 19 | 20 | func (mrw *mockResponseWriter) Write([]byte) (int, error) { 21 | return 1, nil 22 | } 23 | 24 | func (mrw *mockResponseWriter) WriteHeader(statusCode int) {} 25 | 26 | var correctBodyRequest string = ` 27 | { 28 | "request": { 29 | "uid": "test-uid" 30 | } 31 | } 32 | ` 33 | 34 | func setup(t *testing.T) (*mymock.MockAuditWritter, *mymock.MockMetricsServer) { 35 | ctrl := gomock.NewController(t) 36 | aw := mymock.NewMockAuditWritter(ctrl) 37 | ms := mymock.NewMockMetricsServer(ctrl) 38 | counter := mymock.NewMockCounter(ctrl) 39 | counter.EXPECT().Inc().AnyTimes() 40 | ms.EXPECT().CreateAndRegisterCounter(gomock.Any(), gomock.Any()).Return(counter).Times(2) 41 | return aw, ms 42 | } 43 | 44 | func sendRequest(ep eventprocessor.EventProcessor, header map[string][]string, body string) { 45 | reader := strings.NewReader(body) 46 | req, _ := http.NewRequest("POST", "localhost:80", reader) 47 | if header != nil { 48 | req.Header = header 49 | } 50 | respWriter := &mockResponseWriter{} 51 | ep.ProcessEvent(respWriter, req) 52 | } 53 | 54 | func Test_WhenRequestWellFormatted_ThenResponseSent(t *testing.T) { 55 | header := make(map[string][]string) 56 | header["Content-Type"] = []string{"application/json"} 57 | 58 | aw, ms := setup(t) 59 | aw.EXPECT().LogEvent([]byte(correctBodyRequest)) 60 | ep, err := eventprocessorimpl.New(aw, ms) 61 | if err != nil { 62 | t.Errorf("creating event processor failed with : %s", err) 63 | } 64 | 65 | sendRequest(ep, header, correctBodyRequest) 66 | } 67 | 68 | func Test_WhenBadHeader_ThenNoEventLogged(t *testing.T) { 69 | header := make(map[string][]string) 70 | 71 | aw, ms := setup(t) 72 | ep, err := eventprocessorimpl.New(aw, ms) 73 | if err != nil { 74 | t.Errorf("creating event processor failed with : %s", err) 75 | } 76 | 77 | sendRequest(ep, header, correctBodyRequest) 78 | } 79 | 80 | func Test_WhenInvalidJsonBody_ThenNoEventLogged(t *testing.T) { 81 | header := make(map[string][]string) 82 | header["Content-Type"] = []string{"application/json"} 83 | 84 | aw, ms := setup(t) 85 | ep, err := eventprocessorimpl.New(aw, ms) 86 | 87 | if err != nil { 88 | t.Errorf("creating event processor failed with : %s", err) 89 | } 90 | 91 | sendRequest(ep, header, "") 92 | } 93 | -------------------------------------------------------------------------------- /internal/event_processor/ievent_processor.go: -------------------------------------------------------------------------------- 1 | // Package eventprocessor provides the interfaces to process and 2 | // reply to http requests 3 | package eventprocessor 4 | 5 | //go:generate mockgen -package mymock -destination ../../mocks/event_processor_mock.go github.com/RichardoC/kube-audit-rest/internal/event_processor EventProcessor 6 | 7 | import "net/http" 8 | 9 | type EventProcessor interface { 10 | ProcessEvent(http.ResponseWriter, *http.Request) 11 | } 12 | -------------------------------------------------------------------------------- /internal/http_listener/ihttp_listener.go: -------------------------------------------------------------------------------- 1 | // Package httplistener provides the interfaces to create an http server 2 | // that listens on a specific endpoint 3 | package httplistener 4 | 5 | //go:generate mockgen -package mymock -destination ../../mocks/http_listener_mock.go github.com/RichardoC/kube-audit-rest/internal/http_listener HttpListener 6 | 7 | type HttpListener interface { 8 | Start() 9 | Stop() 10 | } 11 | -------------------------------------------------------------------------------- /internal/http_listener/log_request_listener/log_request_listener.go: -------------------------------------------------------------------------------- 1 | package logrequestlistener 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "time" 8 | 9 | "github.com/RichardoC/kube-audit-rest/internal/common" 10 | eventprocessor "github.com/RichardoC/kube-audit-rest/internal/event_processor" 11 | httplistener "github.com/RichardoC/kube-audit-rest/internal/http_listener" 12 | ) 13 | 14 | type logRequestListener struct { 15 | server *http.Server 16 | certFilename string 17 | certKeyFilename string 18 | } 19 | 20 | func New(port int, certFilename string, certKeyFilename string, eProc eventprocessor.EventProcessor) httplistener.HttpListener { 21 | router := http.NewServeMux() 22 | router.HandleFunc("POST /log-request", eProc.ProcessEvent) 23 | 24 | addr := fmt.Sprintf(":%d", port) 25 | server := &http.Server{ 26 | Addr: addr, 27 | Handler: router, 28 | ReadTimeout: 5 * time.Second, 29 | WriteTimeout: 10 * time.Second, 30 | IdleTimeout: 15 * time.Second, 31 | } 32 | 33 | return &logRequestListener{ 34 | server: server, 35 | certFilename: certFilename, 36 | certKeyFilename: certKeyFilename, 37 | } 38 | } 39 | 40 | func (lrl *logRequestListener) Start() { 41 | common.Logger.Infow("Starting server", "addr", lrl.server.Addr) 42 | if err := lrl.server.ListenAndServeTLS(lrl.certFilename, lrl.certKeyFilename); err != nil && err != http.ErrServerClosed { 43 | common.Logger.Fatalw("Failed to start server", "error", err, "addr", lrl.server.Addr) 44 | } 45 | } 46 | 47 | func (lrl *logRequestListener) Stop() { 48 | common.Logger.Warnw("Log Request Listener is shutting down...") 49 | ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) 50 | defer cancel() 51 | 52 | lrl.server.SetKeepAlivesEnabled(false) 53 | if err := lrl.server.Shutdown(ctx); err != nil { 54 | common.Logger.Fatalf("Could not gracefully shutdown the server: %v\n", err) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /internal/http_listener/log_request_listener/log_request_listener_test.go: -------------------------------------------------------------------------------- 1 | package logrequestlistener_test 2 | 3 | import ( 4 | "testing" 5 | 6 | eventprocessor "github.com/RichardoC/kube-audit-rest/internal/event_processor" 7 | logrequestlistener "github.com/RichardoC/kube-audit-rest/internal/http_listener/log_request_listener" 8 | mymock "github.com/RichardoC/kube-audit-rest/mocks" 9 | "github.com/golang/mock/gomock" 10 | ) 11 | 12 | func setup(t *testing.T) eventprocessor.EventProcessor { 13 | ctrl := gomock.NewController(t) 14 | mockEvProc := mymock.NewMockEventProcessor(ctrl) 15 | return mockEvProc 16 | } 17 | 18 | func Test_WhenListenerNotStarted_ThenStopSucceeds(t *testing.T) { 19 | mockEvProc := setup(t) 20 | lrl := logrequestlistener.New(1234, "", "", mockEvProc) 21 | lrl.Stop() 22 | } 23 | 24 | // Testing the Start and Stop of the server is difficult because we need 25 | // to generate temporary TLS self-signed certificates and the overall 26 | // test would be much longer than the code it's trying to test 27 | -------------------------------------------------------------------------------- /internal/metrics/imetrics.go: -------------------------------------------------------------------------------- 1 | // Package metrics provides the interfaces to interact with a metrics server 2 | package metrics 3 | 4 | //go:generate mockgen -package mymock -destination ../../mocks/metrics_mock.go github.com/RichardoC/kube-audit-rest/internal/metrics Counter,MetricsServer 5 | 6 | type Counter interface { 7 | Inc() 8 | } 9 | 10 | // A server that exposes an endpoint where it publishes metrics 11 | type MetricsServer interface { 12 | // Start the server 13 | Start() 14 | // Stop the server gracefully 15 | Stop() 16 | // Creates the counter, registers it and returns it 17 | CreateAndRegisterCounter(name string, help string) Counter 18 | } 19 | -------------------------------------------------------------------------------- /internal/metrics/prometheus_metrics/prometheus_metrics.go: -------------------------------------------------------------------------------- 1 | package prometheusmetrics 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "time" 8 | 9 | "github.com/RichardoC/kube-audit-rest/internal/common" 10 | "github.com/RichardoC/kube-audit-rest/internal/metrics" 11 | "github.com/prometheus/client_golang/prometheus" 12 | "github.com/prometheus/client_golang/prometheus/promhttp" 13 | ) 14 | 15 | type prometheusMetricsServer struct { 16 | reg prometheus.Registerer 17 | server *http.Server 18 | } 19 | 20 | func New(port int) metrics.MetricsServer { 21 | reg := prometheus.NewRegistry() 22 | 23 | router := http.NewServeMux() 24 | router.Handle("/metrics", promhttp.HandlerFor(reg, promhttp.HandlerOpts{Registry: reg})) 25 | 26 | addr := fmt.Sprintf(":%d", port) 27 | server := &http.Server{ 28 | Addr: addr, 29 | Handler: router, 30 | ReadTimeout: 5 * time.Second, 31 | WriteTimeout: 10 * time.Second, 32 | IdleTimeout: 15 * time.Second, 33 | } 34 | 35 | metricsServer := &prometheusMetricsServer{reg: reg, server: server} 36 | return metricsServer 37 | } 38 | 39 | func (ms *prometheusMetricsServer) CreateAndRegisterCounter(name string, help string) metrics.Counter { 40 | counter := prometheus.NewCounter(prometheus.CounterOpts{Name: name, Help: help}) 41 | ms.reg.MustRegister(counter) 42 | return counter 43 | } 44 | 45 | func (ms *prometheusMetricsServer) Start() { 46 | common.Logger.Infow("Starting server", "addr", ms.server.Addr) 47 | if err := ms.server.ListenAndServe(); err != nil && err != http.ErrServerClosed { 48 | common.Logger.Fatalw("Failed to start the Prometheus metrics server", "error", err, "addr", ms.server.Addr) 49 | } 50 | } 51 | 52 | func (ms *prometheusMetricsServer) Stop() { 53 | defer common.Logger.Sync() 54 | common.Logger.Warnw("Prometheus Metrics Server is shutting down...") 55 | ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) 56 | defer cancel() 57 | 58 | ms.server.SetKeepAlivesEnabled(false) 59 | if err := ms.server.Shutdown(ctx); err != nil { 60 | common.Logger.Fatalf("Could not gracefully shutdown the server: %v\n", err) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /internal/metrics/prometheus_metrics/prometheus_metrics_test.go: -------------------------------------------------------------------------------- 1 | package prometheusmetrics_test 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "log" 7 | "net" 8 | "net/http" 9 | "strings" 10 | "testing" 11 | "time" 12 | 13 | prometheusmetrics "github.com/RichardoC/kube-audit-rest/internal/metrics/prometheus_metrics" 14 | "github.com/stretchr/testify/assert" 15 | ) 16 | 17 | var REQUEST_MAX_RETRIES int = 10 18 | 19 | func getFreePort() int { 20 | listener, err := net.Listen("tcp", ":0") 21 | if err != nil { 22 | panic(err) 23 | } 24 | port := listener.Addr().(*net.TCPAddr).Port 25 | listener.Close() 26 | return port 27 | } 28 | 29 | func Test_WhenMetricsNotStarted_ThenStopSucceeds(t *testing.T) { 30 | ms := prometheusmetrics.New(1234) 31 | ms.Stop() 32 | } 33 | 34 | func Test_WhenCounterCreated_ThenItCanBeIncremented(t *testing.T) { 35 | ms := prometheusmetrics.New(1234) 36 | counter := ms.CreateAndRegisterCounter("test_counter", "This counter is for test purposes") 37 | counter.Inc() 38 | } 39 | 40 | func Test_WhenServerStarted_ThenServesRequests(t *testing.T) { 41 | port := getFreePort() 42 | ms := prometheusmetrics.New(port) 43 | go ms.Start() 44 | counter := ms.CreateAndRegisterCounter("test_counter", "This counter is for test purposes") 45 | counter.Inc() 46 | 47 | // Repeat the request up to 10 times 48 | // We need that since we may start doing the request before the server is fully up and running 49 | requestURL := fmt.Sprintf("http://localhost:%d/metrics", port) 50 | res, err := http.Get(requestURL) 51 | for i := 0; i < REQUEST_MAX_RETRIES; i++ { 52 | if err == nil { 53 | break 54 | } 55 | time.Sleep(100 * time.Millisecond) 56 | res, err = http.Get(requestURL) 57 | } 58 | if err != nil { 59 | log.Fatalf("Error making http request: %s\n", err) 60 | } 61 | 62 | // Read http body 63 | defer res.Body.Close() 64 | body, err := io.ReadAll(res.Body) 65 | if err != nil { 66 | log.Fatalf("Failed reading the response. %v", err) 67 | } 68 | strBody := string(body) 69 | 70 | assert.Equal(t, res.StatusCode, 200) 71 | assert.True(t, strings.Contains(strBody, "test_counter 1")) 72 | 73 | ms.Stop() 74 | } 75 | -------------------------------------------------------------------------------- /k8s/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | labels: 5 | app: kube-audit-rest 6 | name: kube-audit-rest 7 | namespace: kube-audit-rest 8 | spec: 9 | progressDeadlineSeconds: 600 10 | replicas: 1 # Can be HA 11 | revisionHistoryLimit: 10 12 | selector: 13 | matchLabels: 14 | app: kube-audit-rest 15 | strategy: 16 | rollingUpdate: 17 | maxSurge: 25% 18 | maxUnavailable: 25% 19 | type: RollingUpdate 20 | template: 21 | metadata: 22 | labels: 23 | app: kube-audit-rest 24 | spec: 25 | automountServiceAccountToken: false 26 | containers: 27 | - image: "ghcr.io/richardoc/kube-audit-rest:${COMMIT}-distroless" 28 | imagePullPolicy: IfNotPresent 29 | name: kube-audit-rest 30 | command: # Adding example for overrides 31 | - "/kube-audit-rest" 32 | args: 33 | - "--logger-max-backups=1" # Example of reducing number of files stored 34 | resources: 35 | requests: 36 | cpu: "2m" 37 | memory: "10Mi" 38 | limits: 39 | cpu: "1" 40 | memory: "32Mi" 41 | ports: 42 | - containerPort: 9090 43 | protocol: TCP 44 | name: https 45 | - containerPort: 55555 46 | protocol: TCP 47 | name: metrics 48 | volumeMounts: 49 | - name: certs 50 | mountPath: "/etc/tls" 51 | readOnly: true 52 | - name: tmp 53 | mountPath: "/tmp" 54 | securityContext: 55 | allowPrivilegeEscalation: false 56 | readOnlyRootFilesystem: true 57 | capabilities: 58 | drop: 59 | - ALL 60 | securityContext: 61 | runAsUser: 255999 # Already true in the container, but does no harm to be explicit 62 | runAsGroup: 255999 63 | fsGroup: 255999 64 | restartPolicy: Always 65 | terminationGracePeriodSeconds: 30 66 | volumes: 67 | - name: certs 68 | secret: 69 | secretName: kube-audit-rest 70 | - name: tmp 71 | emptyDir: 72 | sizeLimit: 2Gi # Based on default of 3 files at 500Mi 73 | -------------------------------------------------------------------------------- /k8s/namespace.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: kube-audit-rest 5 | -------------------------------------------------------------------------------- /k8s/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | app: kube-audit-rest 6 | name: kube-audit-rest 7 | namespace: kube-audit-rest 8 | spec: 9 | ports: 10 | - name: https 11 | port: 443 12 | protocol: TCP 13 | targetPort: https 14 | - name: metrics 15 | port: 55555 16 | protocol: TCP 17 | targetPort: metrics 18 | selector: 19 | app: kube-audit-rest 20 | sessionAffinity: None 21 | type: ClusterIP 22 | 23 | -------------------------------------------------------------------------------- /k8s/webhook.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: admissionregistration.k8s.io/v1 3 | kind: ValidatingWebhookConfiguration # Can also be a MutatingWebhookConfiguration if required 4 | metadata: 5 | name: kube-audit-rest 6 | labels: 7 | app: kube-audit-rest 8 | webhooks: 9 | - name: kube-audit-rest.kube-audit-rest.svc.cluster.local 10 | failurePolicy: Ignore # Don't block requests if auditing fails 11 | timeoutSeconds: 1 # To prevent excessively slowing everything 12 | sideEffects: None 13 | clientConfig: 14 | service: 15 | name: kube-audit-rest 16 | namespace: kube-audit-rest 17 | path: "/log-request" 18 | caBundle: "$CABUNDLEB64" # To be replaced 19 | rules: # To be reduced as needed 20 | - operations: [ "*" ] 21 | apiGroups: ["*"] 22 | apiVersions: ["*"] 23 | resources: ["*/*"] 24 | scope: "*" 25 | admissionReviewVersions: ["v1"] -------------------------------------------------------------------------------- /mocks/audit_writer_mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: github.com/RichardoC/kube-audit-rest/internal/audit_writer (interfaces: AuditWritter) 3 | 4 | // Package mymock is a generated GoMock package. 5 | package mymock 6 | 7 | import ( 8 | reflect "reflect" 9 | 10 | gomock "github.com/golang/mock/gomock" 11 | ) 12 | 13 | // MockAuditWritter is a mock of AuditWritter interface. 14 | type MockAuditWritter struct { 15 | ctrl *gomock.Controller 16 | recorder *MockAuditWritterMockRecorder 17 | } 18 | 19 | // MockAuditWritterMockRecorder is the mock recorder for MockAuditWritter. 20 | type MockAuditWritterMockRecorder struct { 21 | mock *MockAuditWritter 22 | } 23 | 24 | // NewMockAuditWritter creates a new mock instance. 25 | func NewMockAuditWritter(ctrl *gomock.Controller) *MockAuditWritter { 26 | mock := &MockAuditWritter{ctrl: ctrl} 27 | mock.recorder = &MockAuditWritterMockRecorder{mock} 28 | return mock 29 | } 30 | 31 | // EXPECT returns an object that allows the caller to indicate expected use. 32 | func (m *MockAuditWritter) EXPECT() *MockAuditWritterMockRecorder { 33 | return m.recorder 34 | } 35 | 36 | // LogEvent mocks base method. 37 | func (m *MockAuditWritter) LogEvent(arg0 []byte) { 38 | m.ctrl.T.Helper() 39 | m.ctrl.Call(m, "LogEvent", arg0) 40 | } 41 | 42 | // LogEvent indicates an expected call of LogEvent. 43 | func (mr *MockAuditWritterMockRecorder) LogEvent(arg0 interface{}) *gomock.Call { 44 | mr.mock.ctrl.T.Helper() 45 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LogEvent", reflect.TypeOf((*MockAuditWritter)(nil).LogEvent), arg0) 46 | } 47 | 48 | // Sync mocks base method. 49 | func (m *MockAuditWritter) Sync() { 50 | m.ctrl.T.Helper() 51 | m.ctrl.Call(m, "Sync") 52 | } 53 | 54 | // Sync indicates an expected call of Sync. 55 | func (mr *MockAuditWritterMockRecorder) Sync() *gomock.Call { 56 | mr.mock.ctrl.T.Helper() 57 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Sync", reflect.TypeOf((*MockAuditWritter)(nil).Sync)) 58 | } 59 | -------------------------------------------------------------------------------- /mocks/event_processor_mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: github.com/RichardoC/kube-audit-rest/internal/event_processor (interfaces: EventProcessor) 3 | 4 | // Package mymock is a generated GoMock package. 5 | package mymock 6 | 7 | import ( 8 | http "net/http" 9 | reflect "reflect" 10 | 11 | gomock "github.com/golang/mock/gomock" 12 | ) 13 | 14 | // MockEventProcessor is a mock of EventProcessor interface. 15 | type MockEventProcessor struct { 16 | ctrl *gomock.Controller 17 | recorder *MockEventProcessorMockRecorder 18 | } 19 | 20 | // MockEventProcessorMockRecorder is the mock recorder for MockEventProcessor. 21 | type MockEventProcessorMockRecorder struct { 22 | mock *MockEventProcessor 23 | } 24 | 25 | // NewMockEventProcessor creates a new mock instance. 26 | func NewMockEventProcessor(ctrl *gomock.Controller) *MockEventProcessor { 27 | mock := &MockEventProcessor{ctrl: ctrl} 28 | mock.recorder = &MockEventProcessorMockRecorder{mock} 29 | return mock 30 | } 31 | 32 | // EXPECT returns an object that allows the caller to indicate expected use. 33 | func (m *MockEventProcessor) EXPECT() *MockEventProcessorMockRecorder { 34 | return m.recorder 35 | } 36 | 37 | // ProcessEvent mocks base method. 38 | func (m *MockEventProcessor) ProcessEvent(arg0 http.ResponseWriter, arg1 *http.Request) { 39 | m.ctrl.T.Helper() 40 | m.ctrl.Call(m, "ProcessEvent", arg0, arg1) 41 | } 42 | 43 | // ProcessEvent indicates an expected call of ProcessEvent. 44 | func (mr *MockEventProcessorMockRecorder) ProcessEvent(arg0, arg1 interface{}) *gomock.Call { 45 | mr.mock.ctrl.T.Helper() 46 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProcessEvent", reflect.TypeOf((*MockEventProcessor)(nil).ProcessEvent), arg0, arg1) 47 | } 48 | -------------------------------------------------------------------------------- /mocks/http_listener_mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: github.com/RichardoC/kube-audit-rest/internal/http_listener (interfaces: HttpListener) 3 | 4 | // Package mymock is a generated GoMock package. 5 | package mymock 6 | 7 | import ( 8 | reflect "reflect" 9 | 10 | gomock "github.com/golang/mock/gomock" 11 | ) 12 | 13 | // MockHttpListener is a mock of HttpListener interface. 14 | type MockHttpListener struct { 15 | ctrl *gomock.Controller 16 | recorder *MockHttpListenerMockRecorder 17 | } 18 | 19 | // MockHttpListenerMockRecorder is the mock recorder for MockHttpListener. 20 | type MockHttpListenerMockRecorder struct { 21 | mock *MockHttpListener 22 | } 23 | 24 | // NewMockHttpListener creates a new mock instance. 25 | func NewMockHttpListener(ctrl *gomock.Controller) *MockHttpListener { 26 | mock := &MockHttpListener{ctrl: ctrl} 27 | mock.recorder = &MockHttpListenerMockRecorder{mock} 28 | return mock 29 | } 30 | 31 | // EXPECT returns an object that allows the caller to indicate expected use. 32 | func (m *MockHttpListener) EXPECT() *MockHttpListenerMockRecorder { 33 | return m.recorder 34 | } 35 | 36 | // Start mocks base method. 37 | func (m *MockHttpListener) Start() { 38 | m.ctrl.T.Helper() 39 | m.ctrl.Call(m, "Start") 40 | } 41 | 42 | // Start indicates an expected call of Start. 43 | func (mr *MockHttpListenerMockRecorder) Start() *gomock.Call { 44 | mr.mock.ctrl.T.Helper() 45 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Start", reflect.TypeOf((*MockHttpListener)(nil).Start)) 46 | } 47 | 48 | // Stop mocks base method. 49 | func (m *MockHttpListener) Stop() { 50 | m.ctrl.T.Helper() 51 | m.ctrl.Call(m, "Stop") 52 | } 53 | 54 | // Stop indicates an expected call of Stop. 55 | func (mr *MockHttpListenerMockRecorder) Stop() *gomock.Call { 56 | mr.mock.ctrl.T.Helper() 57 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Stop", reflect.TypeOf((*MockHttpListener)(nil).Stop)) 58 | } 59 | -------------------------------------------------------------------------------- /mocks/metrics_mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: github.com/RichardoC/kube-audit-rest/internal/metrics (interfaces: Counter,MetricsServer) 3 | 4 | // Package mymock is a generated GoMock package. 5 | package mymock 6 | 7 | import ( 8 | reflect "reflect" 9 | 10 | metrics "github.com/RichardoC/kube-audit-rest/internal/metrics" 11 | gomock "github.com/golang/mock/gomock" 12 | ) 13 | 14 | // MockCounter is a mock of Counter interface. 15 | type MockCounter struct { 16 | ctrl *gomock.Controller 17 | recorder *MockCounterMockRecorder 18 | } 19 | 20 | // MockCounterMockRecorder is the mock recorder for MockCounter. 21 | type MockCounterMockRecorder struct { 22 | mock *MockCounter 23 | } 24 | 25 | // NewMockCounter creates a new mock instance. 26 | func NewMockCounter(ctrl *gomock.Controller) *MockCounter { 27 | mock := &MockCounter{ctrl: ctrl} 28 | mock.recorder = &MockCounterMockRecorder{mock} 29 | return mock 30 | } 31 | 32 | // EXPECT returns an object that allows the caller to indicate expected use. 33 | func (m *MockCounter) EXPECT() *MockCounterMockRecorder { 34 | return m.recorder 35 | } 36 | 37 | // Inc mocks base method. 38 | func (m *MockCounter) Inc() { 39 | m.ctrl.T.Helper() 40 | m.ctrl.Call(m, "Inc") 41 | } 42 | 43 | // Inc indicates an expected call of Inc. 44 | func (mr *MockCounterMockRecorder) Inc() *gomock.Call { 45 | mr.mock.ctrl.T.Helper() 46 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Inc", reflect.TypeOf((*MockCounter)(nil).Inc)) 47 | } 48 | 49 | // MockMetricsServer is a mock of MetricsServer interface. 50 | type MockMetricsServer struct { 51 | ctrl *gomock.Controller 52 | recorder *MockMetricsServerMockRecorder 53 | } 54 | 55 | // MockMetricsServerMockRecorder is the mock recorder for MockMetricsServer. 56 | type MockMetricsServerMockRecorder struct { 57 | mock *MockMetricsServer 58 | } 59 | 60 | // NewMockMetricsServer creates a new mock instance. 61 | func NewMockMetricsServer(ctrl *gomock.Controller) *MockMetricsServer { 62 | mock := &MockMetricsServer{ctrl: ctrl} 63 | mock.recorder = &MockMetricsServerMockRecorder{mock} 64 | return mock 65 | } 66 | 67 | // EXPECT returns an object that allows the caller to indicate expected use. 68 | func (m *MockMetricsServer) EXPECT() *MockMetricsServerMockRecorder { 69 | return m.recorder 70 | } 71 | 72 | // CreateAndRegisterCounter mocks base method. 73 | func (m *MockMetricsServer) CreateAndRegisterCounter(arg0, arg1 string) metrics.Counter { 74 | m.ctrl.T.Helper() 75 | ret := m.ctrl.Call(m, "CreateAndRegisterCounter", arg0, arg1) 76 | ret0, _ := ret[0].(metrics.Counter) 77 | return ret0 78 | } 79 | 80 | // CreateAndRegisterCounter indicates an expected call of CreateAndRegisterCounter. 81 | func (mr *MockMetricsServerMockRecorder) CreateAndRegisterCounter(arg0, arg1 interface{}) *gomock.Call { 82 | mr.mock.ctrl.T.Helper() 83 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateAndRegisterCounter", reflect.TypeOf((*MockMetricsServer)(nil).CreateAndRegisterCounter), arg0, arg1) 84 | } 85 | 86 | // Start mocks base method. 87 | func (m *MockMetricsServer) Start() { 88 | m.ctrl.T.Helper() 89 | m.ctrl.Call(m, "Start") 90 | } 91 | 92 | // Start indicates an expected call of Start. 93 | func (mr *MockMetricsServerMockRecorder) Start() *gomock.Call { 94 | mr.mock.ctrl.T.Helper() 95 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Start", reflect.TypeOf((*MockMetricsServer)(nil).Start)) 96 | } 97 | 98 | // Stop mocks base method. 99 | func (m *MockMetricsServer) Stop() { 100 | m.ctrl.T.Helper() 101 | m.ctrl.Call(m, "Stop") 102 | } 103 | 104 | // Stop indicates an expected call of Stop. 105 | func (mr *MockMetricsServerMockRecorder) Stop() *gomock.Call { 106 | mr.mock.ctrl.T.Helper() 107 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Stop", reflect.TypeOf((*MockMetricsServer)(nil).Stop)) 108 | } 109 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:recommended", 5 | "docker:enableMajor" 6 | ], 7 | "automergeType": "pr", 8 | "separateMinorPatch": true, 9 | "automerge": true, 10 | "prCreation": "immediate" 11 | } 12 | -------------------------------------------------------------------------------- /testing/ca.cnf: -------------------------------------------------------------------------------- 1 | [req] 2 | default_bits = 2048 3 | prompt = no 4 | default_md = sha256 5 | distinguished_name = dn 6 | x509_extensions = v3_req 7 | 8 | CA_default = [ca] 9 | 10 | [dn] 11 | C=GB 12 | OU=Engineering 13 | emailAddress=admin@localhost 14 | CN = ca.local 15 | 16 | [ca] 17 | copy_extensions = copy 18 | 19 | 20 | [ v3_req ] 21 | # Extensions for a typical CA (`man x509v3_config`). 22 | subjectKeyIdentifier = hash 23 | authorityKeyIdentifier = keyid:always,issuer 24 | basicConstraints = critical, CA:true 25 | keyUsage = critical, digitalSignature, cRLSign, keyCertSign -------------------------------------------------------------------------------- /testing/certs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eux 4 | 5 | # Default to root if no .git missing 6 | ROOT=$(git rev-parse --show-toplevel || echo '.' ) 7 | 8 | cd $ROOT 9 | 10 | CONF_DIR=$(pwd)/testing 11 | 12 | TMP=$(pwd)/tmp 13 | echo $TMP 14 | cd $TMP 15 | 16 | openssl genrsa -out rootCA.key 2048 &> /dev/null 17 | 18 | openssl req -x509 -new -nodes -key rootCA.key -sha256 -days 1460 -out rootCA.pem -config $CONF_DIR/ca.cnf &> /dev/null 19 | 20 | openssl req -new -nodes -sha256 -out server.csr -newkey rsa:2048 -keyout server.key -config $CONF_DIR/server.csr.cnf &> /dev/null 21 | 22 | openssl x509 -req -in server.csr -CA rootCA.pem -CAkey rootCA.key -CAcreateserial -out server.crt -days 500 -sha256 -extensions v3_req -extfile $CONF_DIR/server.csr.cnf &> /dev/null 23 | -------------------------------------------------------------------------------- /testing/cleanup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | 4 | # Default to root if no .git missing 5 | ROOT=$(git rev-parse --show-toplevel || echo '.' ) 6 | 7 | cd $ROOT 8 | 9 | rm -rf tmp 10 | 11 | kubectl delete -f k8s/webhook.yaml 12 | kubectl delete -f k8s/namespace.yaml 13 | 14 | rm kube-audit-rest 15 | -------------------------------------------------------------------------------- /testing/locally/local-testing.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euo pipefail 4 | 5 | # Default to root if no .git missing 6 | ROOT=$(git rev-parse --show-toplevel || echo '.' ) 7 | 8 | cd $ROOT 9 | 10 | # Ensure tmp folder exists 11 | mkdir -p tmp/ 12 | 13 | # Ensure local certs exist 14 | testing/certs.sh 15 | 16 | 17 | echo "Removing old test file" 18 | rm -f tmp/kube-audit-rest.log; 19 | 20 | echo "Removing old servers if still running" 21 | pkill kube-audit-rest || echo "No old server running" 22 | 23 | # Use a random unused ephemeral port 24 | function ephemeral_port() { 25 | local -r min=49152 max=65535 26 | 27 | while true; do 28 | local port=$((RANDOM % (max - min + 1) + min)) 29 | if ! (echo >/dev/tcp/127.0.0.1/$port) >/dev/null 2>&1; then 30 | echo "$port" 31 | break 32 | fi 33 | done 34 | } 35 | 36 | export SERVER_PORT="$(ephemeral_port)" 37 | export METRICS_PORT="$(ephemeral_port)" 38 | 39 | if [[ "$(uname -m)" == 'x86_64' ]] 40 | then 41 | # Run current server with those local certs on port $SERVER_PORT 42 | # With race detection on x86_64 43 | # Redirecting output to confirm standard library logs redirected to 44 | # structured logger to prevent repeats of #31 45 | echo "Also doing race detection" 46 | go run -race ./cmd/kube-audit-rest/main.go --cert-filename=./tmp/server.crt --cert-key-filename=./tmp/server.key \ 47 | --server-port="$SERVER_PORT" --metrics-port="$METRICS_PORT" --logger-filename=./tmp/kube-audit-rest.log > ./tmp/kube-audit-rest-output.log 2>&1 & 48 | else 49 | # Run current server with those local certs on port $SERVER_PORT 50 | go run ./cmd/kube-audit-rest/main.go --cert-filename=./tmp/server.crt --cert-key-filename=./tmp/server.key \ 51 | --server-port="$SERVER_PORT" --metrics-port="$METRICS_PORT" --logger-filename=./tmp/kube-audit-rest.log > ./tmp/kube-audit-rest-output.log 2>&1 & 52 | fi 53 | KUBE_AUDIT_PID=$! 54 | 55 | # Wait for server to run 56 | while ! nc -z localhost "$SERVER_PORT"; do 57 | sleep 1 # wait for 1/10 of the second before check again 58 | done 59 | 60 | go run testing/locally/main.go --server-port="$SERVER_PORT" --metrics-port="$METRICS_PORT" 61 | 62 | export TEST_EXIT="$?" 63 | 64 | sleep 2 # Scientific way of waiting for the file to be written as async... 65 | 66 | # Removing backgrounded process 67 | kill "$KUBE_AUDIT_PID" 68 | 69 | # Ensure every line has a requestReceivedTimestamp 70 | if [ "$(cat tmp/kube-audit-rest.log | grep -c "requestReceivedTimestamp")" -ne "$(wc -l tmp/kube-audit-rest.log | cut -d ' ' -f 1)" ]; then 71 | echo "output not as expected, not all lines contain requestReceivedTimestamp" 72 | exit 1 73 | fi 74 | 75 | 76 | # Sort audit log by uid as it's the only guaranteed field, and kube-audit-rest doesn't guarantee request ordering 77 | # Removing the requestReceivedTimestamp timestamp as it's not deterministic 78 | cat tmp/kube-audit-rest.log | jq -s -c '. | sort_by(.request.uid)| del(.[].requestReceivedTimestamp)| .[]' > tmp/kube-audit-rest-sorted.log 79 | 80 | # Making sure that we're capturing standard library logs via structured logging 81 | cat tmp/kube-audit-rest-output.log | grep "remote error: tls: bad certificate" | jq -e '.msg' > /dev/null || bash -c 'echo "output not as expected: failed to get standard library logs via structured logging" && exit 255' 82 | 83 | diff testing/locally/data/kube-audit-rest-sorted.log tmp/kube-audit-rest-sorted.log && [ "$TEST_EXIT" -eq "0" ] && echo "Test passed" || bash -c 'echo "output not as expected" && exit 255' 84 | -------------------------------------------------------------------------------- /testing/locally/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2023 Richard Tweed. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package main 15 | 16 | import ( 17 | "bufio" 18 | "bytes" 19 | "context" 20 | "crypto/tls" 21 | "crypto/x509" 22 | "fmt" 23 | "io" 24 | "log" 25 | "net" 26 | "net/http" 27 | "os" 28 | "strings" 29 | 30 | "github.com/thought-machine/go-flags" 31 | ) 32 | 33 | type Options struct { 34 | ServerPort int `long:"server-port" description:"Port where the http server listens to" default:"9090"` 35 | MetricsPort int `long:"metrics-port" description:"Port where the metrics server listens to" default:"55555"` 36 | } 37 | 38 | func main() { 39 | var opts Options 40 | parser := flags.NewParser(&opts, flags.Default) 41 | _, err := parser.Parse() 42 | if err != nil { 43 | log.Fatalf("can't parse flags: %v", err) 44 | } 45 | 46 | testFailureCount := 0 47 | 48 | // send a request which doesn't trust our CA 49 | // Set up custom http client so we can correctly validate TLS 50 | clientFaulty := &http.Client{ 51 | Transport: &http.Transport{ 52 | DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { 53 | if addr == fmt.Sprintf("kube-audit-rest:%d", opts.ServerPort) { 54 | addr = fmt.Sprintf("127.0.0.1:%d", opts.ServerPort) 55 | } 56 | dialer := &net.Dialer{} 57 | return dialer.DialContext(ctx, network, addr) 58 | }, 59 | }, 60 | } 61 | _, err = clientFaulty.Post((fmt.Sprintf("https://kube-audit-rest:%d/log-request", opts.ServerPort)), "application/json", bytes.NewReader([]byte("abc"))) 62 | 63 | if !strings.Contains(err.Error(), "x509: certificate signed by unknown authority") { 64 | log.Println("error: didn't fail when go doesn't trust the CA. instead got ", err.Error()) 65 | testFailureCount++ 66 | } 67 | 68 | // Set up for TLS 69 | caCert, err := os.ReadFile("tmp/rootCA.pem") 70 | if err != nil { 71 | log.Fatal(err) 72 | } 73 | caCertPool := x509.NewCertPool() 74 | caCertPool.AppendCertsFromPEM(caCert) 75 | 76 | // Set up custom http client so we can correctly validate TLS 77 | client := &http.Client{ 78 | Transport: &http.Transport{ 79 | TLSClientConfig: &tls.Config{ RootCAs: caCertPool, MinVersion: tls.VersionTLS13 }, 80 | DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { 81 | if addr == fmt.Sprintf("kube-audit-rest:%d", opts.ServerPort) { 82 | addr = fmt.Sprintf("127.0.0.1:%d", opts.ServerPort) 83 | } 84 | dialer := &net.Dialer{} 85 | return dialer.DialContext(ctx, network, addr) 86 | }, 87 | }, 88 | } 89 | 90 | logRequestAddr := fmt.Sprintf("https://kube-audit-rest:%d/log-request", opts.ServerPort) 91 | metricsAddr := fmt.Sprintf("http://localhost:%d/metrics", opts.MetricsPort) 92 | 93 | // Happy path testing 94 | apiLogFile, err := os.Open("testing/locally/data/kube-audit-rest.log") 95 | if err != nil { 96 | log.Fatal(err) 97 | } 98 | defer apiLogFile.Close() 99 | 100 | scanner := bufio.NewScanner(apiLogFile) 101 | for scanner.Scan() { 102 | line := scanner.Bytes() 103 | if err == io.EOF { 104 | log.Println(err) 105 | break 106 | } 107 | line = append(line, '\n') 108 | resp, err := client.Post(logRequestAddr, "application/json", bytes.NewBuffer(line)) 109 | if err != nil { 110 | log.Println("Error while testing the happy path") 111 | testFailureCount += 1 112 | log.Println(resp) 113 | log.Println(err) 114 | } 115 | 116 | } 117 | 118 | // Test unhappy path 119 | 120 | // Send a totally invalid request 121 | resp, err := client.Post(logRequestAddr, "application/json", bytes.NewBuffer([]byte("{a: \"b\"}"))) 122 | if err != nil || resp.StatusCode != http.StatusBadRequest { 123 | log.Println("Error while executing \"Send a totally invalid request\"") 124 | testFailureCount += 1 125 | log.Println(err) 126 | } 127 | 128 | // Send an almost valid request, but missing the uid 129 | resp, err = client.Post(logRequestAddr, "application/json", bytes.NewBuffer([]byte(`{"kind":"AdmissionReview","apiVersion":"admission.k8s.io/v1","request":{"kind":{"group":"authorization.k8s.io","version":"v1","kind":"SelfSubjectAccessReview"},"resource":{"group":"authorization.k8s.io","version":"v1","resource":"selfsubjectaccessreviews"},"requestKind":{"group":"authorization.k8s.io","version":"v1","kind":"SelfSubjectAccessReview"},"requestResource":{"group":"authorization.k8s.io","version":"v1","resource":"selfsubjectaccessreviews"},"operation":"CREATE","userInfo":{"username":"system:admin","groups":["system:masters","system:authenticated"]},"object":{"kind":"SelfSubjectAccessReview","apiVersion":"authorization.k8s.io/v1","metadata":{"creationTimestamp":null,"managedFields":[{"manager":"steveTEST","operation":"Update","apiVersion":"authorization.k8s.io/v1","time":"2022-11-30T17:46:51Z","fieldsType":"FieldsV1","fieldsV1":{"f:spec":{"f:resourceAttributes":{".":{},"f:group":{},"f:resource":{},"f:verb":{},"f:version":{}}}}}]},"spec":{"resourceAttributes":{"verb":"list","group":"batch","version":"v1","resource":"cronjobs"}},"status":{"allowed":false}},"oldObject":null,"dryRun":false,"options":{"kind":"CreateOptions","apiVersion":"meta.k8s.io/v1"}}} 130 | `))) 131 | if err != nil || resp.StatusCode != http.StatusBadRequest { 132 | log.Println("Error while executing \"Send an almost valid request, but missing the uid\"") 133 | testFailureCount += 1 134 | log.Println(err) 135 | } 136 | 137 | // Send something that isn't json 138 | resp, err = client.Post(logRequestAddr, "application/json", bytes.NewBuffer([]byte("abc"))) 139 | if err != nil || resp.StatusCode != http.StatusBadRequest { 140 | log.Println("Error while executing \"Send something that isn't json\"") 141 | testFailureCount += 1 142 | log.Println(err) 143 | } 144 | 145 | // Don't say we're sending json 146 | resp, err = client.Post(logRequestAddr, "text/plain", bytes.NewBuffer([]byte("abc"))) 147 | if err != nil || resp.StatusCode != http.StatusBadRequest { 148 | log.Println("Error while executing \"Don't say we're sending json\"") 149 | testFailureCount += 1 150 | log.Println(err) 151 | } 152 | 153 | // Ensure metrics server running 154 | resp, err = client.Get(metricsAddr) 155 | if err != nil { 156 | testFailureCount += 1 157 | } else { 158 | bodyBytes, err := io.ReadAll(resp.Body) 159 | if err != nil { 160 | testFailureCount += 1 161 | log.Println(err) 162 | } else if !strings.Contains(string(bodyBytes), "kube-audit-rest") { 163 | testFailureCount += 1 164 | log.Println("Failed to find any metrics") 165 | } 166 | } 167 | 168 | if testFailureCount > 0 { 169 | log.Println(testFailureCount, " tests failed") 170 | os.Exit(255) 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /testing/server.csr.cnf: -------------------------------------------------------------------------------- 1 | [req] 2 | default_bits = 2048 3 | prompt = no 4 | default_md = sha256 5 | distinguished_name = dn 6 | req_extensions = v3_req 7 | 8 | [dn] 9 | C=GB 10 | OU=Engineering 11 | emailAddress=admin@localhost 12 | CN = kube-audit-rest 13 | 14 | 15 | [v3_req] 16 | #authorityKeyIdentifier=keyid,issuer 17 | basicConstraints=CA:FALSE 18 | keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment 19 | 20 | 21 | subjectAltName = @alt_names 22 | 23 | [alt_names] 24 | DNS.1 = kube-audit-rest 25 | DNS.2 = *.kube-audit-rest 26 | DNS.3 = *.kube-audit-rest.svc 27 | DNS.4 = *.kube-audit-rest.svc.cluster.local -------------------------------------------------------------------------------- /testing/setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euo pipefail 4 | 5 | # Default to root if no .git missing 6 | ROOT=$(git rev-parse --show-toplevel || echo '.' ) 7 | 8 | cd $ROOT 9 | 10 | export COMMIT="$(git rev-parse HEAD)" 11 | 12 | # For storing temporary files that version control will ignore, such as certs 13 | mkdir -p tmp 14 | 15 | # Create required certs 16 | testing/certs.sh 17 | 18 | nerdctl build -f Dockerfile-distroless . --namespace k8s.io -t "ghcr.io/richardoc/kube-audit-rest:${COMMIT}-distroless" 19 | # nerdctl build -f Dockerfile-alpine . --namespace k8s.io -t "richardoc/kube-audit-rest:${COMMIT}-alpine" 20 | 21 | kubectl -n kube-audit-rest apply -f k8s/namespace.yaml 22 | 23 | # Upload the TLS cert and replace if exists 24 | kubectl -n kube-audit-rest create secret tls kube-audit-rest --cert=tmp/server.crt --key=tmp/server.key --dry-run=client -oyaml | kubectl -n kube-audit-rest apply -f - 25 | 26 | # Substitute in the correct image tag 27 | cat k8s/deployment.yaml | envsubst | kubectl -n kube-audit-rest apply -f - 28 | 29 | kubectl -n kube-audit-rest apply -f k8s/service.yaml 30 | 31 | # Substitute in the correct CA into the webhook 32 | export CABUNDLEB64="$(cat tmp/rootCA.pem | base64 | tr -d '\n')" 33 | cat k8s/webhook.yaml | envsubst | kubectl -n kube-audit-rest apply -f - 34 | unset CABUNDLEB64 35 | 36 | kubectl -n kube-audit-rest rollout restart deployment/kube-audit-rest 37 | --------------------------------------------------------------------------------