├── .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 |
10 |
--------------------------------------------------------------------------------
/resources/logo/CetusGuard-Color-Icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
--------------------------------------------------------------------------------
/resources/logo/CetusGuard-Color-Reduced.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
--------------------------------------------------------------------------------
/resources/logo/CetusGuard-Color-Vertical.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
--------------------------------------------------------------------------------
/resources/logo/CetusGuard-Monochrome-Horizontal.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
--------------------------------------------------------------------------------
/resources/logo/CetusGuard-Monochrome-Icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
--------------------------------------------------------------------------------
/resources/logo/CetusGuard-Monochrome-Reduced.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
--------------------------------------------------------------------------------
/resources/logo/CetusGuard-Monochrome-Vertical.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
--------------------------------------------------------------------------------