├── .dockerignore ├── .github ├── dependabot.yml └── workflows │ ├── main.yml │ ├── security-code.yml │ └── security-scorecard.yml ├── .gitignore ├── Dockerfile ├── LICENSE.md ├── Makefile ├── README.md ├── SECURITY.md ├── cetusguard ├── cetusguard.go ├── cetusguard_test.go ├── cetusguard_unix_test.go ├── rule.go ├── rule_test.go └── testdata │ └── testcert.go ├── cmd └── cetusguard │ └── main.go ├── e2e ├── cli.patch └── run.sh ├── examples └── compose │ ├── .gitignore │ ├── compose.netdata.yml │ ├── compose.podman.yml │ ├── compose.socket-socket.yml │ ├── compose.socket-tcptls.yml │ ├── compose.tcptls-tcptls.yml │ ├── compose.traefik.yml │ ├── docker.sh │ ├── mkcerts.sh │ ├── podman.sh │ └── rules.list ├── go.mod ├── go.sum ├── internal ├── logger │ ├── logger.go │ └── logger_test.go └── utils │ ├── env │ ├── env.go │ └── env_test.go │ ├── flagextra │ ├── flagextra.go │ └── flagextra_test.go │ └── middleware │ ├── middleware.go │ └── middleware_test.go └── resources └── logo ├── CetusGuard-Color-Horizontal.svg ├── CetusGuard-Color-Icon.svg ├── CetusGuard-Color-Reduced.svg ├── CetusGuard-Color-Vertical.svg ├── CetusGuard-Monochrome-Horizontal.svg ├── CetusGuard-Monochrome-Icon.svg ├── CetusGuard-Monochrome-Reduced.svg └── CetusGuard-Monochrome-Vertical.svg /.dockerignore: -------------------------------------------------------------------------------- 1 | /dist/ 2 | 3 | .idea/ 4 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://json.schemastore.org/dependabot-2.0.json 2 | version: 2 3 | 4 | updates: 5 | - package-ecosystem: "gomod" 6 | directory: "/" 7 | schedule: 8 | interval: "weekly" 9 | groups: 10 | gomod-minor-patch: 11 | update-types: ["minor", "patch"] 12 | 13 | - package-ecosystem: "docker" 14 | directory: "/" 15 | schedule: 16 | interval: "weekly" 17 | groups: 18 | docker-all: 19 | patterns: ["*"] 20 | 21 | - package-ecosystem: "github-actions" 22 | directory: "/" 23 | schedule: 24 | interval: "monthly" 25 | groups: 26 | github-actions-all: 27 | patterns: ["*"] 28 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json 2 | name: "Main" 3 | 4 | on: 5 | push: 6 | tags: ["*"] 7 | branches: ["*"] 8 | pull_request: 9 | branches: ["*"] 10 | workflow_dispatch: 11 | 12 | permissions: {} 13 | 14 | jobs: 15 | gofmt: 16 | name: "Gofmt" 17 | runs-on: "ubuntu-latest" 18 | permissions: 19 | contents: "read" 20 | steps: 21 | - name: "Checkout" 22 | uses: "actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683" 23 | - name: "Set up Go" 24 | uses: "actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b" 25 | with: 26 | go-version-file: "./go.mod" 27 | check-latest: true 28 | - name: "Gofmt" 29 | run: | 30 | make gofmt 31 | 32 | staticcheck: 33 | name: "Staticcheck" 34 | runs-on: "ubuntu-latest" 35 | permissions: 36 | contents: "read" 37 | steps: 38 | - name: "Checkout" 39 | uses: "actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683" 40 | - name: "Set up Go" 41 | uses: "actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b" 42 | with: 43 | go-version-file: "./go.mod" 44 | check-latest: true 45 | - name: "Staticcheck" 46 | uses: "dominikh/staticcheck-action@fe1dd0c3658873b46f8c9bb3291096a617310ca6" 47 | with: 48 | install-go: false 49 | 50 | test: 51 | name: "Test on ${{ matrix.os }}" 52 | needs: ["gofmt", "staticcheck"] 53 | runs-on: "${{ matrix.os }}" 54 | permissions: 55 | contents: "read" 56 | strategy: 57 | fail-fast: false 58 | matrix: 59 | os: ["ubuntu-latest", "windows-latest", "macos-latest"] 60 | steps: 61 | - name: "Checkout" 62 | uses: "actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683" 63 | - name: "Set up Go" 64 | uses: "actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b" 65 | with: 66 | go-version-file: "./go.mod" 67 | check-latest: true 68 | - name: "Test" 69 | run: | 70 | make test 71 | 72 | test-race: 73 | name: "Test race" 74 | needs: ["gofmt", "staticcheck"] 75 | runs-on: "ubuntu-latest" 76 | permissions: 77 | contents: "read" 78 | steps: 79 | - name: "Checkout" 80 | uses: "actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683" 81 | - name: "Set up Go" 82 | uses: "actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b" 83 | with: 84 | go-version-file: "./go.mod" 85 | check-latest: true 86 | - name: "Test race" 87 | run: | 88 | make test-race 89 | 90 | test-e2e: 91 | name: "Test e2e" 92 | needs: ["gofmt", "staticcheck"] 93 | runs-on: "ubuntu-24.04" 94 | permissions: 95 | contents: "read" 96 | steps: 97 | - name: "Checkout" 98 | uses: "actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683" 99 | - name: "Set up Go" 100 | uses: "actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b" 101 | with: 102 | go-version-file: "./go.mod" 103 | check-latest: true 104 | - name: "Test e2e" 105 | run: | 106 | make test-e2e 107 | 108 | build: 109 | name: >- 110 | Build for 111 | ${{ matrix.go.GOOS }}-${{ matrix.go.GOARCH }} 112 | ${{ matrix.go.GOARM != '' && format('v{0}', matrix.go.GOARM) || '' }} 113 | needs: ["test", "test-race", "test-e2e"] 114 | runs-on: "ubuntu-latest" 115 | permissions: 116 | contents: "read" 117 | strategy: 118 | fail-fast: false 119 | matrix: 120 | go: 121 | - { GOOS: "linux", GOARCH: "amd64" } 122 | - { GOOS: "linux", GOARCH: "arm64" } 123 | - { GOOS: "linux", GOARCH: "arm", GOARM: "7" } 124 | - { GOOS: "linux", GOARCH: "arm", GOARM: "6" } 125 | - { GOOS: "linux", GOARCH: "riscv64" } 126 | - { GOOS: "linux", GOARCH: "ppc64le" } 127 | - { GOOS: "linux", GOARCH: "s390x" } 128 | - { GOOS: "windows", GOARCH: "amd64" } 129 | - { GOOS: "windows", GOARCH: "arm64" } 130 | - { GOOS: "darwin", GOARCH: "amd64" } 131 | - { GOOS: "darwin", GOARCH: "arm64" } 132 | steps: 133 | - name: "Checkout" 134 | uses: "actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683" 135 | - name: "Set up Go" 136 | uses: "actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b" 137 | with: 138 | go-version-file: "./go.mod" 139 | check-latest: true 140 | - name: "Build" 141 | run: | 142 | make build \ 143 | GOOS="${{ matrix.go.GOOS }}" \ 144 | GOARCH="${{ matrix.go.GOARCH }}" \ 145 | GOARM="${{ matrix.go.GOARM }}" 146 | file ./dist/*-*-* && gzip -nv ./dist/*-*-* 147 | - name: "Upload artifacts" 148 | uses: "actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02" 149 | with: 150 | name: "dist-${{ matrix.go.GOOS }}-${{ matrix.go.GOARCH }}-${{ matrix.go.GOARM }}" 151 | path: "./dist/*.gz" 152 | retention-days: 1 153 | 154 | build-push-docker: 155 | name: "Build and push Docker images" 156 | needs: ["build"] 157 | runs-on: "ubuntu-latest" 158 | permissions: 159 | contents: "read" 160 | packages: "write" 161 | steps: 162 | - name: "Checkout" 163 | uses: "actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683" 164 | - name: "Set up QEMU" 165 | uses: "docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392" 166 | - name: "Set up Docker Buildx" 167 | uses: "docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2" 168 | - name: "Login to GitHub Container Registry" 169 | if: "github.event_name != 'pull_request'" 170 | uses: "docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772" 171 | with: 172 | registry: "ghcr.io" 173 | username: "${{ github.actor }}" 174 | password: "${{ secrets.GITHUB_TOKEN }}" 175 | - name: "Login to Docker Hub" 176 | if: "github.event_name != 'pull_request'" 177 | uses: "docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772" 178 | with: 179 | registry: "docker.io" 180 | username: "${{ secrets.DOCKERHUB_USERNAME }}" 181 | password: "${{ secrets.DOCKERHUB_TOKEN }}" 182 | - name: "Extract metadata" 183 | id: "meta" 184 | uses: "docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804" 185 | with: 186 | images: | 187 | ghcr.io/${{ github.repository }} 188 | docker.io/${{ github.repository }} 189 | tags: | 190 | type=ref,event=branch 191 | type=semver,pattern=v{{version}} 192 | type=semver,pattern=v{{major}}.{{minor}} 193 | type=semver,pattern=v{{major}} 194 | - name: "Build and push" 195 | uses: "docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1" 196 | with: 197 | context: "./" 198 | platforms: "linux/amd64,linux/arm64/v8,linux/arm/v7,linux/arm/v6,linux/riscv64,linux/ppc64le,linux/s390x" 199 | tags: "${{ steps.meta.outputs.tags }}" 200 | labels: "${{ steps.meta.outputs.labels }}" 201 | push: "${{ github.event_name != 'pull_request' }}" 202 | provenance: "mode=max" 203 | sbom: true 204 | 205 | publish-github-release: 206 | name: "Publish GitHub release" 207 | if: "startsWith(github.ref, 'refs/tags/v')" 208 | needs: ["build", "build-push-docker"] 209 | runs-on: "ubuntu-latest" 210 | permissions: 211 | contents: "write" 212 | steps: 213 | - name: "Download artifacts" 214 | uses: "actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093" 215 | with: 216 | pattern: "dist-*" 217 | merge-multiple: true 218 | path: "assets" 219 | - name: "Publish" 220 | uses: "hectorm/ghaction-release@066200d04c3549852afa243d631ea3dc93390f68" 221 | with: 222 | assets-path: "./assets/" 223 | -------------------------------------------------------------------------------- /.github/workflows/security-code.yml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json 2 | name: "Code security analysis" 3 | 4 | on: 5 | push: 6 | tags: ["*"] 7 | branches: ["*"] 8 | pull_request: 9 | branches: ["*"] 10 | schedule: 11 | - cron: "25 10 * * 3" 12 | workflow_dispatch: 13 | 14 | permissions: {} 15 | 16 | jobs: 17 | analyze-codeql: 18 | name: "CodeQL analysis (${{ matrix.language }})" 19 | runs-on: "ubuntu-latest" 20 | permissions: 21 | actions: "read" 22 | contents: "read" 23 | security-events: "write" 24 | strategy: 25 | fail-fast: false 26 | matrix: 27 | language: ["go"] 28 | steps: 29 | - name: "Checkout" 30 | uses: "actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683" 31 | - name: "Set up Go" 32 | uses: "actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b" 33 | with: 34 | go-version-file: "./go.mod" 35 | check-latest: true 36 | - name: "Initialize CodeQL" 37 | uses: "github/codeql-action/init@28deaeda66b76a05916b6923827895f2b14ab387" 38 | with: 39 | languages: "${{ matrix.language }}" 40 | - name: "Autobuild" 41 | uses: "github/codeql-action/autobuild@28deaeda66b76a05916b6923827895f2b14ab387" 42 | - name: "Perform CodeQL analysis" 43 | uses: "github/codeql-action/analyze@28deaeda66b76a05916b6923827895f2b14ab387" 44 | 45 | analyze-gosec: 46 | name: "Gosec analysis" 47 | runs-on: "ubuntu-latest" 48 | permissions: 49 | actions: "read" 50 | contents: "read" 51 | security-events: "write" 52 | steps: 53 | - name: "Checkout" 54 | uses: "actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683" 55 | - name: "Set up Go" 56 | uses: "actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b" 57 | with: 58 | go-version-file: "./go.mod" 59 | check-latest: true 60 | - name: "Perform Gosec analysis" 61 | uses: "securego/gosec@955a68d0d19f4afb7503068f95059f7d0c529017" 62 | with: 63 | args: "-no-fail -tests -fmt sarif -out ./results.sarif ./..." 64 | - name: "Upload SARIF file" 65 | uses: "github/codeql-action/upload-sarif@28deaeda66b76a05916b6923827895f2b14ab387" 66 | with: 67 | sarif_file: "./results.sarif" 68 | -------------------------------------------------------------------------------- /.github/workflows/security-scorecard.yml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json 2 | name: "Scorecard security analysis" 3 | 4 | on: 5 | push: 6 | branches: ["master"] 7 | schedule: 8 | - cron: "25 10 * * 3" 9 | workflow_dispatch: 10 | 11 | permissions: {} 12 | 13 | jobs: 14 | analyze: 15 | name: "Scorecard security analysis" 16 | runs-on: "ubuntu-latest" 17 | permissions: 18 | actions: "read" 19 | contents: "read" 20 | security-events: "write" 21 | steps: 22 | - name: "Checkout" 23 | uses: "actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683" 24 | - name: "Perform security analysis" 25 | uses: "ossf/scorecard-action@f49aabe0b5af0936a0987cfb85d86b75731b0186" 26 | with: 27 | results_file: "./results.sarif" 28 | results_format: "sarif" 29 | repo_token: "${{ secrets.GITHUB_TOKEN }}" 30 | publish_results: false 31 | - name: "Upload SARIF file" 32 | uses: "github/codeql-action/upload-sarif@28deaeda66b76a05916b6923827895f2b14ab387" 33 | with: 34 | sarif_file: "./results.sarif" 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /dist/ 2 | 3 | .idea/ 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker.io/docker/dockerfile:1 2 | 3 | ################################################## 4 | ## "build" stage 5 | ################################################## 6 | 7 | FROM --platform=${BUILDPLATFORM:-linux/amd64} docker.io/golang:1.24.1-bookworm@sha256:fa1a01d362a7b9df68b021d59a124d28cae6d99ebd1a876e3557c4dd092f1b1d AS build 8 | 9 | ARG TARGETOS 10 | ARG TARGETARCH 11 | ARG TARGETVARIANT 12 | 13 | WORKDIR /src/ 14 | COPY ./go.mod ./go.sum ./ 15 | RUN go mod download 16 | COPY ./ ./ 17 | RUN make test 18 | RUN make build \ 19 | GOOS="${TARGETOS-}" \ 20 | GOARCH="${TARGETARCH-}" \ 21 | GOARM="$([ "${TARGETARCH-}" != 'arm' ] || printf '%s' "${TARGETVARIANT#v}")" 22 | RUN test -z "$(readelf -x .interp ./dist/cetusguard-* 2>/dev/null)" 23 | 24 | ################################################## 25 | ## "main" stage 26 | ################################################## 27 | 28 | FROM scratch AS main 29 | 30 | COPY --from=build /src/dist/cetusguard-* /bin/cetusguard 31 | 32 | ENV CETUSGUARD_FRONTEND_ADDR='tcp://:2375' 33 | 34 | ENTRYPOINT ["/bin/cetusguard"] 35 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | ===================== 3 | 4 | Copyright © Héctor Molinero Fernández 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | 3 | SHELL := /bin/sh 4 | .SHELLFLAGS := -euc 5 | 6 | DESTDIR ?= 7 | 8 | prefix ?= /usr/local 9 | exec_prefix ?= $(prefix) 10 | bindir ?= $(exec_prefix)/bin 11 | 12 | GIT := git 13 | GO := go 14 | GOFMT := gofmt 15 | GOSEC := gosec 16 | STATICCHECK := staticcheck 17 | INSTALL := install 18 | 19 | INSTALL_PROGRAM := $(INSTALL) 20 | INSTALL_DATA := $(INSTALL) -m 644 21 | 22 | GIT_TAG := $(shell '$(GIT)' tag -l --contains HEAD) 23 | GIT_SHA := $(shell '$(GIT)' rev-parse HEAD) 24 | VERSION := $(if $(GIT_TAG),$(GIT_TAG),$(GIT_SHA)) 25 | 26 | GOOS := $(shell '$(GO)' env GOOS) 27 | GOARCH := $(shell '$(GO)' env GOARCH) 28 | GOVARIANT := $(GO386)$(GOAMD64)$(GOARM)$(GOMIPS)$(GOMIPS64)$(GOPPC64) 29 | export CGO_ENABLED := 0 30 | 31 | GOFLAGS := -trimpath 32 | LDFLAGS := -s -w -X "main.version=$(VERSION)" 33 | 34 | SRCS := $(shell '$(GIT)' ls-files '*.go' 2>/dev/null ||:) 35 | EXEC := cetusguard-$(GOOS)-$(GOARCH) 36 | 37 | ifneq ($(GOVARIANT),) 38 | EXEC := $(addsuffix -$(GOVARIANT), $(EXEC)) 39 | endif 40 | 41 | ifeq ($(GOOS),windows) 42 | EXEC := $(addsuffix .exe, $(EXEC)) 43 | endif 44 | 45 | .PHONY: all 46 | all: test build 47 | 48 | .PHONY: build 49 | build: ./dist/$(EXEC) 50 | 51 | .PHONY: run 52 | run: ./dist/$(EXEC) 53 | '$<' 54 | 55 | ./dist/$(EXEC): $(SRCS) 56 | @mkdir -p "$$(dirname '$@')" 57 | '$(GO)' build $(GOFLAGS) -ldflags '$(LDFLAGS)' -o '$@' ./cmd/cetusguard/ 58 | 59 | .PHONY: gofmt 60 | gofmt: 61 | @test -z "$$('$(GOFMT)' -s -l ./ | tee /dev/stderr)" 62 | 63 | .PHONY: gosec 64 | gosec: 65 | '$(GOSEC)' -tests ./... 66 | 67 | .PHONY: staticcheck 68 | staticcheck: 69 | '$(STATICCHECK)' -tests ./... 70 | 71 | .PHONY: test 72 | test: 73 | '$(GO)' test -v ./... 74 | 75 | .PHONY: test-race 76 | test-race: 77 | CGO_ENABLED=1 '$(GO)' test -v -race ./... 78 | 79 | .PHONY: test-e2e 80 | test-e2e: 81 | ./e2e/run.sh 82 | 83 | .PHONY: install 84 | install: 85 | @mkdir -p '$(DESTDIR)$(bindir)' 86 | $(INSTALL_PROGRAM) './dist/$(EXEC)' '$(DESTDIR)$(bindir)/cetusguard' 87 | 88 | PHONY: installcheck 89 | installcheck: 90 | @test -x '$(DESTDIR)$(bindir)/cetusguard' 91 | 92 | .PHONY: uninstall 93 | uninstall: 94 | rm -fv '$(DESTDIR)$(bindir)/cetusguard' 95 | 96 | .PHONY: clean 97 | clean: 98 | rm -rfv './dist/' 99 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 | # CetusGuard 6 | 7 | CetusGuard is a tool that protects the Docker daemon socket by filtering calls to its API endpoints. 8 | 9 | Some highlights: 10 | * It is written in a memory-safe language. 11 | * Has a small codebase that is easy to audit. 12 | * Has zero dependencies to mitigate supply chain attacks. 13 | 14 | ## Docker daemon security 15 | 16 | Unless you opt in to [rootless mode][1], the Docker daemon requires root and any service with access to its API can escalate privileges. Even in rootless mode, any container with access to the API can escape out of the container, this applies to both Docker and Podman. 17 | 18 | The Docker daemon [exposes its API][2] by default through a non-networked Unix socket, which can be restricted by file system permissions, and for networked use the daemon supports being exposed through SSH or TCP with TLS client authentication. However, you still need to fully trust any service you give access to its API. 19 | 20 | CetusGuard solves this problem by acting as a proxy between the daemon and the services that consume its API, allowing for example read-only access to some endpoints. 21 | 22 | ## Usage 23 | 24 | CetusGuard is distributed as a Docker image available on [Docker Hub][3] or [GitHub Container Registry][4] and as a statically linked binary available in the [releases section][5] of the project. 25 | 26 | A collection of examples for experimenting with CetusGuard, including some real world scenarios with Traefik and Netdata, can be found in the [./examples/](./examples/) directory. 27 | 28 | These are the supported options: 29 | ``` 30 | -backend-addr string 31 | Container daemon socket to connect to (env CETUSGUARD_BACKEND_ADDR, CONTAINER_HOST, DOCKER_HOST) (default "unix:///var/run/docker.sock") 32 | -backend-tls-cacert string 33 | Path to the backend TLS certificate used to verify the daemon identity (env CETUSGUARD_BACKEND_TLS_CACERT) 34 | -backend-tls-cert string 35 | Path to the backend TLS certificate used to authenticate with the daemon (env CETUSGUARD_BACKEND_TLS_CERT) 36 | -backend-tls-key string 37 | Path to the backend TLS key used to authenticate with the daemon (env CETUSGUARD_BACKEND_TLS_KEY) 38 | -frontend-addr value 39 | Address to bind the server to, can be specified multiple times (env CETUSGUARD_FRONTEND_ADDR) (default ["tcp://127.0.0.1:2375"]) 40 | -frontend-tls-cacert string 41 | Path to the frontend TLS certificate used to verify the identity of clients (env CETUSGUARD_FRONTEND_TLS_CACERT) 42 | -frontend-tls-cert string 43 | Path to the frontend TLS certificate (env CETUSGUARD_FRONTEND_TLS_CERT) 44 | -frontend-tls-key string 45 | Path to the frontend TLS key (env CETUSGUARD_FRONTEND_TLS_KEY) 46 | -log-level int 47 | The minimum entry level to log, from 0 to 7 (env CETUSGUARD_LOG_LEVEL) (default 6) 48 | -no-builtin-rules 49 | Do not load the built-in rules (env CETUSGUARD_NO_BUILTIN_RULES) 50 | -rules value 51 | Filter rules separated by new lines, can be specified multiple times (env CETUSGUARD_RULES) 52 | -rules-file value 53 | Filter rules file, can be specified multiple times (env CETUSGUARD_RULES_FILE) 54 | -version 55 | Show version number and quit 56 | ``` 57 | 58 | ## Filter rules 59 | 60 | By default, only a few common harmless endpoints are allowed, `/_ping`, `/info` and `/version`. 61 | 62 | All other endpoints are denied and must be explicitly allowed through a rule syntax defined by the following ABNF grammar: 63 | ``` 64 | blank = ( SP / HTAB ) 65 | method = 1*%x41-5A ; HTTP method 66 | methods = method *( "," method ) ; HTTP method list 67 | pattern = 1*UNICODE ; Target path regex 68 | rule = *blank methods 1*blank pattern *blank ; Rule 69 | ``` 70 | 71 | Only requests that match the specified HTTP methods and target path regex are allowed. 72 | 73 | There are several variables specified by surrounding `%` that can be used to construct rule patterns, the full list and values can be found in the [`rule.go`](./cetusguard/rule.go) file. 74 | 75 | Lines starting with `!` are ignored. 76 | 77 | Some example rules are: 78 | ``` 79 | ! Ping 80 | GET,HEAD %API_PREFIX_PING% 81 | 82 | ! Get version 83 | GET %API_PREFIX_VERSION% 84 | 85 | ! Get system information 86 | GET %API_PREFIX_INFO% 87 | 88 | ! Get data usage information 89 | GET %API_PREFIX_SYSTEM%/df 90 | 91 | ! Monitor events 92 | GET %API_PREFIX_EVENTS% 93 | 94 | ! List containers 95 | GET %API_PREFIX_CONTAINERS%/json 96 | 97 | ! Inspect a container 98 | GET %API_PREFIX_CONTAINERS%/%CONTAINER_ID_OR_NAME%/json 99 | 100 | ! Create a container 101 | POST %API_PREFIX_CONTAINERS%/create(\?.*)? 102 | 103 | ! Start a container 104 | POST %API_PREFIX_CONTAINERS%/%CONTAINER_ID_OR_NAME%/start(\?.*)? 105 | 106 | ! Kill a container 107 | POST %API_PREFIX_CONTAINERS%/%CONTAINER_ID_OR_NAME%/kill(\?.*)? 108 | 109 | ! Remove a container 110 | DELETE %API_PREFIX_CONTAINERS%/%CONTAINER_ID_OR_NAME%(\?.*)? 111 | 112 | ! Connect a container to a network 113 | POST %API_PREFIX_NETWORKS%/%NETWORK_ID_OR_NAME%/connect(\?.*)? 114 | 115 | ! Disconnect a container from a network 116 | POST %API_PREFIX_NETWORKS%/%NETWORK_ID_OR_NAME%/disconnect(\?.*)? 117 | 118 | ! Inspect an image 119 | GET %API_PREFIX_IMAGES%/%IMAGE_ID_OR_REFERENCE%/json 120 | 121 | ! Pull or import an image 122 | POST %API_PREFIX_IMAGES%/create 123 | 124 | ! Remove an image 125 | DELETE %API_PREFIX_IMAGES%/%IMAGE_ID_OR_REFERENCE%(\?.*)? 126 | ``` 127 | 128 | ## License 129 | 130 | [MIT License](./LICENSE.md) © [Héctor Molinero Fernández](https://hector.molinero.dev). 131 | 132 | [1]: https://docs.docker.com/engine/security/rootless/ 133 | [2]: https://docs.docker.com/engine/security/protect-access/ 134 | [3]: https://hub.docker.com/r/hectorm/cetusguard 135 | [4]: https://github.com/hectorm/cetusguard/pkgs/container/cetusguard 136 | [5]: https://github.com/hectorm/cetusguard/releases 137 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security policy 2 | 3 | ## Reporting a vulnerability 4 | 5 | If you have found or just suspect a security vulnerability, it would be greatly appreciated disclosing it in a responsible manner at hector@molinero.dev. 6 | -------------------------------------------------------------------------------- /cetusguard/cetusguard.go: -------------------------------------------------------------------------------- 1 | package cetusguard 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "crypto/x509" 7 | "errors" 8 | "fmt" 9 | "io" 10 | "net" 11 | "net/http" 12 | "net/url" 13 | "os" 14 | "os/signal" 15 | "path" 16 | "path/filepath" 17 | "strings" 18 | "sync" 19 | "sync/atomic" 20 | "syscall" 21 | "time" 22 | 23 | "github.com/hectorm/cetusguard/internal/logger" 24 | "github.com/hectorm/cetusguard/internal/utils/middleware" 25 | ) 26 | 27 | const ( 28 | minTlsVersion = tls.VersionTLS12 29 | ) 30 | 31 | const ( 32 | mediaTypeRawStream = "application/vnd.docker.raw-stream" 33 | mediaTypeMultiplexedStream = "application/vnd.docker.multiplexed-stream" 34 | ) 35 | 36 | type Backend struct { 37 | Addr string 38 | TlsCacert string 39 | TlsCert string 40 | TlsKey string 41 | } 42 | 43 | type Frontend struct { 44 | Addr []string 45 | TlsCacert string 46 | TlsCert string 47 | TlsKey string 48 | } 49 | 50 | type Server struct { 51 | Backend *Backend 52 | Frontend *Frontend 53 | Rules []Rule 54 | 55 | backendProto string 56 | backendHost string 57 | backendTlsConfig *tls.Config 58 | backendHttpClient *http.Client 59 | 60 | frontendNetListeners []net.Listener 61 | frontendTlsConfig *tls.Config 62 | frontendHttpServer *http.Server 63 | 64 | runningState int32 65 | mu sync.Mutex 66 | } 67 | 68 | func (cg *Server) Start(ready chan<- any) error { 69 | cg.mu.Lock() 70 | var unlockOnce sync.Once 71 | defer unlockOnce.Do(cg.mu.Unlock) 72 | 73 | var closeOnce sync.Once 74 | defer closeOnce.Do(func() { close(ready) }) 75 | 76 | if cg.IsRunning() { 77 | return errors.New("server is already running") 78 | } 79 | defer cg.setIsRunning(false) 80 | 81 | var err error 82 | cg.backendProto, cg.backendHost, err = parseAddr(cg.Backend.Addr) 83 | if err != nil { 84 | return err 85 | } 86 | 87 | cg.backendTlsConfig, err = clientTlsConfig(cg.Backend.TlsCacert, cg.Backend.TlsCert, cg.Backend.TlsKey) 88 | if err != nil { 89 | return err 90 | } 91 | 92 | backendDialer := &net.Dialer{ 93 | Timeout: 30 * time.Second, 94 | KeepAlive: 90 * time.Second, 95 | } 96 | 97 | cg.backendHttpClient = &http.Client{ 98 | Timeout: 30 * time.Second, 99 | Transport: &http.Transport{ 100 | TLSClientConfig: cg.backendTlsConfig, 101 | MaxIdleConns: 10, 102 | MaxIdleConnsPerHost: 10, 103 | TLSHandshakeTimeout: 10 * time.Second, 104 | IdleConnTimeout: 90 * time.Second, 105 | ExpectContinueTimeout: 1 * time.Second, 106 | DialContext: func(ctx context.Context, _ string, _ string) (net.Conn, error) { 107 | return backendDialer.DialContext(ctx, cg.backendProto, cg.backendHost) 108 | }, 109 | }, 110 | } 111 | 112 | cg.frontendNetListeners = nil 113 | for _, addr := range cg.Frontend.Addr { 114 | proto, host, err := parseAddr(addr) 115 | if err != nil { 116 | return err 117 | } 118 | l, err := net.Listen(proto, host) 119 | if err != nil { 120 | return err 121 | } 122 | cg.frontendNetListeners = append(cg.frontendNetListeners, l) 123 | } 124 | defer func() { 125 | for _, l := range cg.frontendNetListeners { 126 | _ = l.Close() 127 | } 128 | }() 129 | 130 | cg.frontendTlsConfig, err = serverTlsConfig(cg.Frontend.TlsCacert, cg.Frontend.TlsCert, cg.Frontend.TlsKey) 131 | if err != nil { 132 | return err 133 | } 134 | 135 | cg.frontendHttpServer = &http.Server{ 136 | TLSConfig: cg.frontendTlsConfig, 137 | ReadTimeout: 120 * time.Minute, 138 | ReadHeaderTimeout: 10 * time.Second, 139 | WriteTimeout: 120 * time.Minute, 140 | IdleTimeout: 90 * time.Second, 141 | ErrorLog: logger.LgrError(), 142 | Handler: http.HandlerFunc(func(wri http.ResponseWriter, req *http.Request) { 143 | if cg.validateRequest(req) { 144 | err := cg.handleValidRequest(wri, req) 145 | if err != nil { 146 | logger.Error(err) 147 | } 148 | } else { 149 | cg.handleInvalidRequest(wri, req) 150 | } 151 | }), 152 | } 153 | 154 | chErr := make(chan error, 1) 155 | 156 | go func() { 157 | chSig := make(chan os.Signal, 1) 158 | signal.Notify(chSig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) 159 | 160 | sig := <-chSig 161 | logger.Infof("%v signal received\n", sig) 162 | 163 | chErr <- cg.Stop() 164 | }() 165 | 166 | for _, l := range cg.frontendNetListeners { 167 | logger.Infof("serve on %s\n", l.Addr()) 168 | go func(l net.Listener, srv *http.Server, tls *tls.Config) { 169 | var err error 170 | if tls != nil && l.Addr().Network() != "unix" { 171 | err = srv.ServeTLS(l, "", "") 172 | } else { 173 | err = srv.Serve(l) 174 | } 175 | if err != http.ErrServerClosed { 176 | chErr <- err 177 | } 178 | }(l, cg.frontendHttpServer, cg.frontendTlsConfig) 179 | } 180 | 181 | cg.setIsRunning(true) 182 | unlockOnce.Do(cg.mu.Unlock) 183 | closeOnce.Do(func() { close(ready) }) 184 | 185 | return <-chErr 186 | } 187 | 188 | func (cg *Server) Stop() error { 189 | cg.mu.Lock() 190 | defer cg.mu.Unlock() 191 | 192 | if !cg.IsRunning() { 193 | return errors.New("server is not running") 194 | } 195 | defer cg.setIsRunning(false) 196 | 197 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 198 | defer cancel() 199 | 200 | cg.backendHttpClient.CloseIdleConnections() 201 | err := cg.frontendHttpServer.Shutdown(ctx) 202 | 203 | logger.Infof("exit\n") 204 | return err 205 | } 206 | 207 | func (cg *Server) Addrs() ([]net.Addr, error) { 208 | if !cg.IsRunning() { 209 | return nil, errors.New("server is not running") 210 | } 211 | 212 | var addr []net.Addr 213 | for _, l := range cg.frontendNetListeners { 214 | addr = append(addr, l.Addr()) 215 | } 216 | 217 | return addr, nil 218 | } 219 | 220 | func (cg *Server) IsRunning() bool { 221 | return atomic.LoadInt32(&cg.runningState) != 0 222 | } 223 | 224 | func (cg *Server) setIsRunning(running bool) { 225 | if running { 226 | atomic.StoreInt32(&cg.runningState, 1) 227 | } else { 228 | atomic.StoreInt32(&cg.runningState, 0) 229 | } 230 | } 231 | 232 | func (cg *Server) validateRequest(req *http.Request) bool { 233 | p := cleanPath(req.URL.Path) 234 | for _, rule := range cg.Rules { 235 | _, mOk := rule.Methods[req.Method] 236 | if mOk && rule.Pattern.MatchString(p) { 237 | return true 238 | } 239 | } 240 | return false 241 | } 242 | 243 | func (cg *Server) handleValidRequest(wri http.ResponseWriter, req *http.Request) error { 244 | logger.Debugf("allowed request: %s %s\n", req.Method, req.URL.Path) 245 | 246 | mWri := &middleware.ResponseWriter{ResponseWriter: wri} 247 | if f, ok := wri.(http.Flusher); ok { 248 | mWri.Flusher = f 249 | } 250 | 251 | newReq := req.Clone(req.Context()) 252 | if cg.backendTlsConfig != nil { 253 | newReq.URL.Scheme = "https" 254 | } else { 255 | newReq.URL.Scheme = "http" 256 | } 257 | if cg.backendProto == "unix" { 258 | newReq.URL.Host = "localhost" 259 | } else { 260 | newReq.URL.Host = cg.backendHost 261 | } 262 | 263 | res, err := cg.backendHttpClient.Transport.RoundTrip(newReq) 264 | if errors.Is(err, context.Canceled) || errors.Is(err, syscall.ECONNREFUSED) { 265 | mWri.WriteHeader(http.StatusBadGateway) 266 | return nil 267 | } else if err != nil { 268 | mWri.WriteHeader(http.StatusBadGateway) 269 | return err 270 | } 271 | defer func() { 272 | _ = res.Body.Close() 273 | }() 274 | 275 | resMediaType := res.Header.Get("Content-Type") 276 | 277 | if resMediaType == mediaTypeRawStream || resMediaType == mediaTypeMultiplexedStream { 278 | logger.Debugf("stream response\n") 279 | 280 | // If the response is a stream, we need to disable the write deadline to prevent the connection from being closed 281 | rc := http.NewResponseController(wri) 282 | err = rc.SetWriteDeadline(time.Time{}) 283 | if err != nil { 284 | return err 285 | } 286 | } 287 | 288 | if res.StatusCode == 101 { 289 | logger.Debugf("connection hijack\n") 290 | 291 | var upCloseOnce sync.Once 292 | var downCloseOnce sync.Once 293 | 294 | up, ok := res.Body.(io.ReadWriteCloser) 295 | if !ok { 296 | mWri.WriteHeader(http.StatusInternalServerError) 297 | return errors.New("body is not writable") 298 | } 299 | defer func() { 300 | upCloseOnce.Do(func() { _ = up.Close() }) 301 | }() 302 | 303 | hj, ok := wri.(http.Hijacker) 304 | if !ok { 305 | mWri.WriteHeader(http.StatusInternalServerError) 306 | return errors.New("unable to hijack connection") 307 | } 308 | 309 | down, downRw, err := hj.Hijack() 310 | if err != nil { 311 | return err 312 | } 313 | defer func() { 314 | downCloseOnce.Do(func() { _ = down.Close() }) 315 | }() 316 | 317 | _, err = downRw.Write([]byte(res.Proto + " " + res.Status + "\r\n")) 318 | if err != nil { 319 | return err 320 | } 321 | 322 | err = res.Header.Write(downRw) 323 | if err != nil { 324 | return err 325 | } 326 | 327 | _, err = downRw.Write([]byte("\r\n")) 328 | if err != nil { 329 | return err 330 | } 331 | 332 | err = downRw.Flush() 333 | if err != nil { 334 | return err 335 | } 336 | 337 | var wg sync.WaitGroup 338 | wg.Add(2) 339 | 340 | go func() { 341 | defer wg.Done() 342 | _, _ = io.Copy(up, down) 343 | }() 344 | 345 | go func() { 346 | defer wg.Done() 347 | _, _ = io.Copy(down, up) 348 | downCloseOnce.Do(func() { _ = down.Close() }) 349 | }() 350 | 351 | wg.Wait() 352 | } else { 353 | for k, vv := range res.Header { 354 | for _, v := range vv { 355 | mWri.Header().Add(k, v) 356 | } 357 | } 358 | mWri.WriteHeader(res.StatusCode) 359 | 360 | if res.StatusCode >= 200 && res.StatusCode != 204 && res.StatusCode != 304 { 361 | _, err = io.Copy(mWri, res.Body) 362 | if errors.Is(err, context.Canceled) || errors.Is(err, syscall.EPIPE) || errors.Is(err, syscall.ECONNRESET) { 363 | return nil 364 | } else if err != nil { 365 | return err 366 | } 367 | } 368 | } 369 | 370 | return nil 371 | } 372 | 373 | func (cg *Server) handleInvalidRequest(wri http.ResponseWriter, req *http.Request) { 374 | logger.Warningf("denied request: %s %s\n", req.Method, req.URL.Path) 375 | 376 | wri.WriteHeader(http.StatusForbidden) 377 | } 378 | 379 | func clientTlsConfig(cacertPath string, certPath string, keyPath string) (*tls.Config, error) { 380 | var tlsConfig *tls.Config 381 | 382 | var cacertPool *x509.CertPool 383 | if cacertPath != "" { 384 | cacert, err := os.ReadFile(filepath.Clean(cacertPath)) 385 | if err != nil { 386 | return nil, err 387 | } 388 | cacertPool = x509.NewCertPool() 389 | if ok := cacertPool.AppendCertsFromPEM(cacert); !ok { 390 | return nil, errors.New("error loading CA certificate") 391 | } 392 | } 393 | 394 | var certificates []tls.Certificate 395 | if certPath != "" || keyPath != "" { 396 | cert, err := tls.LoadX509KeyPair(certPath, keyPath) 397 | if err != nil { 398 | return nil, err 399 | } 400 | certificates = []tls.Certificate{cert} 401 | } 402 | 403 | if cacertPool != nil || len(certificates) > 0 { 404 | tlsConfig = &tls.Config{ 405 | MinVersion: minTlsVersion, 406 | RootCAs: cacertPool, 407 | Certificates: certificates, 408 | } 409 | } 410 | 411 | return tlsConfig, nil 412 | } 413 | 414 | func serverTlsConfig(cacertPath string, certPath string, keyPath string) (*tls.Config, error) { 415 | var tlsConfig *tls.Config 416 | 417 | var clientAuth tls.ClientAuthType 418 | var cacertPool *x509.CertPool 419 | if cacertPath != "" { 420 | cacert, err := os.ReadFile(filepath.Clean(cacertPath)) 421 | if err != nil { 422 | return nil, err 423 | } 424 | cacertPool = x509.NewCertPool() 425 | if ok := cacertPool.AppendCertsFromPEM(cacert); !ok { 426 | return nil, errors.New("error loading CA certificate") 427 | } 428 | clientAuth = tls.RequireAndVerifyClientCert 429 | } else { 430 | clientAuth = tls.NoClientCert 431 | } 432 | 433 | var certificates []tls.Certificate 434 | if certPath != "" || keyPath != "" { 435 | cert, err := tls.LoadX509KeyPair(certPath, keyPath) 436 | if err != nil { 437 | return nil, err 438 | } 439 | certificates = []tls.Certificate{cert} 440 | } 441 | 442 | if cacertPool != nil || len(certificates) > 0 { 443 | tlsConfig = &tls.Config{ 444 | MinVersion: minTlsVersion, 445 | Certificates: certificates, 446 | ClientAuth: clientAuth, 447 | ClientCAs: cacertPool, 448 | } 449 | } 450 | 451 | return tlsConfig, nil 452 | } 453 | 454 | func parseAddr(addr string) (string, string, error) { 455 | parts := strings.SplitN(addr, "://", 2) 456 | if len(parts) != 2 || len(parts[0]) == 0 || len(parts[1]) == 0 { 457 | return "", "", fmt.Errorf("invalid address format: %s", addr) 458 | } 459 | 460 | switch parts[0] { 461 | case "unix": 462 | return parts[0], parts[1], nil 463 | case "tcp": 464 | u, err := url.Parse(addr) 465 | if err != nil { 466 | return "", "", err 467 | } 468 | return u.Scheme, u.Host, nil 469 | default: 470 | return "", "", fmt.Errorf("unsupported address protocol: %s", addr) 471 | } 472 | } 473 | 474 | // Borrowed from net/http/server.go 475 | func cleanPath(p string) string { 476 | if p == "" { 477 | return "/" 478 | } 479 | if p[0] != '/' { 480 | p = "/" + p 481 | } 482 | np := path.Clean(p) 483 | if p[len(p)-1] == '/' && np != "/" { 484 | np += "/" 485 | } 486 | return np 487 | } 488 | -------------------------------------------------------------------------------- /cetusguard/cetusguard_unix_test.go: -------------------------------------------------------------------------------- 1 | //go:build unix 2 | 3 | package cetusguard 4 | 5 | import ( 6 | "context" 7 | "io" 8 | "net" 9 | "net/http" 10 | "path/filepath" 11 | "strings" 12 | "testing" 13 | "time" 14 | ) 15 | 16 | func TestCetusGuardSocketAllowedReq(t *testing.T) { 17 | tc := &testCase{ 18 | daemonListenerFunc: socketDaemonListener, 19 | daemonFunc: socketDaemon, 20 | backendFunc: socketBackend, 21 | frontendFunc: socketFrontend, 22 | clientFunc: socketClient, 23 | } 24 | 25 | defer tc.setup(t)() 26 | tc.daemon.Handler = http.HandlerFunc(httpDaemonHandler) 27 | 28 | ready := make(chan any, 1) 29 | go func() { 30 | err := tc.server.Start(ready) 31 | if err != nil { 32 | t.Error(err) 33 | } 34 | }() 35 | <-ready 36 | 37 | addrs, err := tc.server.Addrs() 38 | if err != nil { 39 | t.Fatal(err) 40 | } 41 | 42 | req, err := httpClientAllowedReq("http", addrs[0].String()) 43 | if err != nil { 44 | t.Fatal(err) 45 | } 46 | 47 | res, err := tc.client.Do(req) 48 | if err != nil { 49 | t.Fatal(err) 50 | } 51 | defer func() { 52 | _ = res.Body.Close() 53 | }() 54 | 55 | if res.StatusCode != http.StatusOK { 56 | t.Fatalf("res.StatusCode = %d, want %d", res.StatusCode, http.StatusOK) 57 | } 58 | 59 | msg, err := io.ReadAll(res.Body) 60 | if err != nil { 61 | t.Fatal(err) 62 | } 63 | 64 | if string(msg) != "PONG" { 65 | t.Fatalf(`msg = "%s", want "%s"`, msg, "PONG") 66 | } 67 | 68 | err = tc.server.Stop() 69 | if err != nil { 70 | t.Fatal(err) 71 | } 72 | } 73 | 74 | func TestCetusGuardSocketAllowedStreamReq(t *testing.T) { 75 | tc := &testCase{ 76 | daemonListenerFunc: socketDaemonListener, 77 | daemonFunc: socketDaemon, 78 | backendFunc: socketBackend, 79 | frontendFunc: socketFrontend, 80 | clientFunc: socketClient, 81 | } 82 | 83 | defer tc.setup(t)() 84 | tc.daemon.Handler = http.HandlerFunc(httpDaemonHandler) 85 | 86 | ready := make(chan any, 1) 87 | go func() { 88 | err := tc.server.Start(ready) 89 | if err != nil { 90 | t.Error(err) 91 | } 92 | }() 93 | <-ready 94 | 95 | addrs, err := tc.server.Addrs() 96 | if err != nil { 97 | t.Fatal(err) 98 | } 99 | 100 | req, err := httpClientAllowedReq("http", addrs[0].String()) 101 | if err != nil { 102 | t.Fatal(err) 103 | } 104 | req.Header.Set("Upgrade", "tcp") 105 | req.Header.Set("Connection", "Upgrade") 106 | 107 | res, err := tc.client.Do(req) 108 | if err != nil { 109 | t.Fatal(err) 110 | } 111 | defer func() { 112 | _ = res.Body.Close() 113 | }() 114 | 115 | if res.StatusCode != http.StatusSwitchingProtocols { 116 | t.Fatalf("res.StatusCode = %d, want %d", res.StatusCode, http.StatusSwitchingProtocols) 117 | } 118 | 119 | msg, err := io.ReadAll(res.Body) 120 | if err != nil { 121 | t.Fatal(err) 122 | } 123 | 124 | if string(msg) != "PONG" { 125 | t.Fatalf(`msg = "%s", want "%s"`, msg, "PONG") 126 | } 127 | 128 | err = tc.server.Stop() 129 | if err != nil { 130 | t.Fatal(err) 131 | } 132 | } 133 | 134 | func TestCetusGuardSocketDeniedMethodReq(t *testing.T) { 135 | tc := &testCase{ 136 | daemonListenerFunc: socketDaemonListener, 137 | daemonFunc: socketDaemon, 138 | backendFunc: socketBackend, 139 | frontendFunc: socketFrontend, 140 | clientFunc: socketClient, 141 | } 142 | 143 | defer tc.setup(t)() 144 | tc.daemon.Handler = http.HandlerFunc(httpDaemonHandler) 145 | 146 | ready := make(chan any, 1) 147 | go func() { 148 | err := tc.server.Start(ready) 149 | if err != nil { 150 | t.Error(err) 151 | } 152 | }() 153 | <-ready 154 | 155 | addrs, err := tc.server.Addrs() 156 | if err != nil { 157 | t.Fatal(err) 158 | } 159 | 160 | req, err := httpClientDeniedMethodReq("http", addrs[0].String()) 161 | if err != nil { 162 | t.Fatal(err) 163 | } 164 | 165 | res, err := tc.client.Do(req) 166 | if err != nil { 167 | t.Fatal(err) 168 | } 169 | defer func() { 170 | _ = res.Body.Close() 171 | }() 172 | 173 | if res.StatusCode != http.StatusForbidden { 174 | t.Fatalf("res.StatusCode = %d, want %d", res.StatusCode, http.StatusForbidden) 175 | } 176 | 177 | err = tc.server.Stop() 178 | if err != nil { 179 | t.Fatal(err) 180 | } 181 | } 182 | 183 | func TestCetusGuardSocketDeniedPatternReq(t *testing.T) { 184 | tc := &testCase{ 185 | daemonListenerFunc: socketDaemonListener, 186 | daemonFunc: socketDaemon, 187 | backendFunc: socketBackend, 188 | frontendFunc: socketFrontend, 189 | clientFunc: socketClient, 190 | } 191 | 192 | defer tc.setup(t)() 193 | tc.daemon.Handler = http.HandlerFunc(httpDaemonHandler) 194 | 195 | ready := make(chan any, 1) 196 | go func() { 197 | err := tc.server.Start(ready) 198 | if err != nil { 199 | t.Error(err) 200 | } 201 | }() 202 | <-ready 203 | 204 | addrs, err := tc.server.Addrs() 205 | if err != nil { 206 | t.Fatal(err) 207 | } 208 | 209 | req, err := httpClientDeniedPatternReq("http", addrs[0].String()) 210 | if err != nil { 211 | t.Fatal(err) 212 | } 213 | 214 | res, err := tc.client.Do(req) 215 | if err != nil { 216 | t.Fatal(err) 217 | } 218 | defer func() { 219 | _ = res.Body.Close() 220 | }() 221 | 222 | if res.StatusCode != http.StatusForbidden { 223 | t.Fatalf("res.StatusCode = %d, want %d", res.StatusCode, http.StatusForbidden) 224 | } 225 | 226 | err = tc.server.Stop() 227 | if err != nil { 228 | t.Fatal(err) 229 | } 230 | } 231 | 232 | func TestCetusGuardSocketTlsAuthBackendReq(t *testing.T) { 233 | tc := &testCase{ 234 | daemonListenerFunc: tcpDaemonListener, 235 | daemonFunc: tlsAuthDaemon, 236 | backendFunc: tlsAuthBackend, 237 | frontendFunc: socketFrontend, 238 | clientFunc: socketClient, 239 | } 240 | 241 | defer tc.setup(t)() 242 | tc.daemon.Handler = http.HandlerFunc(httpDaemonHandler) 243 | 244 | ready := make(chan any, 1) 245 | go func() { 246 | err := tc.server.Start(ready) 247 | if err != nil { 248 | t.Error(err) 249 | } 250 | }() 251 | <-ready 252 | 253 | addrs, err := tc.server.Addrs() 254 | if err != nil { 255 | t.Fatal(err) 256 | } 257 | 258 | req, err := httpClientAllowedReq("http", addrs[0].String()) 259 | if err != nil { 260 | t.Fatal(err) 261 | } 262 | 263 | res, err := tc.client.Do(req) 264 | if err != nil { 265 | t.Fatal(err) 266 | } 267 | defer func() { 268 | _ = res.Body.Close() 269 | }() 270 | 271 | if res.StatusCode != http.StatusOK { 272 | t.Fatalf("res.StatusCode = %d, want %d", res.StatusCode, http.StatusOK) 273 | } 274 | 275 | msg, err := io.ReadAll(res.Body) 276 | if err != nil { 277 | t.Fatal(err) 278 | } 279 | 280 | if string(msg) != "PONG" { 281 | t.Fatalf(`msg = "%s", want "%s"`, msg, "PONG") 282 | } 283 | 284 | err = tc.server.Stop() 285 | if err != nil { 286 | t.Fatal(err) 287 | } 288 | } 289 | 290 | func TestCetusGuardTlsAuthSocketBackendReq(t *testing.T) { 291 | tc := &testCase{ 292 | daemonListenerFunc: socketDaemonListener, 293 | daemonFunc: socketDaemon, 294 | backendFunc: socketBackend, 295 | frontendFunc: tlsAuthFrontend, 296 | clientFunc: tlsAuthClient, 297 | } 298 | 299 | defer tc.setup(t)() 300 | tc.daemon.Handler = http.HandlerFunc(httpDaemonHandler) 301 | 302 | ready := make(chan any, 1) 303 | go func() { 304 | err := tc.server.Start(ready) 305 | if err != nil { 306 | t.Error(err) 307 | } 308 | }() 309 | <-ready 310 | 311 | addrs, err := tc.server.Addrs() 312 | if err != nil { 313 | t.Fatal(err) 314 | } 315 | 316 | req, err := httpClientAllowedReq("https", addrs[0].String()) 317 | if err != nil { 318 | t.Fatal(err) 319 | } 320 | 321 | res, err := tc.client.Do(req) 322 | if err != nil { 323 | t.Fatal(err) 324 | } 325 | defer func() { 326 | _ = res.Body.Close() 327 | }() 328 | 329 | if res.StatusCode != http.StatusOK { 330 | t.Fatalf("res.StatusCode = %d, want %d", res.StatusCode, http.StatusOK) 331 | } 332 | 333 | msg, err := io.ReadAll(res.Body) 334 | if err != nil { 335 | t.Fatal(err) 336 | } 337 | 338 | if string(msg) != "PONG" { 339 | t.Fatalf(`msg = "%s", want "%s"`, msg, "PONG") 340 | } 341 | 342 | err = tc.server.Stop() 343 | if err != nil { 344 | t.Fatal(err) 345 | } 346 | } 347 | 348 | func socketDaemonListener(tmpdir string) (net.Listener, error) { 349 | listener, err := net.Listen("unix", filepath.Join(tmpdir, "d")) 350 | if err != nil { 351 | return nil, err 352 | } 353 | 354 | return listener, nil 355 | } 356 | 357 | func socketDaemon() (*http.Server, error) { 358 | server, err := plainDaemon() 359 | if err != nil { 360 | return nil, err 361 | } 362 | 363 | return server, nil 364 | } 365 | 366 | func socketBackend(listener net.Listener, tmpdir string) (*Backend, error) { 367 | backend, err := plainBackend(listener, tmpdir) 368 | if err != nil { 369 | return nil, err 370 | } 371 | 372 | return backend, nil 373 | } 374 | 375 | func socketFrontend(tmpdir string) (*Frontend, error) { 376 | frontend, err := plainFrontend(tmpdir) 377 | if err != nil { 378 | return nil, err 379 | } 380 | 381 | frontend.Addr = []string{"unix://" + filepath.Join(tmpdir, "c")} 382 | 383 | return frontend, nil 384 | } 385 | 386 | func socketClient() (*http.Client, error) { 387 | client, err := plainClient() 388 | if err != nil { 389 | return nil, err 390 | } 391 | 392 | dialer := &net.Dialer{ 393 | Timeout: 30 * time.Second, 394 | KeepAlive: 90 * time.Second, 395 | } 396 | transport := client.Transport.(*http.Transport) 397 | transport.DialContext = func(ctx context.Context, _ string, addr string) (net.Conn, error) { 398 | return dialer.DialContext(ctx, "unix", addr[:strings.LastIndex(addr, ":")]) 399 | } 400 | 401 | return client, nil 402 | } 403 | -------------------------------------------------------------------------------- /cetusguard/rule.go: -------------------------------------------------------------------------------- 1 | package cetusguard 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | "regexp" 9 | "sort" 10 | "strings" 11 | 12 | "github.com/hectorm/cetusguard/internal/logger" 13 | ) 14 | 15 | var RawBuiltinRules = []string{ 16 | // Ping 17 | `GET,HEAD %API_PREFIX_PING%`, 18 | `GET,HEAD %API_PREFIX_LIBPOD_PING%`, 19 | // Get version 20 | `GET %API_PREFIX_VERSION%`, 21 | `GET %API_PREFIX_LIBPOD_VERSION%`, 22 | // Get system information 23 | `GET %API_PREFIX_INFO%`, 24 | `GET %API_PREFIX_LIBPOD_INFO%`, 25 | } 26 | 27 | var ( 28 | ruleLineRegex = regexp.MustCompile(`^[\t ]*([A-Z]+(?:,[A-Z]+)*)[\t ]+(.+?)[\t ]*$`) 29 | commentLineRegex = regexp.MustCompile(`^[\t ]*(?:!.*)?$`) 30 | newLineRegex = regexp.MustCompile(`\r?\n`) 31 | ruleVars = map[string]string{ 32 | "DOMAIN": `(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*)`, 33 | "IPV4": `(?:[0-9]{1,3}(?:\.[0-9]{1,3}){3})`, 34 | "IPV6": `(?:\[[a-fA-F0-9]{0,4}(?::[a-fA-F0-9]{0,4}){2,7}(?:%[a-zA-Z0-9_]+)?\])`, 35 | "IP": `(?:%IPV4%|%IPV6%)`, 36 | "DOMAIN_OR_IP": `(?:%DOMAIN%|%IP%)`, 37 | "HOST": `(?:%DOMAIN_OR_IP%(?::[0-9]+)?)`, 38 | 39 | "IMAGE_ID": `%_OBJECT_ID%`, 40 | "IMAGE_COMPONENT": `(?:[a-zA-Z0-9]+(?:(?:\.|_{1,2}|-+)[a-zA-Z0-9]+)*)`, 41 | "IMAGE_TAG": `(?:[a-zA-Z0-9_][a-zA-Z0-9_.-]{0,127})`, 42 | "IMAGE_DIGEST": `(?:[a-zA-Z][a-zA-Z0-9]*(?:[_.+-][a-zA-Z][a-zA-Z0-9]*)*:[a-fA-F0-9]{32,})`, 43 | "IMAGE_NAME": `(?:(?:%HOST%/)?%IMAGE_COMPONENT%(?:/%IMAGE_COMPONENT%)*)`, 44 | "IMAGE_REFERENCE": `(?:%IMAGE_NAME%(?::%IMAGE_TAG%)?(?:@%IMAGE_DIGEST%)?)`, 45 | "IMAGE_ID_OR_REFERENCE": `(?:%IMAGE_ID%|%IMAGE_REFERENCE%)`, 46 | 47 | "CONTAINER_ID": `%_OBJECT_ID%`, 48 | "CONTAINER_NAME": `%_OBJECT_NAME%`, 49 | "CONTAINER_ID_OR_NAME": `(?:%CONTAINER_ID%|%CONTAINER_NAME%)`, 50 | 51 | "VOLUME_ID": `%_OBJECT_ID%`, 52 | "VOLUME_NAME": `%_OBJECT_NAME%`, 53 | "VOLUME_ID_OR_NAME": `(?:%VOLUME_ID%|%VOLUME_NAME%)`, 54 | 55 | "NETWORK_ID": `%_OBJECT_ID%`, 56 | "NETWORK_NAME": `(?:[^/]+)`, 57 | "NETWORK_ID_OR_NAME": `(?:%NETWORK_ID%|%NETWORK_NAME%)`, 58 | 59 | "PLUGIN_ID": `%_OBJECT_ID%`, 60 | "PLUGIN_NAME": `%IMAGE_NAME%`, 61 | "PLUGIN_ID_OR_NAME": `(?:%PLUGIN_ID%|%PLUGIN_NAME%)`, 62 | 63 | "API_PREFIX": `%_API_VERSION%?`, 64 | "API_PREFIX_AUTH": `%API_PREFIX%/auth`, 65 | "API_PREFIX_BUILD": `%API_PREFIX%/build`, 66 | "API_PREFIX_COMMIT": `%API_PREFIX%/commit`, 67 | "API_PREFIX_CONFIGS": `%API_PREFIX%/configs`, 68 | "API_PREFIX_CONTAINERS": `%API_PREFIX%/containers`, 69 | "API_PREFIX_DISTRIBUTION": `%API_PREFIX%/distribution`, 70 | "API_PREFIX_EVENTS": `%API_PREFIX%/events`, 71 | "API_PREFIX_EXEC": `%API_PREFIX%/exec`, 72 | "API_PREFIX_GRPC": `%API_PREFIX%/grpc`, 73 | "API_PREFIX_IMAGES": `%API_PREFIX%/images`, 74 | "API_PREFIX_INFO": `%API_PREFIX%/info`, 75 | "API_PREFIX_NETWORKS": `%API_PREFIX%/networks`, 76 | "API_PREFIX_NODES": `%API_PREFIX%/nodes`, 77 | "API_PREFIX_PING": `%API_PREFIX%/_ping`, 78 | "API_PREFIX_PLUGINS": `%API_PREFIX%/plugins`, 79 | "API_PREFIX_SECRETS": `%API_PREFIX%/secrets`, 80 | "API_PREFIX_SERVICES": `%API_PREFIX%/services`, 81 | "API_PREFIX_SESSION": `%API_PREFIX%/session`, 82 | "API_PREFIX_SWARM": `%API_PREFIX%/swarm`, 83 | "API_PREFIX_SYSTEM": `%API_PREFIX%/system`, 84 | "API_PREFIX_TASKS": `%API_PREFIX%/tasks`, 85 | "API_PREFIX_VERSION": `%API_PREFIX%/version`, 86 | "API_PREFIX_VOLUMES": `%API_PREFIX%/volumes`, 87 | 88 | "API_PREFIX_LIBPOD": `%_API_VERSION%/libpod`, 89 | "API_PREFIX_LIBPOD_BUILD": `%API_PREFIX_LIBPOD%/build`, 90 | "API_PREFIX_LIBPOD_COMMIT": `%API_PREFIX_LIBPOD%/commit`, 91 | "API_PREFIX_LIBPOD_CONTAINERS": `%API_PREFIX_LIBPOD%/containers`, 92 | "API_PREFIX_LIBPOD_EVENTS": `%API_PREFIX_LIBPOD%/events`, 93 | "API_PREFIX_LIBPOD_EXEC": `%API_PREFIX_LIBPOD%/exec`, 94 | "API_PREFIX_LIBPOD_GENERATE": `%API_PREFIX_LIBPOD%/generate`, 95 | "API_PREFIX_LIBPOD_IMAGES": `%API_PREFIX_LIBPOD%/images`, 96 | "API_PREFIX_LIBPOD_INFO": `%API_PREFIX_LIBPOD%/info`, 97 | "API_PREFIX_LIBPOD_MANIFESTS": `%API_PREFIX_LIBPOD%/manifests`, 98 | "API_PREFIX_LIBPOD_NETWORKS": `%API_PREFIX_LIBPOD%/networks`, 99 | "API_PREFIX_LIBPOD_PING": `%API_PREFIX_LIBPOD%/_ping`, 100 | "API_PREFIX_LIBPOD_PLAY": `%API_PREFIX_LIBPOD%/play`, 101 | "API_PREFIX_LIBPOD_PODS": `%API_PREFIX_LIBPOD%/pods`, 102 | "API_PREFIX_LIBPOD_SECRETS": `%API_PREFIX_LIBPOD%/secrets`, 103 | "API_PREFIX_LIBPOD_SYSTEM": `%API_PREFIX_LIBPOD%/system`, 104 | "API_PREFIX_LIBPOD_VERSION": `%API_PREFIX_LIBPOD%/version`, 105 | "API_PREFIX_LIBPOD_VOLUMES": `%API_PREFIX_LIBPOD%/volumes`, 106 | 107 | // Private variables, may change in any version 108 | "_API_VERSION": `(?:/v[0-9]+(?:\.[0-9]+)*)`, 109 | "_OBJECT_ID": `(?:[a-fA-F0-9]+)`, 110 | "_OBJECT_NAME": `(?:[a-zA-Z0-9][a-zA-Z0-9_.-]+)`, 111 | } 112 | ) 113 | 114 | func BuildRules(str string) ([]Rule, error) { 115 | var rules []Rule 116 | 117 | lines := newLineRegex.Split(str, -1) 118 | for _, line := range lines { 119 | if commentLineRegex.MatchString(line) { 120 | continue 121 | } 122 | 123 | matches := ruleLineRegex.FindStringSubmatch(line) 124 | if len(matches) != 3 { 125 | return nil, fmt.Errorf("invalid rule line: %s", line) 126 | } 127 | methodsFrag := matches[1] 128 | patternFrag := matches[2] 129 | 130 | methods := make(map[string]struct{}) 131 | for _, method := range strings.Split(methodsFrag, ",") { 132 | methods[method] = struct{}{} 133 | } 134 | 135 | for { 136 | p := patternFrag 137 | for k, v := range ruleVars { 138 | if strings.Contains(p, "%"+k+"%") { 139 | p = strings.ReplaceAll(p, "%"+k+"%", v) 140 | } 141 | } 142 | if p == patternFrag { 143 | break 144 | } 145 | patternFrag = p 146 | } 147 | pattern, err := regexp.Compile("^" + patternFrag + "$") 148 | if err != nil { 149 | return nil, fmt.Errorf("invalid rule pattern: %s", str) 150 | } 151 | 152 | rule := Rule{methods, pattern} 153 | rules = append(rules, rule) 154 | 155 | logger.Debugf("loaded rule: %s\n", rule) 156 | } 157 | 158 | return rules, nil 159 | } 160 | 161 | func BuildRulesFromFilePath(path string) ([]Rule, error) { 162 | var rules []Rule 163 | 164 | file, err := os.Open(filepath.Clean(path)) 165 | if err != nil { 166 | return nil, err 167 | } 168 | defer func() { 169 | _ = file.Close() 170 | }() 171 | 172 | fileInfo, err := file.Stat() 173 | if err != nil { 174 | return nil, err 175 | } 176 | 177 | if !fileInfo.Mode().IsRegular() { 178 | return nil, fmt.Errorf("open %s: not a file", path) 179 | } 180 | 181 | scanner := bufio.NewScanner(file) 182 | scanner.Split(bufio.ScanLines) 183 | for scanner.Scan() { 184 | r, err := BuildRules(scanner.Text()) 185 | if err != nil { 186 | return nil, err 187 | } 188 | 189 | rules = append(rules, r...) 190 | } 191 | 192 | return rules, nil 193 | } 194 | 195 | type Rule struct { 196 | Methods map[string]struct{} 197 | Pattern *regexp.Regexp 198 | } 199 | 200 | func (rule Rule) String() string { 201 | methods := make([]string, 0, len(rule.Methods)) 202 | for k := range rule.Methods { 203 | methods = append(methods, k) 204 | } 205 | sort.Strings(methods) 206 | 207 | return fmt.Sprintf("%s %s", 208 | strings.Join(methods, ","), 209 | rule.Pattern.String(), 210 | ) 211 | } 212 | -------------------------------------------------------------------------------- /cetusguard/rule_test.go: -------------------------------------------------------------------------------- 1 | package cetusguard 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "path/filepath" 7 | "reflect" 8 | "regexp" 9 | "strings" 10 | "testing" 11 | ) 12 | 13 | func init() { 14 | for k, v := range ruleVars { 15 | for kk, vv := range ruleVars { 16 | if strings.Contains(vv, "%"+k+"%") { 17 | ruleVars[kk] = strings.ReplaceAll(vv, "%"+k+"%", v) 18 | } 19 | } 20 | } 21 | } 22 | 23 | func TestRuleVars(t *testing.T) { 24 | varRegex := regexp.MustCompile(`%[a-zA-Z0-9_]+%`) 25 | for k, v := range ruleVars { 26 | if varRegex.MatchString(v) { 27 | t.Errorf("%s variable contains an undefined variable: %s", k, v) 28 | continue 29 | } 30 | if _, err := regexp.Compile("^" + v + "$"); err != nil { 31 | t.Errorf("%s variable could not be compiled: %v", k, err) 32 | continue 33 | } 34 | } 35 | } 36 | 37 | func TestRuleString(t *testing.T) { 38 | rawRule := "GET,HEAD,POST ^/.+$" 39 | rule := Rule{ 40 | Methods: map[string]struct{}{"POST": {}, "HEAD": {}, "GET": {}}, 41 | Pattern: regexp.MustCompile(`^/.+$`), 42 | } 43 | if rule.String() != rawRule { 44 | t.Errorf("rule = %v, want = %v", rule, rawRule) 45 | } 46 | } 47 | 48 | func TestBuildBuiltinRules(t *testing.T) { 49 | _, err := BuildRules(strings.Join(RawBuiltinRules, "\n")) 50 | if err != nil { 51 | t.Errorf("cannot build built-in rules: %v", err) 52 | } 53 | } 54 | 55 | func TestBuildValidRules(t *testing.T) { 56 | rawRules := map[string]Rule{ 57 | "! Comment\nGET,HEAD %API_PREFIX%/test01\n": { 58 | Methods: map[string]struct{}{"GET": {}, "HEAD": {}}, 59 | Pattern: regexp.MustCompile(`^(?:/v[0-9]+(?:\.[0-9]+)*)?/test01$`), 60 | }, 61 | "! Comment\r\nGET,HEAD %API_PREFIX%/test02\r\n": { 62 | Methods: map[string]struct{}{"GET": {}, "HEAD": {}}, 63 | Pattern: regexp.MustCompile(`^(?:/v[0-9]+(?:\.[0-9]+)*)?/test02$`), 64 | }, 65 | "\n\n\n! Comment\n\n\nGET,HEAD %API_PREFIX%/test03\n\n\n": { 66 | Methods: map[string]struct{}{"GET": {}, "HEAD": {}}, 67 | Pattern: regexp.MustCompile(`^(?:/v[0-9]+(?:\.[0-9]+)*)?/test03$`), 68 | }, 69 | " \t ! Comment\n \t GET,HEAD \t %API_PREFIX%/test04 \t ": { 70 | Methods: map[string]struct{}{"GET": {}, "HEAD": {}}, 71 | Pattern: regexp.MustCompile(`^(?:/v[0-9]+(?:\.[0-9]+)*)?/test04$`), 72 | }, 73 | } 74 | 75 | for k, v := range rawRules { 76 | builtRules, err := BuildRules(k) 77 | if err != nil { 78 | t.Error(err) 79 | continue 80 | } 81 | wantedRules := []Rule{v} 82 | if !reflect.DeepEqual(builtRules, wantedRules) { 83 | t.Errorf("builtRules = %v, want = %v", builtRules, wantedRules) 84 | continue 85 | } 86 | } 87 | } 88 | 89 | func TestBuildInvalidRules(t *testing.T) { 90 | rawRules := []string{ 91 | "%API_PREFIX%/test01", 92 | ", %API_PREFIX%/test02", 93 | "GET, %API_PREFIX%/test03", 94 | "GET,HEAD, %API_PREFIX%/test04", 95 | "GET %API_PREFIX%/[9-0]+/test05", 96 | "GET %API_PREFIX%/\x81/test06", 97 | "GET\n%API_PREFIX%/test07", 98 | "GET\r\n%API_PREFIX%/test08", 99 | } 100 | 101 | for _, v := range rawRules { 102 | builtRules, err := BuildRules(v) 103 | if err == nil || builtRules != nil { 104 | t.Errorf("builtRules = %v, want an error", builtRules) 105 | continue 106 | } 107 | } 108 | } 109 | 110 | func TestBuildRulesFromFilePath(t *testing.T) { 111 | rawRules := []byte("\n! Comment\nGET /.+\r\nGET /.+\r\nGET /.+") 112 | 113 | tmpdir := t.TempDir() 114 | path := filepath.Join(tmpdir, "rules.list") 115 | if err := os.WriteFile(path, rawRules, 0600); err != nil { 116 | t.Fatal(err) 117 | } 118 | 119 | builtRules, err := BuildRulesFromFilePath(path) 120 | if err != nil { 121 | t.Fatal(err) 122 | } 123 | 124 | if len(builtRules) != 3 { 125 | t.Errorf("len(builtRules) = %d, want = %d", len(builtRules), 3) 126 | } 127 | } 128 | 129 | func TestBuildInvalidRulesFromFilePath(t *testing.T) { 130 | rawRules := []byte("INVALID") 131 | 132 | tmpdir := t.TempDir() 133 | path := filepath.Join(tmpdir, "rules.list") 134 | if err := os.WriteFile(path, rawRules, 0600); err != nil { 135 | t.Fatal(err) 136 | } 137 | 138 | builtRules, err := BuildRulesFromFilePath(path) 139 | if err == nil || builtRules != nil { 140 | t.Errorf("builtRules = %v, want an error", builtRules) 141 | } 142 | } 143 | 144 | func TestBuildRulesFromSymlinkPath(t *testing.T) { 145 | tmpdir := t.TempDir() 146 | path := filepath.Join(tmpdir, "rules.list") 147 | if err := os.WriteFile(path, []byte(""), 0600); err != nil { 148 | t.Fatal(err) 149 | } 150 | 151 | link := filepath.Join(tmpdir, "rules-link.list") 152 | if err := os.Symlink(path, link); err != nil { 153 | t.Fatal(err) 154 | } 155 | 156 | _, err := BuildRulesFromFilePath(link) 157 | if err != nil { 158 | t.Fatal(err) 159 | } 160 | } 161 | 162 | func TestBuildRulesFromDirectoryPath(t *testing.T) { 163 | tmpdir := t.TempDir() 164 | path := filepath.Join(tmpdir, "rules") 165 | if err := os.Mkdir(path, 0700); err != nil { 166 | log.Fatal(err) 167 | } 168 | 169 | builtRules, err := BuildRulesFromFilePath(path) 170 | if err == nil || builtRules != nil { 171 | t.Errorf("builtRules = %v, want an error", builtRules) 172 | } 173 | } 174 | 175 | func TestBuildRulesFromNonexistentPath(t *testing.T) { 176 | tmpdir := t.TempDir() 177 | path := filepath.Join(tmpdir, "rules.list") 178 | 179 | builtRules, err := BuildRulesFromFilePath(path) 180 | if err == nil || builtRules != nil { 181 | t.Errorf("builtRules = %v, want an error", builtRules) 182 | } 183 | } 184 | 185 | func TestDomainRegex(t *testing.T) { 186 | re := regexp.MustCompile("^" + ruleVars["DOMAIN"] + "$") 187 | 188 | testCases := map[string]bool{ 189 | "": false, 190 | "l": true, 191 | "localhost": true, 192 | "-localhost": false, 193 | "localhost-": false, 194 | "sub.example.test": true, 195 | "sub.-example.test": false, 196 | "sub.example-.test": false, 197 | "001.test": true, 198 | "xn--7o8h.test": true, 199 | "a.a.a.a.a.a.a": true, 200 | "a.a.a...a.a.a": false, 201 | } 202 | 203 | for input, wanted := range testCases { 204 | if result := re.MatchString(input); result != wanted { 205 | t.Errorf("\"%s\" match = %t, want = %t", input, result, wanted) 206 | } 207 | } 208 | } 209 | 210 | func TestIpv4Regex(t *testing.T) { 211 | re := regexp.MustCompile("^" + ruleVars["IPV4"] + "$") 212 | 213 | testCases := map[string]bool{ 214 | "": false, 215 | "0": false, 216 | "0.0": false, 217 | "0.0.0.0.0": false, 218 | "0.0.0.0": true, 219 | "255.255.255.255": true, 220 | "1111.0.0.0": false, 221 | } 222 | 223 | for input, wanted := range testCases { 224 | if result := re.MatchString(input); result != wanted { 225 | t.Errorf("\"%s\" match = %t, want = %t", input, result, wanted) 226 | } 227 | } 228 | } 229 | 230 | func TestIpv6Regex(t *testing.T) { 231 | re := regexp.MustCompile("^" + ruleVars["IPV6"] + "$") 232 | 233 | testCases := map[string]bool{ 234 | "": false, 235 | "::": false, 236 | "[]": false, 237 | "[::]": true, 238 | "[::1]": true, 239 | "[f:f:f:f:f:f:f:f]": true, 240 | "[f:f:f:f:f:f:f:x]": false, 241 | "[f:f:f:f:f:f:f:f:f]": false, 242 | "[ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff]": true, 243 | } 244 | 245 | for input, wanted := range testCases { 246 | if result := re.MatchString(input); result != wanted { 247 | t.Errorf("\"%s\" match = %t, want = %t", input, result, wanted) 248 | } 249 | } 250 | } 251 | 252 | func TestHostRegex(t *testing.T) { 253 | re := regexp.MustCompile("^" + ruleVars["HOST"] + "$") 254 | 255 | testCases := map[string]bool{ 256 | "": false, 257 | "localhost": true, 258 | "localhost:": false, 259 | "localhost:2375": true, 260 | "localhost:aaaa": false, 261 | "sub.example.test": true, 262 | "sub.example.test:": false, 263 | "sub.example.test:2375": true, 264 | "sub.example.test:aaaa": false, 265 | "127.0.0.1": true, 266 | "127.0.0.1:": false, 267 | "127.0.0.1:2375": true, 268 | "127.0.0.1:aaaa": false, 269 | "[::1]": true, 270 | "[::1]:": false, 271 | "[::1]:2375": true, 272 | "[::1]:aaaa": false, 273 | } 274 | 275 | for input, wanted := range testCases { 276 | if result := re.MatchString(input); result != wanted { 277 | t.Errorf("\"%s\" match = %t, want = %t", input, result, wanted) 278 | } 279 | } 280 | } 281 | 282 | func TestObjectIdRegex(t *testing.T) { 283 | re := regexp.MustCompile("^" + ruleVars["_OBJECT_ID"] + "$") 284 | 285 | testCases := map[string]bool{ 286 | "": false, 287 | "0123456789abcdef": true, 288 | "0123456789x": false, 289 | } 290 | 291 | for input, wanted := range testCases { 292 | if result := re.MatchString(input); result != wanted { 293 | t.Errorf("\"%s\" match = %t, want = %t", input, result, wanted) 294 | } 295 | } 296 | } 297 | 298 | func TestObjectNameRegex(t *testing.T) { 299 | re := regexp.MustCompile("^" + ruleVars["_OBJECT_NAME"] + "$") 300 | 301 | testCases := map[string]bool{ 302 | "": false, 303 | "x": false, 304 | "x0": true, 305 | "0x": true, 306 | "xx": true, 307 | "xx_.-": true, 308 | "-xx": false, 309 | "busybox": true, 310 | } 311 | 312 | for input, wanted := range testCases { 313 | if result := re.MatchString(input); result != wanted { 314 | t.Errorf("\"%s\" match = %t, want = %t", input, result, wanted) 315 | } 316 | } 317 | } 318 | 319 | func TestImageReferenceRegex(t *testing.T) { 320 | re := regexp.MustCompile("^" + ruleVars["IMAGE_REFERENCE"] + "$") 321 | 322 | testCases := map[string]bool{ 323 | "": false, 324 | "b": true, 325 | "busybox": true, 326 | "busybox:": false, 327 | "busybox:l": true, 328 | "busybox:latest": true, 329 | "busybox:latest@": false, 330 | "busybox:latest@sha256": false, 331 | "busybox:latest@sha256:": false, 332 | "busybox:latest@sha256:x": false, 333 | "busybox:latest@sha256:09c731d73926315908778730f9e6068": false, 334 | "busybox:latest@sha256:09c731d73926315908778730f9e60686": true, 335 | "busybox:latest@sha256:09c731d73926315908778730f9e606864fb72f1523d5c1c81c02dc51563885ba": true, 336 | "busybox@sha256:09c731d73926315908778730f9e606864fb72f1523d5c1c81c02dc51563885ba": true, 337 | "busybox@sha256-:09c731d73926315908778730f9e606864fb72f1523d5c1c81c02dc51563885ba": false, 338 | "busybox@sha256-test:09c731d73926315908778730f9e606864fb72f1523d5c1c81c02dc51563885ba": true, 339 | "busybox@sha256--test:09c731d73926315908778730f9e606864fb72f1523d5c1c81c02dc51563885ba": false, 340 | "busybox@sha256-0test:09c731d73926315908778730f9e606864fb72f1523d5c1c81c02dc51563885ba": false, 341 | "busybox@sha256-test0:09c731d73926315908778730f9e606864fb72f1523d5c1c81c02dc51563885ba": true, 342 | "busybox@sha256_test:09c731d73926315908778730f9e606864fb72f1523d5c1c81c02dc51563885ba": true, 343 | "busybox@sha256.test:09c731d73926315908778730f9e606864fb72f1523d5c1c81c02dc51563885ba": true, 344 | "busybox@sha256+test:09c731d73926315908778730f9e606864fb72f1523d5c1c81c02dc51563885ba": true, 345 | "docker.io/busybox:latest": true, 346 | "docker.io/library/busybox:latest": true, 347 | "localhost:5000/library/busybox:latest": true, 348 | "127.0.0.1:5000/library/busybox:latest": true, 349 | "[::1]:5000/library/busybox:latest": true, 350 | "-busybox:latest": false, 351 | "busybox:-latest": false, 352 | "docker.io/-library/busybox:latest": false, 353 | "docker.io/foo/bar/busybox:latest": true, 354 | "docker.io/foo.bar/busybox:latest": true, 355 | "docker.io/foo..bar/busybox:latest": false, 356 | "docker.io/foo_bar/busybox:latest": true, 357 | "docker.io/foo__bar/busybox:latest": true, 358 | "docker.io/foo___bar/busybox:latest": false, 359 | "docker.io/foo-bar/busybox:latest": true, 360 | "docker.io/foo--bar/busybox:latest": true, 361 | "docker.io/foo---bar/busybox:latest": true, 362 | } 363 | 364 | for input, wanted := range testCases { 365 | if result := re.MatchString(input); result != wanted { 366 | t.Errorf("\"%s\" match = %t, want = %t", input, result, wanted) 367 | } 368 | } 369 | } 370 | 371 | func TestApiPrefixRegex(t *testing.T) { 372 | re := regexp.MustCompile("^" + ruleVars["API_PREFIX"] + "$") 373 | 374 | testCases := map[string]bool{ 375 | "": true, 376 | "/": false, 377 | "/v": false, 378 | "/v9": true, 379 | "/v99": true, 380 | "/v99.9": true, 381 | "/v99.99": true, 382 | "/v99.99.9": true, 383 | "/v99.99.99": true, 384 | "/9.9": false, 385 | "/v9.9/": false, 386 | "/v9.9.": false, 387 | "/v.9.9": false, 388 | "/v9..9": false, 389 | "/va.a": false, 390 | } 391 | 392 | for input, wanted := range testCases { 393 | if result := re.MatchString(input); result != wanted { 394 | t.Errorf("\"%s\" match = %t, want = %t", input, result, wanted) 395 | } 396 | } 397 | } 398 | 399 | func TestApiPrefixPing(t *testing.T) { 400 | re := regexp.MustCompile("^" + ruleVars["API_PREFIX_PING"] + "$") 401 | 402 | testCases := map[string]bool{ 403 | "": false, 404 | "/": false, 405 | "/_ping": true, 406 | "/_ping/": false, 407 | "/_pong": false, 408 | "_ping": false, 409 | "//_ping": false, 410 | "/v9.9/": false, 411 | "/v9.9/_ping": true, 412 | "v9.9/_ping": false, 413 | } 414 | 415 | for input, wanted := range testCases { 416 | if result := re.MatchString(input); result != wanted { 417 | t.Errorf("\"%s\" match = %t, want = %t", input, result, wanted) 418 | } 419 | } 420 | } 421 | 422 | func TestApiPrefixLibpodRegex(t *testing.T) { 423 | re := regexp.MustCompile("^" + ruleVars["API_PREFIX_LIBPOD"] + "$") 424 | 425 | testCases := map[string]bool{ 426 | "": false, 427 | "/": false, 428 | "/libpod": false, 429 | "/v/libpod": false, 430 | "/v9/libpod": true, 431 | "/v99.99.99/libpod": true, 432 | } 433 | 434 | for input, wanted := range testCases { 435 | if result := re.MatchString(input); result != wanted { 436 | t.Errorf("\"%s\" match = %t, want = %t", input, result, wanted) 437 | } 438 | } 439 | } 440 | 441 | func TestApiPrefixLibpodPingRegex(t *testing.T) { 442 | re := regexp.MustCompile("^" + ruleVars["API_PREFIX_LIBPOD_PING"] + "$") 443 | 444 | testCases := map[string]bool{ 445 | "": false, 446 | "/": false, 447 | "/_ping": false, 448 | "/libpod/_ping": false, 449 | "/v/libpod/_ping": false, 450 | "/v9.9/libpod/_ping": true, 451 | "/v9.9/libpod/_ping/": false, 452 | "/v9.9/libpod/_pong": false, 453 | } 454 | 455 | for input, wanted := range testCases { 456 | if result := re.MatchString(input); result != wanted { 457 | t.Errorf("\"%s\" match = %t, want = %t", input, result, wanted) 458 | } 459 | } 460 | } 461 | -------------------------------------------------------------------------------- /cetusguard/testdata/testcert.go: -------------------------------------------------------------------------------- 1 | package testdata 2 | 3 | import "strings" 4 | 5 | var TestTlsCacert = []byte(strings.ReplaceAll(`-----BEGIN TESTING CERTIFICATE----- 6 | MIIFLTCCAxWgAwIBAgIUOUAIhjrC9MIDPSu9Wl08XMDXYO4wDQYJKoZIhvcNAQEL 7 | BQAwJTEjMCEGA1UEAwwaZGFlbW9uOkNvbnRhaW5lciBkYWVtb24gQ0EwIBcNMDAw 8 | MTAxMDAwMDAwWhgPMjA5OTEyMzEwMDAwMDBaMCUxIzAhBgNVBAMMGmRhZW1vbjpD 9 | b250YWluZXIgZGFlbW9uIENBMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKC 10 | AgEAxaDr5Rfjsd3T5i4yKYo0z4XOyodDZ9bLsa/w8bjGmE5mVa4rQu953wABGUfV 11 | jIikm6eDM/mRRgYLu1Saz6QISU2EhFIvP7OiuAXqKlQ8euF/mVuMDhNbDpXtx36O 12 | o0KplxLV1yn1gom4NxFBAb3LHnGjGeDCAe79RhbpqTmt/GU9qQgav3EGJtUWH5Ey 13 | PxpD1Z4llPAP/lfLFckzUcZUxuMwEWEKh9bt37eR51iPbfhvVQgt90Xc9lYyfHiu 14 | IcpJWIWKOoo3h3dQvlhEUEhzKdkVWuwl9ijQNdodm5dfdh0sW9kaIZnRfjS7fXb5 15 | 0RyoTUjO8LgjgotidRW2oQoQqBcRoEww0PU8RGTjkOzqZPqBkLBe8MG0wSnluJmQ 16 | vBrUOsgK5OxS1XW3cx6vgwCMuLG8L7bHgTrlvrm7skfA5gVV4hddkfcJgaS+DeyD 17 | WF+VATVfapYvqESgpxa/o6hX7E1pemHam++eR9dTyM8+n5X7ZUMQsSuyeEc1IN9G 18 | b01Zg/gWQC8dmFwtWnYGVcRY7ddH4nf/qALGaBeYdTu/sFVVe9zhM3UbWeS9HcHF 19 | C/cDkPmvd8aeOSoS7ZVtoF+j43EtNlLxMMo5qWvne3r+1gp2/Vxts1NWnpvQm+1a 20 | BVcBACDgUwQ7DmuWJwDm3I2ZZYxwBgZR8Qtr/NV4uUG5QR0CAwEAAaNTMFEwHQYD 21 | VR0OBBYEFF4/UDd0mwfi4xXUA/Sy9rN4CrG+MB8GA1UdIwQYMBaAFF4/UDd0mwfi 22 | 4xXUA/Sy9rN4CrG+MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggIB 23 | AHPHfz0vy+1Adw/TVPab6YT/7ztH5i+8V38kef/oaVlEkJFDGVyFrK0rjChACFk6 24 | nFA7G5k+Dmboo64dAJFZP+0+M2i8TAS6BToXQtNyoVqBAC2on2cyAb7fucdcy7sT 25 | NNdfiBEqGjPQoEY0xT1RIfyXP3PZHG9mNbMk9kR6l5eTACjRUYFhvX8lOOmKwgiR 26 | 38A4ggYP1Rn3igBd+A8Tdnsto21pl/BUFoZqiNcIRVcO4m0cEbCDk4cujNC4l2+e 27 | 4b9so0RsKdAXMx1k1PYUgIAEpHzfy+mN3n7f3UFQYF6qfrgkpudjnuM04OyImjR2 28 | K/MIDvCCTJRIOd8mEFzXztltb7gRUwTs0Sf8XMgiz+Px4WkXiPMTmt31InpLVpVd 29 | sEy37kJVzhRurkccwv11kaFQZdBwWsGInqHI0/91ySlxTajcZUMNTr7JaxYojV6f 30 | 7Tfx2ht/ytjyHRtanENZARpEVacKzdKm1HDLpk/fNAJhWA7+p7Nt0eBfvoSdpNZF 31 | gGcNDba1SlIz4sybie0yNE6NzBvt5dXUvzb65hqvKgc0lG+pAjYfdNaNh92NlVtw 32 | luHoZLdD3M213fTRxuj+zK/Zsc4is3C9LDC7xuZ/uY/Zq78NwuwxA+D3O2+kWhy7 33 | x4BKpSGiKDvk4bqtJbeJfRFDqzTYuBK8QK4lYFUWcAXu 34 | -----END TESTING CERTIFICATE-----`, "TESTING CERTIFICATE", "CERTIFICATE")) 35 | 36 | var TestTlsServerCert = []byte(strings.ReplaceAll(`-----BEGIN TESTING CERTIFICATE----- 37 | MIIFDjCCAvagAwIBAgIUaQoQ6KbHUT1wW9G6MWncqWZEiUYwDQYJKoZIhvcNAQEL 38 | BQAwJTEjMCEGA1UEAwwaZGFlbW9uOkNvbnRhaW5lciBkYWVtb24gQ0EwIBcNMDAw 39 | MTAxMDAwMDAwWhgPMjA5OTEyMzEwMDAwMDBaMCkxJzAlBgNVBAMMHmRhZW1vbjpD 40 | b250YWluZXIgZGFlbW9uIHNlcnZlcjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCC 41 | AgoCggIBALhPQp/b6kelKDXN5Rnr8zDQ2ACVO4jiX/14ylt9xOm5PqJfvm5MEeJ+ 42 | ese9Jwro8ZnqzrX53Cs1beMQc4UJxc6TyjUAhDj4riIL87xihQLl5fNdSw0KpgHr 43 | kdxNEIsaNV4CR/e6iHFfeV26/1727P4DJjoOBoC7dd3XuJYDIkpBbBrl7sJi6i4N 44 | lUrD7z/PAF9sKUw5H8/Iq6ZjYfiR07E+gyGH0vHTHJC7Iveisyonl6CR9Bvk0SW0 45 | 52wQPP1hs6nzK3Bdx7ylkoXcENb/dC+PGJYLfyrKEcJwGvyAutZYhqZ2zExv34xO 46 | u4B+n++4T1uYuMg4/Oe7+zlzbIAjZYp2JLK5Af77UT/IAfGKBchcZfBcNjHaqvpZ 47 | 5G0+TM2QmT2mEZGdNeAaHbnF2RiD/hGGr2I6Y2MmBc1MDn/oJtNdo7196CR9TLpl 48 | qtk4NMtCkXrD9ySWx2lO1X+sTBC0872+L9GdfIkKzm5bI1yp91cM9wEUEF1fNVJQ 49 | VaK3V5tVdTWcsIr8yEihcoOXYtiUtItMvmj5oPFBzDTnErTdkYR34NmHlnXg+Ipi 50 | d05IgODjnZu0IreDOd0+bsc+kWiEIm0ftfndngHFfPQCiNsz3GrbxUy5nCecHE4T 51 | +Ie3tWXEbxPCtfoDYengSOBeL51aixMNdJ6uy2H3KsfIlP+mLTRjAgMBAAGjMDAu 52 | MCwGA1UdEQQlMCOCCWxvY2FsaG9zdIcEfwAAAYcQAAAAAAAAAAAAAAAAAAAAATAN 53 | BgkqhkiG9w0BAQsFAAOCAgEAqtMkGk2dW1MpOGtzdXNRWsY/QgDVfT++ZE2d9neE 54 | R4Udv2TyXxq8m8paNm2Uj6tO4Rpw8j2A7Soi1UhawE6yj/vmi3oBT3LcufdXsUhz 55 | 907OqLFIgdmOKLV0JlfKWGh3aqtfQIZpb9Y940JTGkMRg9VYrgFJcnDBOy1qFd4b 56 | yXZhiSUHQvZKjqo8RffuLgrcUrZLlC1U9PgUsgB3Ab5dFUzis9T+TjNQPLgsCzyQ 57 | k6OxOiZFbK3qJgpghJ3FtP9tNSiIyHri1fS5daeRoM8vYWuggwMbxJdSIe1EhdOq 58 | muraHAiZWU12AuIADeHCbtPHh1dI2RpfV9rvFpX3pz4cKNmqDCobgkKbwP+9xuvr 59 | rEe9G59ZWuF1rMpYyE67iw9R7naqeA5BFNa3PYbtSATckiR9pMtjvSofQuwbZa/j 60 | e9kodp1TITVIIJKUpNZ46TcAWD3PkeTjNv+ZMPoo/XoHtl2ODC/LrEtJ8wK6yz11 61 | zjzfy0Q6kZ0Np2ejP2l1DjYQZtJSWgnGLBEg6Vn4htFODXVY2nts7US4I0W18Dxk 62 | 6hq02ehxadAmQl227iPekvo9OdkjmzbXpPrb0Ie2mw0rDmSSqkhW8lwzAUJm5225 63 | zh8tMVPWqvaoj3gYNJ5h+lYwwF6OdiRtDuuZJx9jkUpCWZigpg/zdGIBIzkUkrra 64 | 4HE= 65 | -----END TESTING CERTIFICATE-----`, "TESTING CERTIFICATE", "CERTIFICATE")) 66 | 67 | var TestTlsServerExpiredCert = []byte(strings.ReplaceAll(`-----BEGIN TESTING CERTIFICATE----- 68 | MIIFDDCCAvSgAwIBAgIUaQoQ6KbHUT1wW9G6MWncqWZEiUgwDQYJKoZIhvcNAQEL 69 | BQAwJTEjMCEGA1UEAwwaZGFlbW9uOkNvbnRhaW5lciBkYWVtb24gQ0EwHhcNMDAw 70 | MTAxMDAwMDAwWhcNMDAwMTAyMDAwMDAwWjApMScwJQYDVQQDDB5kYWVtb246Q29u 71 | dGFpbmVyIGRhZW1vbiBzZXJ2ZXIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK 72 | AoICAQC4T0Kf2+pHpSg1zeUZ6/Mw0NgAlTuI4l/9eMpbfcTpuT6iX75uTBHifnrH 73 | vScK6PGZ6s61+dwrNW3jEHOFCcXOk8o1AIQ4+K4iC/O8YoUC5eXzXUsNCqYB65Hc 74 | TRCLGjVeAkf3uohxX3lduv9e9uz+AyY6DgaAu3Xd17iWAyJKQWwa5e7CYuouDZVK 75 | w+8/zwBfbClMOR/PyKumY2H4kdOxPoMhh9Lx0xyQuyL3orMqJ5egkfQb5NEltOds 76 | EDz9YbOp8ytwXce8pZKF3BDW/3QvjxiWC38qyhHCcBr8gLrWWIamdsxMb9+MTruA 77 | fp/vuE9bmLjIOPznu/s5c2yAI2WKdiSyuQH++1E/yAHxigXIXGXwXDYx2qr6WeRt 78 | PkzNkJk9phGRnTXgGh25xdkYg/4Rhq9iOmNjJgXNTA5/6CbTXaO9fegkfUy6ZarZ 79 | ODTLQpF6w/cklsdpTtV/rEwQtPO9vi/RnXyJCs5uWyNcqfdXDPcBFBBdXzVSUFWi 80 | t1ebVXU1nLCK/MhIoXKDl2LYlLSLTL5o+aDxQcw05xK03ZGEd+DZh5Z14PiKYndO 81 | SIDg452btCK3gzndPm7HPpFohCJtH7X53Z4BxXz0AojbM9xq28VMuZwnnBxOE/iH 82 | t7VlxG8TwrX6A2Hp4EjgXi+dWosTDXSersth9yrHyJT/pi00YwIDAQABozAwLjAs 83 | BgNVHREEJTAjgglsb2NhbGhvc3SHBH8AAAGHEAAAAAAAAAAAAAAAAAAAAAEwDQYJ 84 | KoZIhvcNAQELBQADggIBAFnJ2CxUVI+eC49X8JuaWT8lhBbhoiG/OnivS9uu7Bv8 85 | gpHC37UFZEZ6iF64U14AFWR32+SYYmOBv4yDY0PQHfYm6Yq0zCgb8Zzl7HuEIzIP 86 | F8NAOstRSC7qxtDVqxdPXzueRhPjUu4/yFiXd3pef0m7VIlYOtAs5NR3aWUMZj4R 87 | /ZzFaeXJX9YZfuoifVGaIJjl1ociuPexisji/4P98gkGys5mBtuUVxNKzH8aJ5yN 88 | fZis7P0gIvuwvl5HcQwGIJPmJvkhPEYCLxwJH3zIIPaZmmzF52fZYilky85vv4VV 89 | HCX3KKXBpxpCy06SUsuK6bPG0pLg9AvU/r/s7Vk5VfOqKgpvd392VzTCGKpXVa8m 90 | kxDD+epiZ9rq6HX/j+4ZHQMtGb1CzG+0UT5YGBFlPEk0jnt6y5Bh3cn/WPWEZSM9 91 | EFaW2GgvRw6pyfwV1WgQX/JlVFXVmKFWh1XvcsxmkC6ViIUXbF+NuTNCB9l+P57V 92 | if00WrLnKqdzy46XNHFh6hIQHs62HAuLj6Pk4QahpJALyNsgzaGbEOiIjWuzf9LB 93 | JeO2Zr4Ga5u+qr/1zmNPjvh1rRmR35jYG6dDn971dwxRaBPkI2Dqf/hsobEhEOjM 94 | HOfyX94BD5J1x70esKVy1bQHqYKCt45q4k3kqvZqi9T/g+Uhw7MzOddQHBb/lEjs 95 | -----END TESTING CERTIFICATE-----`, "TESTING CERTIFICATE", "CERTIFICATE")) 96 | 97 | var TestTlsServerKey = []byte(strings.ReplaceAll(`-----BEGIN RSA TESTING KEY----- 98 | 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 147 | -----END RSA TESTING KEY-----`, "TESTING KEY", "PRIVATE KEY")) 148 | 149 | var TestTlsClientCert = []byte(strings.ReplaceAll(`-----BEGIN TESTING CERTIFICATE----- 150 | MIIE9TCCAt2gAwIBAgIUaQoQ6KbHUT1wW9G6MWncqWZEiUcwDQYJKoZIhvcNAQEL 151 | BQAwJTEjMCEGA1UEAwwaZGFlbW9uOkNvbnRhaW5lciBkYWVtb24gQ0EwIBcNMDAw 152 | MTAxMDAwMDAwWhgPMjA5OTEyMzEwMDAwMDBaMCkxJzAlBgNVBAMMHmRhZW1vbjpD 153 | b250YWluZXIgZGFlbW9uIGNsaWVudDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCC 154 | AgoCggIBANBvmLzdpymbQ11AHZDXzy2q0rjASOPIux3HMaH8edcvfc/GiOnrJK7u 155 | jyEkB+hOe39yrHpGHHjNCmOhGZgB1iQA2y7mq+6o65qNxDVae+F/36QX1xhpdH8p 156 | k2HNtjM39B9C6M9KFHGRf5IOCPeLicCrwtNIOcoGyzXf4JeZBug0kWR2MiPav4DZ 157 | dj1ojGK+hDbOslpKdbnoqv2TDEs1i2+XSDHVJVE64bH5KHqssNAYlMouVFzIOXjJ 158 | y8K9Q3qsweSdIgGI90dkSQ7tYmGZ7X5eiygd49VOE/omeC7la7KkU5z72tgD/AXR 159 | s5/J9AmStrXhIHbmL3rngRBhoLVo4qfk+Afu0HcXoSTI6jwdsnPcnUaICgFfjPmL 160 | 4x0/A8+lFVkyTYu4Wjw+CgTDneJcsbN+LQ3Lv3KNn3jozfG0M/trHhNSgEGJh7Ep 161 | chO1L3w1JrvVlIUcwEPshCNT0fcaVmdc24wN0BHBc4WoN/swlw12S5IGUTJe2FA4 162 | YkQz1PKv+EWfcprYwXX9q1HP6574V5+w6XKgS2+1BetCGM6/lxSnFGvzvZLooNvc 163 | nL6c5T1E0xXS3UthdUZq9DrmxerGGJg62e0CEpdlqdJYu1a3S6WcpiAHIG+UVd+t 164 | CUBO72FAwZK4Ast1NStHKxvN/LFDvKuoptpYdocFwVGR2d33CI4TAgMBAAGjFzAV 165 | MBMGA1UdJQQMMAoGCCsGAQUFBwMCMA0GCSqGSIb3DQEBCwUAA4ICAQCuUE9P35Vl 166 | vkPPK0RSPTZDnFiGo+9PK6qERFD6LO83aIS4lbXowEf5A4Tys1WMSVorfAL6RNp0 167 | ZlAXqHB1pI7sMn1KHWEaseZUIeVy4gym4Xy8UFAw+7EbOyp8iZqtJqKHaVqJe7ja 168 | ndu0MKGBErfPxAkN06Q1HBbWHCHr9dgTA5cf1U0Z8GCigM9An62XB8pCNkegjcO/ 169 | M9eG3Ou5DAeQZo5vkDsZg4kefoTe/kt2JnaCpvevqta1FO5Bpg4usBzsNRZTybod 170 | kBTwuSYfaYIHK+D45wSjKt9L0IDlbhFbgaLOhu7af3hqupc6s14UPGrqPCH6zUyi 171 | x0tMSc0c9DjMdwfUB97CUVvbiWklZMVKaaC5rj2Hft7+BkS7Z9jAeFa92NlfrbjM 172 | N0l9uVkSlYKPV0bYnwLkK2vPksuX4djICJncoWQCfXp7IcSQg/2uhHEADpUtkfBB 173 | DuMlihMgY+ksA+a6rsckQghZl4hL7pKFKzlR5UmLb5jVgtuQ3BvP6oA8zEY/9Zn3 174 | XEctAfOxQVXhtlmYDoT60UNVA5hr6z6T4/k2RW7IJKytI8ipNyGe0WX+O070nnNQ 175 | VOinuhOuStrUkAhqCdFZzockO6+0DlC1fIsf+1cJt7n7mL61Pm37fyKQ1U//micD 176 | rV7f2SB5Nh998QWpEu1xzyKt+3b0+sWOEg== 177 | -----END TESTING CERTIFICATE-----`, "TESTING CERTIFICATE", "CERTIFICATE")) 178 | 179 | var TestTlsClientKey = []byte(strings.ReplaceAll(`-----BEGIN RSA TESTING KEY----- 180 | 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 229 | -----END RSA TESTING KEY-----`, "TESTING KEY", "PRIVATE KEY")) 230 | 231 | var TestAltTlsCacert = []byte(strings.ReplaceAll(`-----BEGIN TESTING CERTIFICATE----- 232 | MIIFPTCCAyWgAwIBAgIUXcQjjHmmGiXt+0qoohb54fCo9M8wDQYJKoZIhvcNAQEL 233 | BQAwLTErMCkGA1UEAwwiYWx0LWRhZW1vbjpDb250YWluZXIgYWx0IGRhZW1vbiBD 234 | QTAgFw0wMDAxMDEwMDAwMDBaGA8yMDk5MTIzMTAwMDAwMFowLTErMCkGA1UEAwwi 235 | YWx0LWRhZW1vbjpDb250YWluZXIgYWx0IGRhZW1vbiBDQTCCAiIwDQYJKoZIhvcN 236 | AQEBBQADggIPADCCAgoCggIBANgbfL4Nsw+1pcFFlnUVybpDvYHoP/ybWdcq+SIr 237 | zDQAiJ1C1gSGoTSoOkkoMzlnMs5CJEwWJEVzFYuzCX73dVWdOPmqvqakrQvQQN/k 238 | 10GaNtCEjvWBiAsuk4UegdjiQUX89eEH1TPZmlT7Wj49k3pwMJTgd1FeLztgSgBU 239 | Yv7ctT2T/dcLfduTP9AZIHeSASRnX3eFywbuGRX9zSLuXzzo4oGsYtzfmK/OMpJ7 240 | pqrvunzYvFVGDGWfNUxRjYHjJ26Lx7f81GEO7AHVkl8m8+KHBsKN09OO+lRVhrhT 241 | x8Tgrv9UQv1FtQ2CeIbm93wGzw86nkRkHoi98t4vNFKKYdnwW5AnMczmc3xa7VKu 242 | GRzPLvjbPw33U5vWBtZ7Pb0y7nWO7Cipn4B9LnOX3rxA01ZsY0I+1rWnzGUjf2xx 243 | ZTZZlTG8t0/nfaLtDl8oIRwsbrkdEk3G8Or2BveFU/yhGrq4LBbjGZzb3HywZvIS 244 | 84XBSEof0JhBC7gGvo7DpE4LsgDtu4x9VjmMWY4Bfx2Qhj2J4jHHfKrF2QPN7Ucy 245 | nHBsdI5Vn9CyHq0eyui1P5vEHUiVBli6c0L0MDdLXfIpFUmAPweOaYVj36I1nhzK 246 | BwysPdn40cy3bMbrItwsghl0AOwdphg7Sk5YMHhE3GfW+LiZpEyvCkh3qHfe9Fxd 247 | zGMnAgMBAAGjUzBRMB0GA1UdDgQWBBQxudLRWwPnX0WzPh40XRx+ycQdYTAfBgNV 248 | HSMEGDAWgBQxudLRWwPnX0WzPh40XRx+ycQdYTAPBgNVHRMBAf8EBTADAQH/MA0G 249 | CSqGSIb3DQEBCwUAA4ICAQB5W9Ul08Iw7FuTulc4Q/yKmW9Zc5g5S2J//nLUwEEh 250 | oV8eP9nAqhb0GDBo9x3fWYutarxmLv6u+6CxWKhZGVbSF+FpsLmch9vIvxv2+iPC 251 | uUIlHYs2LfoTDlw6ZF5VKBWUOfZPFGxQevW8/B+jBCIqfk3U5c6AsCDnmeWPv/ul 252 | gUvJvbCydwvE8DNQzm8sn5Zfpfs8ljjF9DoKMZg1zft7t9jqyB9LST2njXriqfIW 253 | XBouA+AbKRphaIX3m6EcfzpppVVwcRnTHkX9flOJRAdy9hnvGcwQwv0JtqclqDzN 254 | McZ5PqnuVT39Nd5VbAZ4U14+tLAISkplH6p/rmhLNXSaA7GQUhdcd/saKyVaQXZH 255 | qGtrdsUGW4IYmMUYjYH7WwIPBno/DrZxU77dqacxpGjY1/0hMNbfw09kvvdrCSN2 256 | C2ZxLPjYskoT714v6pJYWh4BUozrpvEi5f9oF4smCA6ZifxUxe7hody7aLGgLY+T 257 | C8sxtZouVsBTBbDCoMh89vzMcrIFo5+VJ9ZKwZcvA4tm1m5x7nt/cmAIyVsrGjsR 258 | X4sQiEYi/Bu3Y8jXYRjcLmK0lxotMYY+crAgvXPIFvlh2wAlFla411fRO+8/17Ls 259 | +UhcjSk9Vx9ezhvPc3ZCQgqTlBOluEwO2laB3pUIaq5lvrnLPG+0Oq/3XZFwRFRd 260 | Tw== 261 | -----END TESTING CERTIFICATE-----`, "TESTING CERTIFICATE", "CERTIFICATE")) 262 | 263 | var TestAltTlsServerCert = []byte(strings.ReplaceAll(`-----BEGIN TESTING CERTIFICATE----- 264 | MIIFHjCCAwagAwIBAgIUVermdqhsr3w99Gt1XgafVGnywfQwDQYJKoZIhvcNAQEL 265 | BQAwLTErMCkGA1UEAwwiYWx0LWRhZW1vbjpDb250YWluZXIgYWx0IGRhZW1vbiBD 266 | QTAgFw0wMDAxMDEwMDAwMDBaGA8yMDk5MTIzMTAwMDAwMFowMTEvMC0GA1UEAwwm 267 | YWx0LWRhZW1vbjpDb250YWluZXIgYWx0IGRhZW1vbiBzZXJ2ZXIwggIiMA0GCSqG 268 | SIb3DQEBAQUAA4ICDwAwggIKAoICAQDshQ96YcWdIKlcOUQK3NE0vwjMr5Nv/HyK 269 | gZeU+y0nLTy89x2qzsSAh1bb9+Agskr1I+fRHGc0uVQlMJcKsWHczi8yBgZsKpzb 270 | K0c1XGsBtGAXIbcizJIij+MOmKiRYQiDWvGtFqEy5XTpmnBmhGRDG7l9KN8vcL3X 271 | ga0VgNXdYgghOt148GwoZt9p9L6/i02qL8L6966XFtC/MyA0hHoixy4LvJn+QBtq 272 | nQPI0dWMG8dAgAcwxcqzX3k9x9PxQCWHAMAR9Rww1j8A0hFFfZKl/mx+1NjkXilM 273 | m9jDDwy7LwbkzfqLT5i0isqRy3hKGuXUa/PqKO9Qc8mzo40NGBQVDp2MiQhxcpCg 274 | rDViyzJEGf5/fxseDNqNxB186U3/x8wOsfzg6Y0je9xOf/36gUlpj/zOZVZm8Fia 275 | zqhjZiEVxBFFPNswv1qHtwF194rSWWSkaEWnh5h2rmsCM90XzQDOc9CfGBsRTEOu 276 | u86mfKfUEUK/tTW4yfIIReGsrbmDm+tV2UXEuHSyMRvHJnqe4d/jBQzbyxvnnD89 277 | Z2SPwewnWmyPadmSVrd0bqZrLYiVcSlteIw5O/VZptsQp7Mrz8odLqCLHBKkJEvO 278 | LU050bhqDY2Mg9umzyhW/0lK6YHCcYFbEDfrL8l8gbvaYMoD3EN/32GdycQUojHc 279 | 3p/Ol9Bz3wIDAQABozAwLjAsBgNVHREEJTAjgglsb2NhbGhvc3SHBH8AAAGHEAAA 280 | AAAAAAAAAAAAAAAAAAEwDQYJKoZIhvcNAQELBQADggIBAKtjMpHkURFtO0YgqoPM 281 | MSgdFC17DHET8hNHNsQ6lO/5Q+pmtCHP79jA6/YIQkVFUGWVnQTL281vxBfsVx7H 282 | QI7UgDcEHlMQdmUAYXF5UxWqGVfLkOFICRzXuiXbAR/WcmCx+clZbKOp5kewi6Nh 283 | o0eLj/cVnrqwpmEOzK6Vob7XIuOOTKZhFKDLJ145QvDhaOOHwbHSU6nM3ivaB7xS 284 | WrDA3gpFmh6VweuICvcokYryIBPTuWnIGIw0OI0YML8hYHBJht1fYZl45yCaDCq3 285 | qP1F+qEaz9OL9IDm+nJSchBqHAVpudTAfvsZ3H7OtbiQxdmGW9bWLqAPdqgmECfm 286 | WxOQq2pF41cwGon9tD3uW8z6qxOu8ew0Xpcl0ivm5X/vhrugN9woVH38IbMkjJ8h 287 | ZJWl43PKonb9t/sQioOH/duHQBEOJmcanZCJFz0keThAPUmb8Dxknu3a3LJbL1lA 288 | 6vmt3/GaFPdarotdeMhkcxL3hxPZwOyXYOBEopowHGzvdw6f7O3xtOTAyLFPur+M 289 | sMHoeKAv69UzEnubHFkCMa6SNy8wfmvIWzUE4fj+/T23osqoNcJcF104uhxJ9SIk 290 | DkfkdQnUJgVkvonTWwQ9MaOSvCOgdC3XhZMyyGZ2EzlN8xQkBV/bNJ4fCZRJESES 291 | LWOPYGcBo03d5eIMZw4hLxiM 292 | -----END TESTING CERTIFICATE-----`, "TESTING CERTIFICATE", "CERTIFICATE")) 293 | 294 | var TestAltTlsServerKey = []byte(strings.ReplaceAll(`-----BEGIN RSA TESTING KEY----- 295 | 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 344 | -----END RSA TESTING KEY-----`, "TESTING KEY", "PRIVATE KEY")) 345 | 346 | var TestAltTlsClientCert = []byte(strings.ReplaceAll(`-----BEGIN TESTING CERTIFICATE----- 347 | MIIFBTCCAu2gAwIBAgIUVermdqhsr3w99Gt1XgafVGnywfUwDQYJKoZIhvcNAQEL 348 | BQAwLTErMCkGA1UEAwwiYWx0LWRhZW1vbjpDb250YWluZXIgYWx0IGRhZW1vbiBD 349 | QTAgFw0wMDAxMDEwMDAwMDBaGA8yMDk5MTIzMTAwMDAwMFowMTEvMC0GA1UEAwwm 350 | YWx0LWRhZW1vbjpDb250YWluZXIgYWx0IGRhZW1vbiBjbGllbnQwggIiMA0GCSqG 351 | SIb3DQEBAQUAA4ICDwAwggIKAoICAQCehYHfqsoK4V8uxxGm5LNe+XCcLnSAvmlq 352 | +tpb97bvYwa/falscAk2lSp3lJdyH8ZURykzdowpaRc3J3/IipaNzGYHitco9anO 353 | unusIpFTZMQvYAeBRiTaaMAS1LKCTAHgJayRJC13TFZfhuNc+BNiFx2g1WQHBTC7 354 | 6iIeI2BqZzO+irTdVDAMvaKEHdff6OpsmQtwPQVLw/l+1ObUfrzRy3KfpIi9n19b 355 | A7jgdgQN323JpHfpXbtM82r773wmWY496GzYGtL3N3L2EfEEzvFf/4yCfE5Tc4Za 356 | hnMQCCYHn9RgSlSJwggjUxIlI4Exk5ljM43MbI8XXUfzcH++soK64SgiewWCwkGS 357 | Eo8yyiFRAmuleM7NDzeejXpTHz89Z7yDVQxpadT+9H52vNmWVnSon7+JGc6OMMM+ 358 | DK6U1cCF4vnlyivuPDrtCu8d3e14DuOIja3fP2G/Fqz0FJ5fsy3UY3Fdyi+tw1lp 359 | aK3J4hZ4AM09TUFf/2gWIxOWvd09qeWE1OfFUEhnhDDwiIvONfLWJ3sUWia1dNfe 360 | bK2/n+FhTGsdMo9LRqD0HTnA6ZRdzRhvLCC/13qtinGFuAzl6xUJcL6AD/bfLM8i 361 | Q4SIe8fgFInny3zsV0G1t2e02mhPRomhdWNpG9IPwIbEJbdK1lXzKUPikWZaTdR8 362 | 8PgqUNp3WQIDAQABoxcwFTATBgNVHSUEDDAKBggrBgEFBQcDAjANBgkqhkiG9w0B 363 | AQsFAAOCAgEAEqDLFVj0B9pswGq6wmStEnHA6wAIRDuUWTvz2YvWGQD1kHU41Yac 364 | Y0x7JVG42xY60SWDVcibdEtJo5Qsx2E3nkHdFhz1z4JRmR0ml+9/E4j1RWQ1g6uN 365 | oZAbPGhR5IwcpdJoFDTfH4McF20Nivp9TlgAs5iqzPbhcCIiXXoNU9BReFwjNrql 366 | h9obrgh0mJxeYGQBM3lnQv4wG278mUnUa1eE2mev0awXLn18E4AvZ00TOPPS73S3 367 | Q5APCtQUIUAB8ZJcUMYgE1BHR6+GKp8IjgcbbjXruoD6vqsowmKjpPaHcSw47Vlt 368 | qWEseyXLHz/2dVSMrhdb99OVuSxRbA4beUlUh9+eb2Qf/RHOeJ7KLRaPWB191qSB 369 | GcklHrf9hL/3ROooyHy0+D90bvm/uPwBs/D8vVQhg8zrV+EyNy9XvZ9TcgWj24aE 370 | vM/mlNlsdEQzqsGOx7YhGWEBs7AFPtfjd0b7YFWSPeMCGLpqK52toyG+8AEOtDKX 371 | 3Ol7yONrNgeM7QNTK/aPlNGS9YJxIJ7GEHvcz82IGPTlqoovhOVoskiElsUAHHLw 372 | wfaKgCz6gBF6c8LNbA7jEcInbqEn+fOHh4yuUUcMgEFKtrIqbEipeGxhwMUKWIgL 373 | NB94p7TjcLm9gthIWMCpiS/3A6LwkZ55+kiVUbmwG2U8WQESrXcdMYQ= 374 | -----END TESTING CERTIFICATE-----`, "TESTING CERTIFICATE", "CERTIFICATE")) 375 | 376 | var TestAltTlsClientKey = []byte(strings.ReplaceAll(`-----BEGIN RSA TESTING KEY----- 377 | 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 426 | -----END RSA TESTING KEY-----`, "TESTING KEY", "PRIVATE KEY")) 427 | 428 | var TestInvalidTlsCacert = []byte(strings.ReplaceAll(`-----BEGIN TESTING CERTIFICATE----- 429 | INVALID CA CERT 430 | -----END TESTING CERTIFICATE-----`, "TESTING CERTIFICATE", "CERTIFICATE")) 431 | 432 | var TestInvalidTlsServerCert = []byte(strings.ReplaceAll(`-----BEGIN TESTING CERTIFICATE----- 433 | INVALID SERVER CERT 434 | -----END TESTING CERTIFICATE-----`, "TESTING CERTIFICATE", "CERTIFICATE")) 435 | 436 | var TestInvalidTlsServerKey = []byte(strings.ReplaceAll(`-----BEGIN RSA TESTING KEY----- 437 | INVALID SERVER KEY 438 | -----END RSA TESTING KEY-----`, "TESTING KEY", "PRIVATE KEY")) 439 | 440 | var TestInvalidTlsClientCert = []byte(strings.ReplaceAll(`-----BEGIN TESTING CERTIFICATE----- 441 | INVALID CLIENT CERT 442 | -----END TESTING CERTIFICATE-----`, "TESTING CERTIFICATE", "CERTIFICATE")) 443 | 444 | var TestInvalidTlsClientKey = []byte(strings.ReplaceAll(`-----BEGIN RSA TESTING KEY----- 445 | INVALID CLIENT KEY 446 | -----END RSA TESTING KEY-----`, "TESTING KEY", "PRIVATE KEY")) 447 | -------------------------------------------------------------------------------- /cmd/cetusguard/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | "strings" 8 | 9 | "github.com/hectorm/cetusguard/cetusguard" 10 | "github.com/hectorm/cetusguard/internal/logger" 11 | "github.com/hectorm/cetusguard/internal/utils/env" 12 | "github.com/hectorm/cetusguard/internal/utils/flagextra" 13 | ) 14 | 15 | var ( 16 | version = "dev" 17 | author = "H\u00E9ctor Molinero Fern\u00E1ndez " 18 | license = "MIT, https://opensource.org/licenses/MIT" 19 | repository = "https://github.com/hectorm/cetusguard" 20 | ) 21 | 22 | func main() { 23 | var backendAddr string 24 | flag.StringVar( 25 | &backendAddr, 26 | "backend-addr", 27 | env.StringEnv("unix:///var/run/docker.sock", "CETUSGUARD_BACKEND_ADDR", "CONTAINER_HOST", "DOCKER_HOST"), 28 | "Container daemon socket to connect to (env CETUSGUARD_BACKEND_ADDR, CONTAINER_HOST, DOCKER_HOST)", 29 | ) 30 | 31 | var frontendAddr []string 32 | flag.Var( 33 | flagextra.NewStringSliceValue(env.StringSliceEnv([]string{"tcp://127.0.0.1:2375"}, "CETUSGUARD_FRONTEND_ADDR"), &frontendAddr), 34 | "frontend-addr", 35 | "Address to bind the server to, can be specified multiple times (env CETUSGUARD_FRONTEND_ADDR)", 36 | ) 37 | 38 | var backendTlsCacert string 39 | flag.StringVar( 40 | &backendTlsCacert, 41 | "backend-tls-cacert", 42 | env.StringEnv("", "CETUSGUARD_BACKEND_TLS_CACERT"), 43 | "Path to the backend TLS certificate used to verify the daemon identity (env CETUSGUARD_BACKEND_TLS_CACERT)", 44 | ) 45 | 46 | var backendTlsCert string 47 | flag.StringVar( 48 | &backendTlsCert, 49 | "backend-tls-cert", 50 | env.StringEnv("", "CETUSGUARD_BACKEND_TLS_CERT"), 51 | "Path to the backend TLS certificate used to authenticate with the daemon (env CETUSGUARD_BACKEND_TLS_CERT)", 52 | ) 53 | 54 | var backendTlsKey string 55 | flag.StringVar( 56 | &backendTlsKey, 57 | "backend-tls-key", 58 | env.StringEnv("", "CETUSGUARD_BACKEND_TLS_KEY"), 59 | "Path to the backend TLS key used to authenticate with the daemon (env CETUSGUARD_BACKEND_TLS_KEY)", 60 | ) 61 | 62 | var frontendTlsCacert string 63 | flag.StringVar( 64 | &frontendTlsCacert, 65 | "frontend-tls-cacert", 66 | env.StringEnv("", "CETUSGUARD_FRONTEND_TLS_CACERT"), 67 | "Path to the frontend TLS certificate used to verify the identity of clients (env CETUSGUARD_FRONTEND_TLS_CACERT)", 68 | ) 69 | 70 | var frontendTlsCert string 71 | flag.StringVar( 72 | &frontendTlsCert, 73 | "frontend-tls-cert", 74 | env.StringEnv("", "CETUSGUARD_FRONTEND_TLS_CERT"), 75 | "Path to the frontend TLS certificate (env CETUSGUARD_FRONTEND_TLS_CERT)", 76 | ) 77 | 78 | var frontendTlsKey string 79 | flag.StringVar( 80 | &frontendTlsKey, 81 | "frontend-tls-key", 82 | env.StringEnv("", "CETUSGUARD_FRONTEND_TLS_KEY"), 83 | "Path to the frontend TLS key (env CETUSGUARD_FRONTEND_TLS_KEY)", 84 | ) 85 | 86 | var ruleList []string 87 | flag.Var( 88 | flagextra.NewStringSliceValue(env.StringSliceEnv(nil, "CETUSGUARD_RULES"), &ruleList), 89 | "rules", 90 | "Filter rules separated by new lines, can be specified multiple times (env CETUSGUARD_RULES)", 91 | ) 92 | 93 | var ruleFileList []string 94 | flag.Var( 95 | flagextra.NewStringSliceValue(env.StringSliceEnv(nil, "CETUSGUARD_RULES_FILE"), &ruleFileList), 96 | "rules-file", 97 | "Filter rules file, can be specified multiple times (env CETUSGUARD_RULES_FILE)", 98 | ) 99 | 100 | var noBuiltinRules bool 101 | flag.BoolVar( 102 | &noBuiltinRules, 103 | "no-builtin-rules", 104 | env.BoolEnv(false, "CETUSGUARD_NO_BUILTIN_RULES"), 105 | "Do not load the built-in rules (env CETUSGUARD_NO_BUILTIN_RULES)", 106 | ) 107 | 108 | var logLevel int 109 | flag.IntVar( 110 | &logLevel, 111 | "log-level", 112 | env.IntEnv(logger.LvlInfo, "CETUSGUARD_LOG_LEVEL"), 113 | fmt.Sprintf("The minimum entry level to log, from %d to %d (env CETUSGUARD_LOG_LEVEL)", logger.LvlNone, logger.LvlDebug), 114 | ) 115 | 116 | var printVersion bool 117 | flag.BoolVar( 118 | &printVersion, 119 | "version", 120 | false, 121 | "Show version number and quit", 122 | ) 123 | 124 | flag.Parse() 125 | logger.SetLevel(logLevel) 126 | 127 | if printVersion { 128 | fmt.Printf("CetusGuard %s\n", version) 129 | fmt.Printf("Author: %s\n", author) 130 | fmt.Printf("License: %s\n", license) 131 | fmt.Printf("Repository: %s\n", repository) 132 | os.Exit(0) 133 | } 134 | 135 | var rules []cetusguard.Rule 136 | if !noBuiltinRules { 137 | rawRules := strings.Join(cetusguard.RawBuiltinRules, "\n") 138 | builtRules, err := cetusguard.BuildRules(rawRules) 139 | if err != nil { 140 | logger.Critical(err) 141 | } 142 | rules = append(rules, builtRules...) 143 | } 144 | for _, ruleElem := range ruleList { 145 | builtRules, err := cetusguard.BuildRules(ruleElem) 146 | if err != nil { 147 | logger.Critical(err) 148 | } 149 | rules = append(rules, builtRules...) 150 | } 151 | for _, ruleFileElem := range ruleFileList { 152 | builtRules, err := cetusguard.BuildRulesFromFilePath(ruleFileElem) 153 | if err != nil { 154 | logger.Critical(err) 155 | } 156 | rules = append(rules, builtRules...) 157 | } 158 | 159 | cg := &cetusguard.Server{ 160 | Backend: &cetusguard.Backend{ 161 | Addr: backendAddr, 162 | TlsCacert: backendTlsCacert, 163 | TlsCert: backendTlsCert, 164 | TlsKey: backendTlsKey, 165 | }, 166 | Frontend: &cetusguard.Frontend{ 167 | Addr: frontendAddr, 168 | TlsCacert: frontendTlsCacert, 169 | TlsCert: frontendTlsCert, 170 | TlsKey: frontendTlsKey, 171 | }, 172 | Rules: rules, 173 | } 174 | 175 | ready := make(chan any, 1) 176 | err := cg.Start(ready) 177 | if err != nil { 178 | logger.Critical(err) 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /e2e/cli.patch: -------------------------------------------------------------------------------- 1 | diff --git a/e2e/compose-env.yaml b/e2e/compose-env.yaml 2 | index 2173f0e668..d9d2303ba7 100644 3 | --- a/e2e/compose-env.yaml 4 | +++ b/e2e/compose-env.yaml 5 | @@ -6,2 +6,13 @@ services: 6 | engine: 7 | + image: 'localhost.test/cetusguard:${TEST_ID:?}' 8 | + restart: 'on-failure' 9 | + logging: { driver: 'journald', options: { tag: '${TEST_ID:?}' } } 10 | + read_only: true 11 | + environment: 12 | + CETUSGUARD_BACKEND_ADDR: 'tcp://engine-shaded:2375' 13 | + CETUSGUARD_FRONTEND_ADDR: 'tcp://:2375' 14 | + CETUSGUARD_RULES: 'GET,HEAD,POST,PUT,DELETE /.+' 15 | + CETUSGUARD_LOG_LEVEL: '7' 16 | + 17 | + engine-shaded: 18 | image: 'docker:${ENGINE_VERSION:-28}-dind' 19 | -------------------------------------------------------------------------------- /e2e/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -eu 4 | export LC_ALL='C' 5 | 6 | SCRIPT_DIR="$(CDPATH='' cd -- "$(dirname -- "${0:?}")" && pwd -P)" 7 | 8 | CLI_TREEISH='v28.0.1' 9 | CLI_REMOTE='https://github.com/docker/cli.git' 10 | CLI_PATCH="${SCRIPT_DIR:?}/cli.patch" 11 | CLI_DIR="$(mktemp -d)" 12 | 13 | TEST_ID="e2e-$(date -u +'%Y%m%d%H%M%S')" 14 | 15 | cleanup() { ret="$?"; rm -rf "${CLI_DIR:?}"; trap - EXIT; exit "${ret:?}"; } 16 | trap cleanup EXIT TERM INT HUP 17 | 18 | main() { 19 | git -C "${CLI_DIR:?}" init --quiet 20 | git -C "${CLI_DIR:?}" remote add origin "${CLI_REMOTE:?}" 21 | git -C "${CLI_DIR:?}" fetch --depth=1 origin "${CLI_TREEISH:?}" 22 | git -C "${CLI_DIR:?}" checkout FETCH_HEAD 23 | git -C "${CLI_DIR:?}" submodule update --init --recursive --depth=1 24 | git -C "${CLI_DIR:?}" apply -v "${CLI_PATCH:?}" 25 | 26 | printf 'TEST_ID=%s\n' "${TEST_ID:?}" > "${CLI_DIR:?}"/.env 27 | docker build --tag localhost.test/cetusguard:"${TEST_ID:?}" "${SCRIPT_DIR:?}"/../ 28 | ( cd "${CLI_DIR:?}"; make -f "${CLI_DIR:?}"/docker.Makefile test-e2e-non-experimental; ) || ret="$?" 29 | 30 | journalctl --no-pager --output=cat CONTAINER_TAG="${TEST_ID:?}" 31 | test -n "$(journalctl --output=cat CONTAINER_TAG="${TEST_ID:?}" | head -n1)" 32 | test -z "$(journalctl --output=cat CONTAINER_TAG="${TEST_ID:?}" | grep -v '^\(WARNING\|INFO\|DEBUG\):')" 33 | 34 | exit "${ret:-0}" 35 | } 36 | 37 | main "${@-}" 38 | -------------------------------------------------------------------------------- /examples/compose/.gitignore: -------------------------------------------------------------------------------- 1 | /certs/ 2 | -------------------------------------------------------------------------------- /examples/compose/compose.netdata.yml: -------------------------------------------------------------------------------- 1 | services: 2 | 3 | cetusguard: 4 | container_name: "cetusguard" 5 | image: "docker.io/hectorm/cetusguard:v1" 6 | restart: "on-failure" 7 | read_only: true 8 | networks: 9 | - "cetusguard" 10 | volumes: 11 | - "/var/run/docker.sock:/var/run/docker.sock:ro" 12 | environment: 13 | CETUSGUARD_BACKEND_ADDR: "unix:///var/run/docker.sock" 14 | CETUSGUARD_FRONTEND_ADDR: "tcp://:2375" 15 | CETUSGUARD_RULES: | 16 | ! List images 17 | GET %API_PREFIX_IMAGES%/json 18 | ! List containers 19 | GET %API_PREFIX_CONTAINERS%/json 20 | ! Inspect a container 21 | GET %API_PREFIX_CONTAINERS%/%CONTAINER_ID_OR_NAME%/json 22 | CETUSGUARD_LOG_LEVEL: "7" 23 | 24 | netdata: 25 | container_name: "cetusguard-netdata" 26 | image: "docker.io/netdata/netdata:v2" 27 | restart: "on-failure" 28 | cap_add: 29 | - "SYS_PTRACE" 30 | security_opt: 31 | - "apparmor=unconfined" 32 | networks: 33 | - "cetusguard" 34 | ports: 35 | - "127.0.0.1:19999:19999/tcp" 36 | volumes: 37 | - "/etc/os-release:/host/etc/os-release:ro" 38 | - "/etc/passwd:/host/etc/passwd:ro" 39 | - "/etc/group:/host/etc/group:ro" 40 | - "/proc/:/host/proc/:ro" 41 | - "/sys/:/host/sys/:ro" 42 | environment: 43 | DOCKER_HOST: "cetusguard:2375" 44 | 45 | networks: 46 | 47 | cetusguard: 48 | name: "cetusguard" 49 | -------------------------------------------------------------------------------- /examples/compose/compose.podman.yml: -------------------------------------------------------------------------------- 1 | services: 2 | 3 | cetusguard: 4 | container_name: "cetusguard" 5 | image: "docker.io/hectorm/cetusguard:v1" 6 | restart: "on-failure" 7 | read_only: true 8 | networks: 9 | - "cetusguard-private" 10 | - "cetusguard-public" 11 | volumes: 12 | - "./rules.list:/rules.list:ro" 13 | environment: 14 | CETUSGUARD_BACKEND_ADDR: "tcp://podmand:2375" 15 | CETUSGUARD_FRONTEND_ADDR: "tcp://:2375" 16 | CETUSGUARD_RULES_FILE: "/rules.list" 17 | CETUSGUARD_LOG_LEVEL: "7" 18 | depends_on: 19 | - "podmand" 20 | 21 | podmand: 22 | container_name: "cetusguard-podmand" 23 | image: "quay.io/podman/stable:v5" 24 | restart: "on-failure" 25 | privileged: true 26 | networks: 27 | - "cetusguard-private" 28 | entrypoint: "/usr/bin/podman" 29 | command: ["system", "service", "--time=0", "tcp://:2375"] 30 | 31 | podman: 32 | container_name: "cetusguard-podman" 33 | image: "quay.io/podman/stable:v5" 34 | restart: "on-failure" 35 | networks: 36 | - "cetusguard-public" 37 | environment: 38 | CONTAINER_HOST: "tcp://cetusguard:2375" 39 | entrypoint: "/bin/sh" 40 | command: ["-c", "trap : TERM INT; while :; do sleep 60; done & wait"] 41 | depends_on: 42 | - "cetusguard" 43 | 44 | networks: 45 | 46 | cetusguard-private: 47 | name: "cetusguard-private" 48 | 49 | cetusguard-public: 50 | name: "cetusguard-public" 51 | -------------------------------------------------------------------------------- /examples/compose/compose.socket-socket.yml: -------------------------------------------------------------------------------- 1 | services: 2 | 3 | cetusguard: 4 | container_name: "cetusguard" 5 | image: "docker.io/hectorm/cetusguard:v1" 6 | restart: "on-failure" 7 | read_only: true 8 | networks: 9 | - "cetusguard-private" 10 | - "cetusguard-public" 11 | volumes: 12 | - "./rules.list:/rules.list:ro" 13 | - "cetusguard-socket:/sockets/cetusguard/" 14 | - "dockerd-socket:/sockets/docker/:ro" 15 | environment: 16 | CETUSGUARD_BACKEND_ADDR: "unix:///sockets/docker/docker.sock" 17 | CETUSGUARD_FRONTEND_ADDR: "unix:///sockets/cetusguard/cetusguard.sock" 18 | CETUSGUARD_RULES_FILE: "/rules.list" 19 | CETUSGUARD_LOG_LEVEL: "7" 20 | depends_on: 21 | - "dockerd" 22 | 23 | dockerd: 24 | container_name: "cetusguard-dockerd" 25 | image: "docker.io/docker:28-dind" 26 | restart: "on-failure" 27 | privileged: true 28 | networks: 29 | - "cetusguard-private" 30 | volumes: 31 | - "./certs/server/:/certs/server/:ro" 32 | - "dockerd-socket:/sockets/docker/" 33 | environment: 34 | DOCKER_HOST: "unix:///sockets/docker/docker.sock" 35 | DOCKER_TLS_CERTDIR: "/certs/" 36 | 37 | docker: 38 | container_name: "cetusguard-docker" 39 | image: "docker.io/docker:28-cli" 40 | restart: "on-failure" 41 | networks: 42 | - "cetusguard-public" 43 | volumes: 44 | - "cetusguard-socket:/sockets/cetusguard/:ro" 45 | environment: 46 | DOCKER_HOST: "unix:///sockets/cetusguard/cetusguard.sock" 47 | entrypoint: "/bin/sh" 48 | command: ["-c", "trap : TERM INT; while :; do sleep 60; done & wait"] 49 | depends_on: 50 | - "cetusguard" 51 | 52 | networks: 53 | 54 | cetusguard-private: 55 | name: "cetusguard-private" 56 | 57 | cetusguard-public: 58 | name: "cetusguard-public" 59 | 60 | volumes: 61 | 62 | cetusguard-socket: 63 | name: "cetusguard-socket" 64 | 65 | dockerd-socket: 66 | name: "cetusguard-dockerd-socket" 67 | -------------------------------------------------------------------------------- /examples/compose/compose.socket-tcptls.yml: -------------------------------------------------------------------------------- 1 | services: 2 | 3 | cetusguard: 4 | container_name: "cetusguard" 5 | image: "docker.io/hectorm/cetusguard:v1" 6 | restart: "on-failure" 7 | read_only: true 8 | networks: 9 | - "cetusguard-private" 10 | - "cetusguard-public" 11 | volumes: 12 | - "./rules.list:/rules.list:ro" 13 | - "./certs/server/:/certs/server/:ro" 14 | - "dockerd-socket:/sockets/docker/:ro" 15 | environment: 16 | CETUSGUARD_BACKEND_ADDR: "unix:///sockets/docker/docker.sock" 17 | CETUSGUARD_FRONTEND_ADDR: "tcp://:2376" 18 | CETUSGUARD_FRONTEND_TLS_CACERT: "/certs/server/ca.pem" 19 | CETUSGUARD_FRONTEND_TLS_CERT: "/certs/server/cert.pem" 20 | CETUSGUARD_FRONTEND_TLS_KEY: "/certs/server/key.pem" 21 | CETUSGUARD_RULES_FILE: "/rules.list" 22 | CETUSGUARD_LOG_LEVEL: "7" 23 | depends_on: 24 | - "dockerd" 25 | 26 | dockerd: 27 | container_name: "cetusguard-dockerd" 28 | image: "docker.io/docker:28-dind" 29 | restart: "on-failure" 30 | privileged: true 31 | networks: 32 | - "cetusguard-private" 33 | volumes: 34 | - "./certs/server/:/certs/server/:ro" 35 | - "dockerd-socket:/sockets/docker/" 36 | environment: 37 | DOCKER_HOST: "unix:///sockets/docker/docker.sock" 38 | DOCKER_TLS_CERTDIR: "/certs/" 39 | 40 | docker: 41 | container_name: "cetusguard-docker" 42 | image: "docker.io/docker:28-cli" 43 | restart: "on-failure" 44 | networks: 45 | - "cetusguard-public" 46 | volumes: 47 | - "./certs/client/:/certs/client/:ro" 48 | environment: 49 | DOCKER_HOST: "tcp://cetusguard:2376" 50 | DOCKER_TLS_VERIFY: "1" 51 | DOCKER_CERT_PATH: "/certs/client/" 52 | entrypoint: "/bin/sh" 53 | command: ["-c", "trap : TERM INT; while :; do sleep 60; done & wait"] 54 | depends_on: 55 | - "cetusguard" 56 | 57 | networks: 58 | 59 | cetusguard-private: 60 | name: "cetusguard-private" 61 | 62 | cetusguard-public: 63 | name: "cetusguard-public" 64 | 65 | volumes: 66 | 67 | dockerd-socket: 68 | name: "cetusguard-dockerd-socket" 69 | -------------------------------------------------------------------------------- /examples/compose/compose.tcptls-tcptls.yml: -------------------------------------------------------------------------------- 1 | services: 2 | 3 | cetusguard: 4 | container_name: "cetusguard" 5 | image: "docker.io/hectorm/cetusguard:v1" 6 | restart: "on-failure" 7 | read_only: true 8 | networks: 9 | - "cetusguard-private" 10 | - "cetusguard-public" 11 | volumes: 12 | - "./rules.list:/rules.list:ro" 13 | - "./certs/client/:/certs/client/:ro" 14 | - "./certs/server/:/certs/server/:ro" 15 | environment: 16 | CETUSGUARD_BACKEND_ADDR: "tcp://dockerd:2376" 17 | CETUSGUARD_BACKEND_TLS_CACERT: "/certs/client/ca.pem" 18 | CETUSGUARD_BACKEND_TLS_CERT: "/certs/client/cert.pem" 19 | CETUSGUARD_BACKEND_TLS_KEY: "/certs/client/key.pem" 20 | CETUSGUARD_FRONTEND_ADDR: "tcp://:2376" 21 | CETUSGUARD_FRONTEND_TLS_CACERT: "/certs/server/ca.pem" 22 | CETUSGUARD_FRONTEND_TLS_CERT: "/certs/server/cert.pem" 23 | CETUSGUARD_FRONTEND_TLS_KEY: "/certs/server/key.pem" 24 | CETUSGUARD_RULES_FILE: "/rules.list" 25 | CETUSGUARD_LOG_LEVEL: "7" 26 | depends_on: 27 | - "dockerd" 28 | 29 | dockerd: 30 | container_name: "cetusguard-dockerd" 31 | image: "docker.io/docker:28-dind" 32 | restart: "on-failure" 33 | privileged: true 34 | networks: 35 | - "cetusguard-private" 36 | volumes: 37 | - "./certs/server/:/certs/server/:ro" 38 | environment: 39 | DOCKER_HOST: "tcp://dockerd:2376" 40 | DOCKER_TLS_CERTDIR: "/certs/" 41 | 42 | docker: 43 | container_name: "cetusguard-docker" 44 | image: "docker.io/docker:28-cli" 45 | restart: "on-failure" 46 | networks: 47 | - "cetusguard-public" 48 | volumes: 49 | - "./certs/client/:/certs/client/:ro" 50 | environment: 51 | DOCKER_HOST: "tcp://cetusguard:2376" 52 | DOCKER_TLS_VERIFY: "1" 53 | DOCKER_CERT_PATH: "/certs/client/" 54 | entrypoint: "/bin/sh" 55 | command: ["-c", "trap : TERM INT; while :; do sleep 60; done & wait"] 56 | depends_on: 57 | - "cetusguard" 58 | 59 | networks: 60 | 61 | cetusguard-private: 62 | name: "cetusguard-private" 63 | 64 | cetusguard-public: 65 | name: "cetusguard-public" 66 | -------------------------------------------------------------------------------- /examples/compose/compose.traefik.yml: -------------------------------------------------------------------------------- 1 | services: 2 | 3 | cetusguard: 4 | container_name: "cetusguard" 5 | image: "docker.io/hectorm/cetusguard:v1" 6 | restart: "on-failure" 7 | read_only: true 8 | networks: 9 | - "cetusguard" 10 | volumes: 11 | - "/var/run/docker.sock:/var/run/docker.sock:ro" 12 | environment: 13 | CETUSGUARD_BACKEND_ADDR: "unix:///var/run/docker.sock" 14 | CETUSGUARD_FRONTEND_ADDR: "tcp://:2375" 15 | CETUSGUARD_RULES: | 16 | ! Monitor events 17 | GET %API_PREFIX_EVENTS% 18 | ! List containers 19 | GET %API_PREFIX_CONTAINERS%/json 20 | ! Inspect a container 21 | GET %API_PREFIX_CONTAINERS%/%CONTAINER_ID_OR_NAME%/json 22 | CETUSGUARD_LOG_LEVEL: "7" 23 | 24 | traefik: 25 | container_name: "cetusguard-traefik" 26 | image: "docker.io/traefik:v3.3" 27 | restart: "on-failure" 28 | networks: 29 | - "cetusguard" 30 | - "public" 31 | ports: 32 | - "127.0.0.1:3000:3000/tcp" 33 | - "127.0.0.1:8080:8080/tcp" 34 | command: 35 | - "--api.insecure=true" 36 | - "--api.dashboard=true" 37 | - "--providers.docker.endpoint=tcp://cetusguard:2375" 38 | - "--providers.docker.network=public" 39 | - "--providers.docker.exposedbydefault=false" 40 | - "--entrypoints.traefik.address=:3000/tcp" 41 | - "--entrypoints.whoami.address=:8080/tcp" 42 | 43 | whoami: 44 | container_name: "cetusguard-whoami" 45 | image: "docker.io/traefik/whoami:latest" 46 | restart: "on-failure" 47 | networks: 48 | - "public" 49 | labels: 50 | traefik.enable: "true" 51 | traefik.http.routers.whoami.rule: "PathPrefix(`/`)" 52 | traefik.http.routers.whoami.entryPoints: "whoami" 53 | traefik.http.routers.whoami.service: "whoami" 54 | traefik.http.services.whoami.loadbalancer.server.port: "80" 55 | 56 | networks: 57 | 58 | cetusguard: 59 | name: "cetusguard" 60 | 61 | public: 62 | name: "public" 63 | -------------------------------------------------------------------------------- /examples/compose/docker.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -eu 4 | export LC_ALL='C' 5 | 6 | exec docker exec -it cetusguard-docker docker "${@-}" 7 | -------------------------------------------------------------------------------- /examples/compose/mkcerts.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -eu 4 | export LC_ALL='C' 5 | 6 | # export LD_PRELOAD='/usr/lib/faketime/libfaketime.so.1' 7 | # export FAKETIME='1970-01-01 00:00:00' 8 | 9 | { 10 | set -a 11 | 12 | CERTS_DIR="$(CDPATH='' cd -- "$(dirname -- "${0:?}")" && pwd -P)"/certs/ 13 | 14 | CA_KEY="${CERTS_DIR:?}"/ca/key.pem 15 | CA_CSR="${CERTS_DIR:?}"/ca/csr.pem 16 | CA_SRL="${CERTS_DIR:?}"/ca/cert.srl 17 | CA_CRT="${CERTS_DIR:?}"/ca/cert.pem 18 | CA_CRT_CNF="${CERTS_DIR:?}"/ca/openssl.cnf 19 | CA_CRT_SUBJ='/CN=Container daemon CA' 20 | CA_CRT_VALIDITY_DAYS='7300' 21 | CA_CRT_RENOVATION_DAYS='30' 22 | CA_RENEW_PREHOOK='' 23 | CA_RENEW_POSTHOOK='' 24 | 25 | SERVER_KEY="${CERTS_DIR:?}"/server/key.pem 26 | SERVER_CSR="${CERTS_DIR:?}"/server/csr.pem 27 | SERVER_CRT="${CERTS_DIR:?}"/server/cert.pem 28 | SERVER_CRT_CNF="${CERTS_DIR:?}"/server/openssl.cnf 29 | SERVER_CRT_CA="${CERTS_DIR:?}"/server/ca.pem 30 | SERVER_CRT_FULLCHAIN="${CERTS_DIR:?}"/server/fullchain.pem 31 | SERVER_CRT_SUBJ='/CN=Container daemon server' 32 | SERVER_CRT_SAN=$(printf '%s\n' \ 33 | "DNS:$(hostname -f)" \ 34 | 'DNS:cetusguard' \ 35 | 'DNS:dockerd' \ 36 | 'DNS:localhost' \ 37 | 'IP:127.0.0.1' \ 38 | 'IP:::1' \ 39 | | paste -sd, -) 40 | SERVER_CRT_VALIDITY_DAYS='7300' 41 | SERVER_CRT_RENOVATION_DAYS='30' 42 | SERVER_RENEW_PREHOOK='' 43 | SERVER_RENEW_POSTHOOK='' 44 | 45 | CLIENT_KEY="${CERTS_DIR:?}"/client/key.pem 46 | CLIENT_CSR="${CERTS_DIR:?}"/client/csr.pem 47 | CLIENT_CRT="${CERTS_DIR:?}"/client/cert.pem 48 | CLIENT_CRT_CNF="${CERTS_DIR:?}"/client/openssl.cnf 49 | CLIENT_CRT_CA="${CERTS_DIR:?}"/client/ca.pem 50 | CLIENT_CRT_FULLCHAIN="${CERTS_DIR:?}"/client/fullchain.pem 51 | CLIENT_CRT_SUBJ='/CN=Container daemon client' 52 | CLIENT_CRT_VALIDITY_DAYS='7300' 53 | CLIENT_CRT_RENOVATION_DAYS='30' 54 | CLIENT_P12="${CERTS_DIR:?}"/client/cert.p12 55 | CLIENT_P12_PASS='changeit' 56 | CLIENT_RENEW_PREHOOK='' 57 | CLIENT_RENEW_POSTHOOK='' 58 | 59 | set +a 60 | } 61 | 62 | if [ ! -e "${CERTS_DIR:?}"/ca/ ]; then mkdir -p "${CERTS_DIR:?}"/ca/; fi 63 | if [ ! -e "${CERTS_DIR:?}"/server/ ]; then mkdir -p "${CERTS_DIR:?}"/server/; fi 64 | if [ ! -e "${CERTS_DIR:?}"/client/ ]; then mkdir -p "${CERTS_DIR:?}"/client/; fi 65 | 66 | # Generate CA private key if it does not exist 67 | if [ ! -e "${CA_KEY:?}" ] \ 68 | || ! openssl ecparam -check -in "${CA_KEY:?}" -noout >/dev/null 2>&1 69 | then 70 | printf '%s\n' 'Generating CA private key...' 71 | openssl ecparam -genkey -name prime256v1 -out "${CA_KEY:?}" 72 | fi 73 | 74 | # Generate CA certificate if it does not exist or will expire soon 75 | if [ ! -e "${CA_CRT:?}" ] \ 76 | || [ "$(openssl x509 -pubkey -in "${CA_CRT:?}" -noout 2>/dev/null)" != "$(openssl pkey -pubout -in "${CA_KEY:?}" -outform PEM 2>/dev/null)" ] \ 77 | || ! openssl x509 -checkend "$((60*60*24*CA_CRT_RENOVATION_DAYS))" -in "${CA_CRT:?}" -noout >/dev/null 2>&1 78 | then 79 | if [ -n "${CA_RENEW_PREHOOK?}" ]; then 80 | sh -euc "${CA_RENEW_PREHOOK:?}" 81 | fi 82 | 83 | printf '%s\n' 'Generating CA certificate...' 84 | openssl req -new \ 85 | -key "${CA_KEY:?}" \ 86 | -out "${CA_CSR:?}" \ 87 | -subj "${CA_CRT_SUBJ:?}" 88 | cat > "${CA_CRT_CNF:?}" <<-EOF 89 | [ x509_exts ] 90 | subjectKeyIdentifier = hash 91 | authorityKeyIdentifier = keyid:always,issuer:always 92 | basicConstraints = critical,CA:TRUE,pathlen:0 93 | keyUsage = critical,keyCertSign,cRLSign 94 | EOF 95 | openssl x509 -req \ 96 | -in "${CA_CSR:?}" \ 97 | -out "${CA_CRT:?}" \ 98 | -signkey "${CA_KEY:?}" \ 99 | -days "${CA_CRT_VALIDITY_DAYS:?}" \ 100 | -sha256 \ 101 | -extfile "${CA_CRT_CNF:?}" \ 102 | -extensions x509_exts 103 | openssl x509 -in "${CA_CRT:?}" -fingerprint -noout 104 | 105 | if [ -n "${CA_RENEW_POSTHOOK?}" ]; then 106 | sh -euc "${CA_RENEW_POSTHOOK:?}" 107 | fi 108 | fi 109 | 110 | # Generate server private key if it does not exist 111 | if [ ! -e "${SERVER_KEY:?}" ] \ 112 | || ! openssl ecparam -check -in "${SERVER_KEY:?}" -noout >/dev/null 2>&1 113 | then 114 | printf '%s\n' 'Generating server private key...' 115 | openssl ecparam -genkey -name prime256v1 -out "${SERVER_KEY:?}" 116 | fi 117 | 118 | # Generate server certificate if it does not exist or will expire soon 119 | if [ ! -e "${SERVER_CRT:?}" ] \ 120 | || [ "$(openssl x509 -pubkey -in "${SERVER_CRT:?}" -noout 2>/dev/null)" != "$(openssl pkey -pubout -in "${SERVER_KEY:?}" -outform PEM 2>/dev/null)" ] \ 121 | || ! openssl verify -CAfile "${CA_CRT:?}" "${SERVER_CRT:?}" >/dev/null 2>&1 \ 122 | || ! openssl x509 -checkend "$((60*60*24*SERVER_CRT_RENOVATION_DAYS))" -in "${SERVER_CRT:?}" -noout >/dev/null 2>&1 123 | then 124 | if [ -n "${SERVER_RENEW_PREHOOK?}" ]; then 125 | sh -euc "${SERVER_RENEW_PREHOOK:?}" 126 | fi 127 | 128 | printf '%s\n' 'Generating server certificate...' 129 | openssl req -new \ 130 | -key "${SERVER_KEY:?}" \ 131 | -out "${SERVER_CSR:?}" \ 132 | -subj "${SERVER_CRT_SUBJ:?}" 133 | cat > "${SERVER_CRT_CNF:?}" <<-EOF 134 | [ x509_exts ] 135 | subjectAltName = ${SERVER_CRT_SAN:?} 136 | basicConstraints = critical,CA:FALSE 137 | keyUsage = critical,digitalSignature 138 | extendedKeyUsage = critical,serverAuth 139 | EOF 140 | openssl x509 -req \ 141 | -in "${SERVER_CSR:?}" \ 142 | -out "${SERVER_CRT:?}" \ 143 | -CA "${CA_CRT:?}" \ 144 | -CAkey "${CA_KEY:?}" \ 145 | -CAserial "${CA_SRL:?}" -CAcreateserial \ 146 | -days "${SERVER_CRT_VALIDITY_DAYS:?}" \ 147 | -sha256 \ 148 | -extfile "${SERVER_CRT_CNF:?}" \ 149 | -extensions x509_exts 150 | openssl x509 -in "${SERVER_CRT:?}" -fingerprint -noout 151 | 152 | cat "${CA_CRT:?}" > "${SERVER_CRT_CA:?}" 153 | cat "${SERVER_CRT:?}" "${SERVER_CRT_CA:?}" > "${SERVER_CRT_FULLCHAIN:?}" 154 | 155 | if [ -n "${SERVER_RENEW_POSTHOOK?}" ]; then 156 | sh -euc "${SERVER_RENEW_POSTHOOK:?}" 157 | fi 158 | fi 159 | 160 | # Generate client private key if it does not exist 161 | if [ ! -e "${CLIENT_KEY:?}" ] \ 162 | || ! openssl ecparam -check -in "${CLIENT_KEY:?}" -noout >/dev/null 2>&1 163 | then 164 | printf '%s\n' 'Generating client private key...' 165 | openssl ecparam -genkey -name prime256v1 -out "${CLIENT_KEY:?}" 166 | fi 167 | 168 | # Generate client certificate if it does not exist or will expire soon 169 | if [ ! -e "${CLIENT_CRT:?}" ] \ 170 | || [ "$(openssl x509 -pubkey -in "${CLIENT_CRT:?}" -noout 2>/dev/null)" != "$(openssl pkey -pubout -in "${CLIENT_KEY:?}" -outform PEM 2>/dev/null)" ] \ 171 | || ! openssl verify -CAfile "${CA_CRT:?}" "${CLIENT_CRT:?}" >/dev/null 2>&1 \ 172 | || ! openssl x509 -checkend "$((60*60*24*CLIENT_CRT_RENOVATION_DAYS))" -in "${CLIENT_CRT:?}" -noout >/dev/null 2>&1 173 | then 174 | if [ -n "${CLIENT_RENEW_PREHOOK?}" ]; then 175 | sh -euc "${CLIENT_RENEW_PREHOOK:?}" 176 | fi 177 | 178 | printf '%s\n' 'Generating client certificate...' 179 | openssl req -new \ 180 | -key "${CLIENT_KEY:?}" \ 181 | -out "${CLIENT_CSR:?}" \ 182 | -subj "${CLIENT_CRT_SUBJ:?}" 183 | cat > "${CLIENT_CRT_CNF:?}" <<-EOF 184 | [ x509_exts ] 185 | basicConstraints = critical,CA:FALSE 186 | keyUsage = critical,digitalSignature 187 | extendedKeyUsage = critical,clientAuth 188 | EOF 189 | openssl x509 -req \ 190 | -in "${CLIENT_CSR:?}" \ 191 | -out "${CLIENT_CRT:?}" \ 192 | -CA "${CA_CRT:?}" \ 193 | -CAkey "${CA_KEY:?}" \ 194 | -CAserial "${CA_SRL:?}" -CAcreateserial \ 195 | -days "${CLIENT_CRT_VALIDITY_DAYS:?}" \ 196 | -sha256 \ 197 | -extfile "${CLIENT_CRT_CNF:?}" \ 198 | -extensions x509_exts 199 | openssl x509 -in "${CLIENT_CRT:?}" -fingerprint -noout 200 | 201 | cat "${CA_CRT:?}" > "${CLIENT_CRT_CA:?}" 202 | cat "${CLIENT_CRT:?}" "${CLIENT_CRT_CA:?}" > "${CLIENT_CRT_FULLCHAIN:?}" 203 | 204 | openssl pkcs12 -export \ 205 | -inkey "${CLIENT_KEY:?}" \ 206 | -in "${CLIENT_CRT:?}" \ 207 | -out "${CLIENT_P12:?}" \ 208 | -passout "pass:${CLIENT_P12_PASS:?}" 209 | 210 | if [ -n "${CLIENT_RENEW_POSTHOOK?}" ]; then 211 | sh -euc "${CLIENT_RENEW_POSTHOOK:?}" 212 | fi 213 | fi 214 | -------------------------------------------------------------------------------- /examples/compose/podman.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -eu 4 | export LC_ALL='C' 5 | 6 | exec podman exec -it cetusguard-podman podman --remote "${@-}" 7 | -------------------------------------------------------------------------------- /examples/compose/rules.list: -------------------------------------------------------------------------------- 1 | GET,HEAD,POST,PUT,DELETE %API_PREFIX_CONTAINERS%(/.*)? 2 | GET,HEAD,POST,PUT,DELETE %API_PREFIX_LIBPOD_CONTAINERS%(/.*)? 3 | 4 | GET,HEAD,POST,PUT,DELETE %API_PREFIX_IMAGES%(/.*)? 5 | GET,HEAD,POST,PUT,DELETE %API_PREFIX_LIBPOD_IMAGES%(/.*)? 6 | 7 | GET,HEAD,POST,PUT,DELETE %API_PREFIX_VOLUMES%(/.*)? 8 | GET,HEAD,POST,PUT,DELETE %API_PREFIX_LIBPOD_VOLUMES%(/.*)? 9 | 10 | GET,HEAD,POST,PUT,DELETE %API_PREFIX_NETWORKS%(/.*)? 11 | GET,HEAD,POST,PUT,DELETE %API_PREFIX_LIBPOD_NETWORKS%(/.*)? 12 | 13 | GET,HEAD,POST,PUT,DELETE %API_PREFIX_BUILD%(/.*)? 14 | GET,HEAD,POST,PUT,DELETE %API_PREFIX_LIBPOD_BUILD%(/.*)? 15 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/hectorm/cetusguard 2 | 3 | go 1.24.0 4 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hectorm/cetusguard/6b187da734b2ba1cd26eaae44a82412917077cf7/go.sum -------------------------------------------------------------------------------- /internal/logger/logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "math" 7 | "os" 8 | ) 9 | 10 | // Syslog log levels from RFC 5424, 11 | // https://datatracker.ietf.org/doc/html/rfc5424 12 | const ( 13 | LvlNone = 0 14 | LvlCritical = 2 15 | LvlError = 3 16 | LvlWarning = 4 17 | LvlInfo = 6 18 | LvlDebug = 7 19 | ) 20 | 21 | var ( 22 | lgrCritical *log.Logger 23 | lgrError *log.Logger 24 | lgrWarning *log.Logger 25 | lgrInfo *log.Logger 26 | lgrDebug *log.Logger 27 | level int 28 | exitOnCritical bool 29 | ) 30 | 31 | func init() { 32 | lgrCritical = log.New(io.Discard, "CRITICAL: ", log.Ldate|log.Ltime) 33 | lgrError = log.New(io.Discard, "ERROR: ", log.Ldate|log.Ltime) 34 | lgrWarning = log.New(io.Discard, "WARNING: ", log.Ldate|log.Ltime) 35 | lgrInfo = log.New(io.Discard, "INFO: ", log.Ldate|log.Ltime) 36 | lgrDebug = log.New(io.Discard, "DEBUG: ", log.Ldate|log.Ltime) 37 | SetLevel(LvlInfo) 38 | exitOnCritical = true 39 | } 40 | 41 | func Level() int { 42 | return level 43 | } 44 | 45 | func SetLevel(val int) { 46 | level = int(math.Min(math.Max(float64(val), LvlNone), LvlDebug)) 47 | if level >= LvlCritical { 48 | lgrCritical.SetOutput(os.Stderr) 49 | } else { 50 | lgrCritical.SetOutput(io.Discard) 51 | } 52 | if level >= LvlError { 53 | lgrError.SetOutput(os.Stderr) 54 | } else { 55 | lgrError.SetOutput(io.Discard) 56 | } 57 | if level >= LvlWarning { 58 | lgrWarning.SetOutput(os.Stderr) 59 | } else { 60 | lgrWarning.SetOutput(io.Discard) 61 | } 62 | if level >= LvlInfo { 63 | lgrInfo.SetOutput(os.Stdout) 64 | } else { 65 | lgrInfo.SetOutput(io.Discard) 66 | } 67 | if level >= LvlDebug { 68 | lgrDebug.SetOutput(os.Stdout) 69 | } else { 70 | lgrDebug.SetOutput(io.Discard) 71 | } 72 | } 73 | 74 | func LgrCritical() *log.Logger { 75 | return lgrCritical 76 | } 77 | 78 | func LgrError() *log.Logger { 79 | return lgrError 80 | } 81 | 82 | func LgrWarning() *log.Logger { 83 | return lgrWarning 84 | } 85 | 86 | func LgrInfo() *log.Logger { 87 | return lgrInfo 88 | } 89 | 90 | func LgrDebug() *log.Logger { 91 | return lgrDebug 92 | } 93 | 94 | func Critical(v ...any) { 95 | lgrCritical.Print(v...) 96 | if exitOnCritical { 97 | os.Exit(1) 98 | } 99 | } 100 | 101 | func Criticalf(format string, v ...any) { 102 | lgrCritical.Printf(format, v...) 103 | if exitOnCritical { 104 | os.Exit(1) 105 | } 106 | } 107 | 108 | func Criticalln(v ...any) { 109 | lgrCritical.Println(v...) 110 | if exitOnCritical { 111 | os.Exit(1) 112 | } 113 | } 114 | 115 | func Error(v ...any) { 116 | lgrError.Print(v...) 117 | } 118 | 119 | func Errorf(f string, v ...any) { 120 | lgrError.Printf(f, v...) 121 | } 122 | 123 | func Errorln(v ...any) { 124 | lgrError.Println(v...) 125 | } 126 | 127 | func Warning(v ...any) { 128 | lgrWarning.Print(v...) 129 | } 130 | 131 | func Warningf(f string, v ...any) { 132 | lgrWarning.Printf(f, v...) 133 | } 134 | 135 | func Warningln(v ...any) { 136 | lgrWarning.Println(v...) 137 | } 138 | 139 | func Info(v ...any) { 140 | lgrInfo.Print(v...) 141 | } 142 | 143 | func Infof(f string, v ...any) { 144 | lgrInfo.Printf(f, v...) 145 | } 146 | 147 | func Infoln(v ...any) { 148 | lgrInfo.Println(v...) 149 | } 150 | 151 | func Debug(v ...any) { 152 | lgrDebug.Print(v...) 153 | } 154 | 155 | func Debugf(f string, v ...any) { 156 | lgrDebug.Printf(f, v...) 157 | } 158 | 159 | func Debugln(v ...any) { 160 | lgrDebug.Println(v...) 161 | } 162 | -------------------------------------------------------------------------------- /internal/logger/logger_test.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "bytes" 5 | "regexp" 6 | "testing" 7 | ) 8 | 9 | func TestSetLevel(t *testing.T) { 10 | SetLevel(LvlNone) 11 | if Level() != LvlNone { 12 | t.Errorf("level = %d, want %d", Level(), LvlNone) 13 | } 14 | 15 | SetLevel(-99) 16 | if Level() != LvlNone { 17 | t.Errorf("level = %d, want %d", Level(), LvlNone) 18 | } 19 | 20 | SetLevel(LvlDebug) 21 | if Level() != LvlDebug { 22 | t.Errorf("level = %d, want %d", Level(), LvlDebug) 23 | } 24 | 25 | SetLevel(99) 26 | if Level() != LvlDebug { 27 | t.Errorf("level = %d, want %d", Level(), LvlDebug) 28 | } 29 | } 30 | 31 | func TestLoggerCritical(t *testing.T) { 32 | buf := new(bytes.Buffer) 33 | LgrCritical().SetOutput(buf) 34 | exitOnCritical = false 35 | 36 | Critical("FOO", "BAR") 37 | if !regexp.MustCompile(`^CRITICAL: .+ FOOBAR`).MatchString(buf.String()) { 38 | t.Errorf("unexpected Critical log output: %s", buf) 39 | } 40 | } 41 | 42 | func TestLoggerCriticalf(t *testing.T) { 43 | buf := new(bytes.Buffer) 44 | LgrCritical().SetOutput(buf) 45 | exitOnCritical = false 46 | 47 | Criticalf("%s %s", "FOO", "BAR") 48 | if !regexp.MustCompile(`^CRITICAL: .+ FOO BAR`).MatchString(buf.String()) { 49 | t.Errorf("unexpected Criticalf log output: %s", buf) 50 | } 51 | } 52 | 53 | func TestLoggerCriticalln(t *testing.T) { 54 | buf := new(bytes.Buffer) 55 | LgrCritical().SetOutput(buf) 56 | exitOnCritical = false 57 | 58 | Criticalln("FOO", "BAR") 59 | if !regexp.MustCompile(`^CRITICAL: .+ FOO BAR`).MatchString(buf.String()) { 60 | t.Errorf("unexpected Criticaln log output: %s", buf) 61 | } 62 | } 63 | 64 | func TestLoggerError(t *testing.T) { 65 | buf := new(bytes.Buffer) 66 | LgrError().SetOutput(buf) 67 | 68 | Error("FOO", "BAR") 69 | if !regexp.MustCompile(`^ERROR: .+ FOOBAR`).MatchString(buf.String()) { 70 | t.Errorf("unexpected Error log output: %s", buf) 71 | } 72 | } 73 | 74 | func TestLoggerErrorf(t *testing.T) { 75 | buf := new(bytes.Buffer) 76 | LgrError().SetOutput(buf) 77 | 78 | Errorf("%s %s", "FOO", "BAR") 79 | if !regexp.MustCompile(`^ERROR: .+ FOO BAR`).MatchString(buf.String()) { 80 | t.Errorf("unexpected Errorf log output: %s", buf) 81 | } 82 | } 83 | 84 | func TestLoggerErrorln(t *testing.T) { 85 | buf := new(bytes.Buffer) 86 | LgrError().SetOutput(buf) 87 | 88 | Errorln("FOO", "BAR") 89 | if !regexp.MustCompile(`^ERROR: .+ FOO BAR`).MatchString(buf.String()) { 90 | t.Errorf("unexpected Errorln log output: %s", buf) 91 | } 92 | } 93 | 94 | func TestLoggerWarning(t *testing.T) { 95 | buf := new(bytes.Buffer) 96 | LgrWarning().SetOutput(buf) 97 | 98 | Warning("FOO", "BAR") 99 | if !regexp.MustCompile(`^WARNING: .+ FOOBAR`).MatchString(buf.String()) { 100 | t.Errorf("unexpected Warning log output: %s", buf) 101 | } 102 | } 103 | 104 | func TestLoggerWarningf(t *testing.T) { 105 | buf := new(bytes.Buffer) 106 | LgrWarning().SetOutput(buf) 107 | 108 | Warningf("%s %s", "FOO", "BAR") 109 | if !regexp.MustCompile(`^WARNING: .+ FOO BAR`).MatchString(buf.String()) { 110 | t.Errorf("unexpected Warningf log output: %s", buf) 111 | } 112 | } 113 | 114 | func TestLoggerWarningln(t *testing.T) { 115 | buf := new(bytes.Buffer) 116 | LgrWarning().SetOutput(buf) 117 | 118 | Warningln("FOO", "BAR") 119 | if !regexp.MustCompile(`^WARNING: .+ FOO BAR`).MatchString(buf.String()) { 120 | t.Errorf("unexpected Warningln log output: %s", buf) 121 | } 122 | } 123 | 124 | func TestLoggerInfo(t *testing.T) { 125 | buf := new(bytes.Buffer) 126 | LgrInfo().SetOutput(buf) 127 | 128 | Info("FOO", "BAR") 129 | if !regexp.MustCompile(`^INFO: .+ FOOBAR`).MatchString(buf.String()) { 130 | t.Errorf("unexpected Info log output: %s", buf) 131 | } 132 | } 133 | 134 | func TestLoggerInfof(t *testing.T) { 135 | buf := new(bytes.Buffer) 136 | LgrInfo().SetOutput(buf) 137 | 138 | Infof("%s %s", "FOO", "BAR") 139 | if !regexp.MustCompile(`^INFO: .+ FOO BAR`).MatchString(buf.String()) { 140 | t.Errorf("unexpected Infof log output: %s", buf) 141 | } 142 | } 143 | 144 | func TestLoggerInfoln(t *testing.T) { 145 | buf := new(bytes.Buffer) 146 | LgrInfo().SetOutput(buf) 147 | 148 | Infoln("FOO", "BAR") 149 | if !regexp.MustCompile(`^INFO: .+ FOO BAR`).MatchString(buf.String()) { 150 | t.Errorf("unexpected Infoln log output: %s", buf) 151 | } 152 | } 153 | 154 | func TestLoggerDebug(t *testing.T) { 155 | buf := new(bytes.Buffer) 156 | LgrDebug().SetOutput(buf) 157 | 158 | Debug("FOO", "BAR") 159 | if !regexp.MustCompile(`^DEBUG: .+ FOOBAR`).MatchString(buf.String()) { 160 | t.Errorf("unexpected Debug log output: %s", buf) 161 | } 162 | } 163 | 164 | func TestLoggerDebugf(t *testing.T) { 165 | buf := new(bytes.Buffer) 166 | LgrDebug().SetOutput(buf) 167 | 168 | Debugf("%s %s", "FOO", "BAR") 169 | if !regexp.MustCompile(`^DEBUG: .+ FOO BAR`).MatchString(buf.String()) { 170 | t.Errorf("unexpected Debugf log output: %s", buf) 171 | } 172 | } 173 | 174 | func TestLoggerDebugln(t *testing.T) { 175 | buf := new(bytes.Buffer) 176 | LgrDebug().SetOutput(buf) 177 | 178 | Debugln("FOO", "BAR") 179 | if !regexp.MustCompile(`^DEBUG: .+ FOO BAR`).MatchString(buf.String()) { 180 | t.Errorf("unexpected Debugln log output: %s", buf) 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /internal/utils/env/env.go: -------------------------------------------------------------------------------- 1 | package env 2 | 3 | import ( 4 | "os" 5 | "strconv" 6 | ) 7 | 8 | func StringEnv(def string, keys ...string) string { 9 | for _, key := range keys { 10 | if val, ok := os.LookupEnv(key); ok { 11 | return val 12 | } 13 | } 14 | return def 15 | } 16 | 17 | func StringSliceEnv(def []string, keys ...string) []string { 18 | for _, key := range keys { 19 | if val, ok := os.LookupEnv(key); ok { 20 | return []string{val} 21 | } 22 | } 23 | return def 24 | } 25 | 26 | func IntEnv(def int, keys ...string) int { 27 | for _, key := range keys { 28 | if val, ok := os.LookupEnv(key); ok { 29 | if n, err := strconv.Atoi(val); err == nil { 30 | return n 31 | } 32 | } 33 | } 34 | return def 35 | } 36 | 37 | func BoolEnv(def bool, keys ...string) bool { 38 | for _, key := range keys { 39 | if val, ok := os.LookupEnv(key); ok { 40 | if b, err := strconv.ParseBool(val); err == nil { 41 | return b 42 | } 43 | } 44 | } 45 | return def 46 | } 47 | -------------------------------------------------------------------------------- /internal/utils/env/env_test.go: -------------------------------------------------------------------------------- 1 | package env 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestStringEnvDefault(t *testing.T) { 8 | val := StringEnv("BAR", "FOO") 9 | 10 | if val != "BAR" { 11 | t.Errorf("val = \"%s\", want \"%s\"", val, "BAR") 12 | } 13 | } 14 | 15 | func TestStringEnvFirst(t *testing.T) { 16 | t.Setenv("FOO1", "VAL1") 17 | t.Setenv("FOO2", "VAL2") 18 | t.Setenv("FOO3", "VAL3") 19 | 20 | val := StringEnv("BAR", "FOO1", "FOO2", "FOO3") 21 | 22 | if val != "VAL1" { 23 | t.Errorf("val = \"%s\", want \"%s\"", val, "VAL1") 24 | } 25 | } 26 | 27 | func TestStringEnvSecond(t *testing.T) { 28 | t.Setenv("FOO2", "VAL2") 29 | t.Setenv("FOO3", "VAL3") 30 | 31 | val := StringEnv("BAR", "FOO1", "FOO2", "FOO3") 32 | 33 | if val != "VAL2" { 34 | t.Errorf("val = \"%s\", want \"%s\"", val, "VAL2") 35 | } 36 | } 37 | 38 | func TestStringEnvEmpty(t *testing.T) { 39 | t.Setenv("FOO", "") 40 | 41 | val := StringEnv("BAR", "FOO") 42 | 43 | if val != "" { 44 | t.Errorf("val = \"%s\", want \"%s\"", val, "") 45 | } 46 | } 47 | 48 | func TestStringSliceEnvDefault(t *testing.T) { 49 | val := StringSliceEnv([]string{"BAR"}, "FOO") 50 | 51 | if len(val) != 1 || val[0] != "BAR" { 52 | t.Errorf("val = %v, want %v", val, []string{"BAR"}) 53 | } 54 | } 55 | 56 | func TestStringSliceEnvFirst(t *testing.T) { 57 | t.Setenv("FOO1", "VAL1") 58 | t.Setenv("FOO2", "VAL2") 59 | t.Setenv("FOO3", "VAL3") 60 | 61 | val := StringSliceEnv([]string{"BAR"}, "FOO1", "FOO2", "FOO3") 62 | 63 | if len(val) != 1 || val[0] != "VAL1" { 64 | t.Errorf("val = %v, want %v", val, []string{"VAL1"}) 65 | } 66 | } 67 | 68 | func TestStringSliceEnvSecond(t *testing.T) { 69 | t.Setenv("FOO2", "VAL2") 70 | t.Setenv("FOO3", "VAL3") 71 | 72 | val := StringSliceEnv([]string{"BAR"}, "FOO1", "FOO2", "FOO3") 73 | 74 | if len(val) != 1 || val[0] != "VAL2" { 75 | t.Errorf("val = %v, want %v", val, []string{"VAL2"}) 76 | } 77 | } 78 | 79 | func TestStringSliceEnvEmpty(t *testing.T) { 80 | t.Setenv("FOO", "") 81 | 82 | val := StringSliceEnv([]string{"BAR"}, "FOO") 83 | 84 | if len(val) != 1 || val[0] != "" { 85 | t.Errorf("val = %v, want %v", val, []string{}) 86 | } 87 | } 88 | 89 | func TestIntEnvDefault(t *testing.T) { 90 | val := IntEnv(0, "FOO") 91 | 92 | if val != 0 { 93 | t.Errorf("val = %d, want %d", val, 0) 94 | } 95 | } 96 | 97 | func TestIntEnvFirst(t *testing.T) { 98 | t.Setenv("FOO1", "1") 99 | t.Setenv("FOO2", "2") 100 | t.Setenv("FOO3", "3") 101 | 102 | val := IntEnv(0, "FOO1", "FOO2", "FOO3") 103 | 104 | if val != 1 { 105 | t.Errorf("val = %d, want %d", val, 1) 106 | } 107 | } 108 | 109 | func TestIntEnvSecond(t *testing.T) { 110 | t.Setenv("FOO2", "2") 111 | t.Setenv("FOO3", "3") 112 | 113 | val := IntEnv(0, "FOO1", "FOO2", "FOO3") 114 | 115 | if val != 2 { 116 | t.Errorf("val = %d, want %d", val, 2) 117 | } 118 | } 119 | 120 | func TestIntEnvWrongType(t *testing.T) { 121 | t.Setenv("FOO", "BAR") 122 | 123 | val := IntEnv(0, "FOO") 124 | 125 | if val != 0 { 126 | t.Errorf("val = %d, want %d", val, 0) 127 | } 128 | } 129 | 130 | func TestIntEnvEmpty(t *testing.T) { 131 | t.Setenv("FOO", "") 132 | 133 | val := IntEnv(0, "FOO") 134 | 135 | if val != 0 { 136 | t.Errorf("val = %d, want %d", val, 0) 137 | } 138 | } 139 | 140 | func TestBoolEnvDefault(t *testing.T) { 141 | val := BoolEnv(true, "FOO") 142 | 143 | if !val { 144 | t.Errorf("val = %t, want %t", val, true) 145 | } 146 | } 147 | 148 | func TestBoolEnvFirst(t *testing.T) { 149 | t.Setenv("FOO1", "true") 150 | t.Setenv("FOO2", "false") 151 | t.Setenv("FOO3", "false") 152 | 153 | val := BoolEnv(true, "FOO1", "FOO2", "FOO3") 154 | 155 | if !val { 156 | t.Errorf("val = %t, want %t", val, true) 157 | } 158 | } 159 | 160 | func TestBoolEnvSecond(t *testing.T) { 161 | t.Setenv("FOO2", "true") 162 | t.Setenv("FOO3", "false") 163 | 164 | val := BoolEnv(true, "FOO1", "FOO2", "FOO3") 165 | 166 | if !val { 167 | t.Errorf("val = %t, want %t", val, true) 168 | } 169 | } 170 | 171 | func TestBoolEnvEmpty(t *testing.T) { 172 | t.Setenv("FOO", "") 173 | 174 | val := BoolEnv(false, "FOO") 175 | 176 | if val { 177 | t.Errorf("val = %t, want %t", val, false) 178 | } 179 | } 180 | 181 | func TestBoolEnvWrongType(t *testing.T) { 182 | t.Setenv("FOO", "BAR") 183 | 184 | val := BoolEnv(false, "FOO") 185 | 186 | if val { 187 | t.Errorf("val = %t, want %t", val, false) 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /internal/utils/flagextra/flagextra.go: -------------------------------------------------------------------------------- 1 | package flagextra 2 | 3 | import ( 4 | "encoding/json" 5 | ) 6 | 7 | func NewStringSliceValue(val []string, p *[]string) *stringSliceValue { 8 | *p = val 9 | return &stringSliceValue{ 10 | val: p, 11 | def: true, 12 | } 13 | } 14 | 15 | type stringSliceValue struct { 16 | val *[]string 17 | def bool 18 | } 19 | 20 | func (ss *stringSliceValue) Set(v string) error { 21 | if ss.def { 22 | ss.def = false 23 | *ss.val = []string{v} 24 | } else { 25 | *ss.val = append(*ss.val, v) 26 | } 27 | return nil 28 | } 29 | 30 | func (ss *stringSliceValue) String() string { 31 | b, err := json.Marshal(ss.val) 32 | if err != nil { 33 | return "" 34 | } 35 | return string(b) 36 | } 37 | -------------------------------------------------------------------------------- /internal/utils/flagextra/flagextra_test.go: -------------------------------------------------------------------------------- 1 | package flagextra 2 | 3 | import ( 4 | "flag" 5 | "io" 6 | "reflect" 7 | "testing" 8 | ) 9 | 10 | func TestFlagStringSliceValueDefault(t *testing.T) { 11 | fs := flag.NewFlagSet("foobar", flag.ContinueOnError) 12 | fs.SetOutput(io.Discard) 13 | 14 | var result []string 15 | fs.Var( 16 | NewStringSliceValue([]string{"bar1", "bar2"}, &result), 17 | "foo", 18 | "Foo", 19 | ) 20 | 21 | err := fs.Parse([]string{"arg1", "arg2"}) 22 | if err != nil { 23 | t.Fatal(err) 24 | } 25 | 26 | wanted := []string{"bar1", "bar2"} 27 | 28 | if !reflect.DeepEqual(result, wanted) { 29 | t.Errorf("result = %v, want %v", result, wanted) 30 | } 31 | } 32 | 33 | func TestFlagStringSliceValueOne(t *testing.T) { 34 | fs := flag.NewFlagSet("foobar", flag.ContinueOnError) 35 | fs.SetOutput(io.Discard) 36 | 37 | var result []string 38 | fs.Var( 39 | NewStringSliceValue(nil, &result), 40 | "foo", 41 | "Foo", 42 | ) 43 | 44 | err := fs.Parse([]string{"-foo", "bar1", "arg1", "arg2"}) 45 | if err != nil { 46 | t.Fatal(err) 47 | } 48 | 49 | wanted := []string{"bar1"} 50 | 51 | if !reflect.DeepEqual(result, wanted) { 52 | t.Errorf("result = %v, want %v", result, wanted) 53 | } 54 | } 55 | 56 | func TestFlagStringSliceValueTwo(t *testing.T) { 57 | fs := flag.NewFlagSet("foobar", flag.ContinueOnError) 58 | fs.SetOutput(io.Discard) 59 | 60 | var result []string 61 | fs.Var( 62 | NewStringSliceValue(nil, &result), 63 | "foo", 64 | "Foo", 65 | ) 66 | 67 | err := fs.Parse([]string{"-foo", "bar1", "-foo", "bar2", "arg1", "arg2"}) 68 | if err != nil { 69 | t.Fatal(err) 70 | } 71 | 72 | wanted := []string{"bar1", "bar2"} 73 | 74 | if !reflect.DeepEqual(result, wanted) { 75 | t.Errorf("result = %v, want %v", result, wanted) 76 | } 77 | } 78 | 79 | func TestFlagStringSliceValueNoValue(t *testing.T) { 80 | fs := flag.NewFlagSet("foobar", flag.ContinueOnError) 81 | fs.SetOutput(io.Discard) 82 | 83 | var result []string 84 | fs.Var( 85 | NewStringSliceValue(nil, &result), 86 | "foo", 87 | "Foo", 88 | ) 89 | 90 | err := fs.Parse([]string{"-foo"}) 91 | if err == nil { 92 | t.Errorf("result = %v, want an error", result) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /internal/utils/middleware/middleware.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import "net/http" 4 | 5 | type ResponseWriter struct { 6 | http.ResponseWriter 7 | http.Flusher 8 | http.Hijacker 9 | } 10 | 11 | func (wri *ResponseWriter) Write(data []byte) (int, error) { 12 | n, err := wri.ResponseWriter.Write(data) 13 | 14 | if wri.Flusher != nil { 15 | wri.Flush() 16 | } 17 | 18 | return n, err 19 | } 20 | 21 | func (wri *ResponseWriter) WriteHeader(statusCode int) { 22 | wri.ResponseWriter.WriteHeader(statusCode) 23 | 24 | if wri.Flusher != nil { 25 | wri.Flush() 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /internal/utils/middleware/middleware_test.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "io" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | ) 9 | 10 | func TestMiddlewareResponseWriterFlush(t *testing.T) { 11 | ts := httptest.NewServer(http.HandlerFunc(func(wri http.ResponseWriter, req *http.Request) { 12 | mWri := &ResponseWriter{ResponseWriter: wri} 13 | if f, ok := wri.(http.Flusher); ok { 14 | mWri.Flusher = f 15 | 16 | mWri.WriteHeader(http.StatusTeapot) 17 | _, _ = mWri.Write([]byte("I'm a teapot")) 18 | } else { 19 | mWri.WriteHeader(http.StatusInternalServerError) 20 | _, _ = mWri.Write([]byte("I'm NOT a teapot")) 21 | } 22 | })) 23 | defer ts.Close() 24 | 25 | res, err := ts.Client().Get(ts.URL) 26 | if err != nil { 27 | t.Fatal(err) 28 | } 29 | defer func() { 30 | _ = res.Body.Close() 31 | }() 32 | 33 | if res.StatusCode != http.StatusTeapot { 34 | t.Fatalf("res.StatusCode = %d, want %d", res.StatusCode, http.StatusTeapot) 35 | } 36 | 37 | msg, err := io.ReadAll(res.Body) 38 | if err != nil { 39 | t.Fatal(err) 40 | } 41 | 42 | if string(msg) != "I'm a teapot" { 43 | t.Fatalf(`msg = "%s", want "%s"`, msg, "I'm a teapot") 44 | } 45 | } 46 | 47 | func TestMiddlewareResponseWriterHijack(t *testing.T) { 48 | ts := httptest.NewServer(http.HandlerFunc(func(wri http.ResponseWriter, req *http.Request) { 49 | if hj, ok := wri.(http.Hijacker); ok { 50 | down, downRw, err := hj.Hijack() 51 | if err != nil { 52 | return 53 | } 54 | defer func() { 55 | _ = down.Close() 56 | }() 57 | 58 | _, _ = downRw.Write([]byte("HTTP/1.1 418\r\n\r\nI'm a teapot")) 59 | _ = downRw.Flush() 60 | } else { 61 | wri.WriteHeader(http.StatusInternalServerError) 62 | _, _ = wri.Write([]byte("I'm NOT a teapot")) 63 | } 64 | })) 65 | defer ts.Close() 66 | 67 | res, err := ts.Client().Get(ts.URL) 68 | if err != nil { 69 | t.Fatal(err) 70 | } 71 | defer func() { 72 | _ = res.Body.Close() 73 | }() 74 | 75 | if res.StatusCode != http.StatusTeapot { 76 | t.Fatalf("res.StatusCode = %d, want %d", res.StatusCode, http.StatusTeapot) 77 | } 78 | 79 | msg, err := io.ReadAll(res.Body) 80 | if err != nil { 81 | t.Fatal(err) 82 | } 83 | 84 | if string(msg) != "I'm a teapot" { 85 | t.Fatalf(`msg = "%s", want "%s"`, msg, "I'm a teapot") 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /resources/logo/CetusGuard-Color-Horizontal.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /resources/logo/CetusGuard-Color-Icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /resources/logo/CetusGuard-Color-Reduced.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /resources/logo/CetusGuard-Color-Vertical.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /resources/logo/CetusGuard-Monochrome-Horizontal.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /resources/logo/CetusGuard-Monochrome-Icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /resources/logo/CetusGuard-Monochrome-Reduced.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /resources/logo/CetusGuard-Monochrome-Vertical.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | --------------------------------------------------------------------------------