├── .github ├── dependabot.yml └── workflows │ ├── codeql-analysis.yml │ ├── docker-image.yml │ └── release.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── profiles ├── portainer-ro ├── portainer-rw ├── promtail ├── readonly ├── telegraf ├── traefik ├── traefik-swarm ├── trivy └── unprotected └── src ├── go.mod ├── go.sum ├── main.go ├── profile.go └── proxy.go /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "gomod" # See documentation for possible values 9 | directory: "/src" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | - package-ecosystem: "docker" 13 | directory: "/" 14 | schedule: 15 | interval: "weekly" 16 | - package-ecosystem: "github-actions" 17 | directory: "/" 18 | schedule: 19 | interval: "weekly" 20 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ main ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ main ] 20 | schedule: 21 | - cron: '27 19 * * 3' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'go' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v4 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v3 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 52 | 53 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 54 | # If this step fails, then you should remove it and run the build manually (see below) 55 | - name: Autobuild 56 | uses: github/codeql-action/autobuild@v3 57 | 58 | # ℹ️ Command-line programs to run using the OS shell. 59 | # 📚 https://git.io/JvXDl 60 | 61 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 62 | # and modify them (or add more) to build your code if your project 63 | # uses a compiled language 64 | 65 | #- run: | 66 | # make bootstrap 67 | # make release 68 | 69 | - name: Perform CodeQL Analysis 70 | uses: github/codeql-action/analyze@v3 71 | -------------------------------------------------------------------------------- /.github/workflows/docker-image.yml: -------------------------------------------------------------------------------- 1 | name: Docker Image CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags: 8 | - 'v*.*.*' 9 | pull_request: 10 | # The branches below must be a subset of the branches above 11 | branches: [ main ] 12 | 13 | jobs: 14 | docker: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Check out the repo 18 | uses: actions/checkout@v4 19 | 20 | - name: Docker meta 21 | id: meta 22 | uses: docker/metadata-action@v5 23 | with: 24 | # list of Docker images to use as base name for tags 25 | images: | 26 | ${{ github.repository }} 27 | ghcr.io/${{ github.repository }} 28 | # generate Docker tags based on the following events/attributes 29 | tags: | 30 | type=semver,pattern={{major}}.{{minor}}.{{patch}} 31 | type=semver,pattern={{major}}.{{minor}} 32 | type=semver,pattern={{major}} 33 | type=edge,branch=main 34 | 35 | - name: Set up QEMU 36 | uses: docker/setup-qemu-action@v3 37 | 38 | - name: Set up Docker Buildx 39 | uses: docker/setup-buildx-action@v3 40 | 41 | - name: Log in to Docker Hub 42 | if: github.event_name != 'pull_request' 43 | uses: docker/login-action@v3 44 | with: 45 | username: ${{ secrets.DOCKER_USERNAME }} 46 | password: ${{ secrets.DOCKER_TOKEN }} 47 | 48 | - name: Login to GHCR 49 | if: github.event_name != 'pull_request' 50 | uses: docker/login-action@v3 51 | with: 52 | registry: ghcr.io 53 | username: ${{ github.repository_owner }} 54 | password: ${{ secrets.GITHUB_TOKEN }} 55 | 56 | - name: Build and push 57 | uses: docker/build-push-action@v6 58 | with: 59 | context: . 60 | platforms: linux/amd64,linux/arm,linux/arm/v7,linux/aarch64 61 | push: ${{ github.event_name != 'pull_request' }} 62 | tags: ${{ steps.meta.outputs.tags }} 63 | labels: ${{ steps.meta.outputs.labels }} 64 | 65 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Releases 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*.*.*' 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - id: imagetag 14 | run: | 15 | echo "DOCKER_IMAGE_TAG=$(echo ${{github.ref_name}} | cut -dv -f2)" >> $GITHUB_ENV 16 | - name: Create Release 17 | id: create_release 18 | if: github.event_name != 'pull_request' 19 | uses: actions/create-release@v1 20 | env: 21 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 22 | with: 23 | tag_name: ${{ github.ref_name }} 24 | release_name: ${{ github.ref_name }} 25 | draft: false 26 | prerelease: false 27 | body: | 28 | Docker images: 29 | - ${{ github.repository }}:${{ env.DOCKER_IMAGE_TAG }} 30 | - ghcr.io/${{ github.repository }}:${{ env.DOCKER_IMAGE_TAG }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | 3 | # Binaries for programs and plugins 4 | *.exe 5 | *.exe~ 6 | *.dll 7 | *.so 8 | *.dylib 9 | 10 | # Test binary, built with `go test -c` 11 | *.test 12 | 13 | # Output of the go coverage tool, specifically when used with LiteIDE 14 | *.out 15 | 16 | # Dependency directories (remove the comment below to include it) 17 | # vendor/ 18 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM docker.io/golang:1.24.3-alpine3.20 as builder 2 | 3 | WORKDIR /go/src/app 4 | 5 | COPY src . 6 | 7 | RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /docker-socket-protector 8 | 9 | 10 | 11 | 12 | 13 | FROM scratch 14 | 15 | EXPOSE 2375/tcp 16 | 17 | COPY --from=builder /docker-socket-protector /docker-socket-protector 18 | 19 | COPY profiles /profiles 20 | 21 | CMD ["/docker-socket-protector"] 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 knrdl 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Docker Socket Protector 2 | Some containerized applications (e.g. portainer, traefik, watchtower, ouroboros) demand access to the Docker socket (`/var/run/docker.sock`). But exposing the Docker socket to a container basically equals giving it **full root privileges** to the host system. Especially when the application just needs to read some information via the Docker socket the access should be locked down. Docker Socket Protector can do this by limiting the control an application gets over the Docker daemon. Only the required functions will be exposed via a customized Docker socket. Therefore, this little program acts as a customizable filtering proxy. 3 | 4 | ```mermaid 5 | graph LR 6 | a[Container] 7 | b[Docker\nSocket\nProtector] 8 | c[Docker\nDaemon] 9 | 10 | 11 | subgraph Docker Network 12 | a-->| tcp://docker-socket-protector:2375 | b 13 | end 14 | 15 | b-->|/var/run/docker.sock| c 16 | 17 | 18 | click b href "https://github.com/knrdl/docker-socket-protector" 19 | click c href "https://docs.docker.com/config/daemon/" 20 | ``` 21 | 22 | ## Setup 23 | 24 | | Env Var | Values | | 25 | |--------------|------------------|----------------------| 26 | | LOG_REQUESTS | `true` / `false` | Whether requests are written to stdout. Useful to craft custom profiles. | 27 | | PROFILE | See [predefined profiles](./profiles/)| The filename of the profile to apply rules from. | 28 | 29 | This software filters requests to the Docker socket based on a whitelist of rules. These rules are stored in a profile file. See [here](./profiles/) for examples shipped with the software. To allow all requests use the profile `unprotected`. 30 | 31 | The restricted docker socket is provided via TCP on port 2375. See example below. 32 | 33 | ### Traefik Example Setup 34 | 35 | Traefik is a modern reverse proxy with a great [docker integration](https://doc.traefik.io/traefik/providers/docker/). It extracts routing rules from container labels. Therefore, Traefik needs **readonly** access to the Docker socket. 36 | 37 | ```yaml 38 | version: '3.9' 39 | 40 | services: 41 | 42 | traefik: 43 | image: traefik 44 | command: "--providers.docker.endpoint=http://docker-socket-protector:2375" 45 | ports: 46 | - "80:80" 47 | networks: 48 | - docker_socket_net 49 | 50 | docker-socket-protector: 51 | image: knrdl/docker-socket-protector # or: ghcr.io/knrdl/docker-socket-protector 52 | hostname: docker-socket-protector 53 | read_only: true 54 | cap_drop: [ all ] 55 | environment: 56 | LOG_REQUESTS: "true" 57 | PROFILE: "traefik" 58 | volumes: 59 | - /var/run/docker.sock:/var/run/docker.sock 60 | networks: 61 | - docker_socket_net 62 | mem_limit: 128mb 63 | 64 | networks: 65 | docker_socket_net: 66 | attachable: false 67 | internal: true 68 | ``` 69 | 70 | ### Portainer Example Setup 71 | 72 | [Portainer](https://docs.portainer.io/) is a container management web interface 73 | with RBAC controls for standalone and Swarm based Docker hosts. Portainer 74 | requires access to both disruptive and destructive API calls and two 75 | profiles are provided for a standalone Portainer instance; one profile is 76 | read-only `portainer-ro`, while the other has write access `portainer-rw`. 77 | 78 | ```yaml 79 | version: '3.9' 80 | 81 | services: 82 | 83 | portainer: 84 | image: portainer/portainer-ce:latest 85 | security_opt: 86 | - no-new-privileges:true 87 | command: "-H tcp://docker-socket-protector:2375" 88 | ports: 89 | - "127.0.0.1:9000:9000" 90 | networks: 91 | - docker_socket_net 92 | 93 | docker-socket-protector: 94 | image: knrdl/docker-socket-protector # or: ghcr.io/knrdl/docker-socket-protector 95 | hostname: docker-socket-protector 96 | read_only: true 97 | cap_drop: [ all ] 98 | environment: 99 | LOG_REQUESTS: 'true' 100 | PROFILE: 'portainer-rw' 101 | volumes: 102 | - /var/run/docker.sock:/var/run/docker.sock 103 | networks: 104 | - docker_socket_net 105 | mem_limit: 128mb 106 | 107 | networks: 108 | docker_socket_net: 109 | attachable: false 110 | internal: true 111 | ``` 112 | 113 | ### Crafting custom profiles 114 | 115 | 1. Record all requests to the Docker socket, e.g.: `docker run -it --rm -e PROFILE=unprotected -e LOG_REQUESTS=true -p127.0.0.1:2375:2375 -v /var/run/docker.sock:/var/run/docker.sock knrdl/docker-socket-protector` 116 | 2. Analyze the lines starting with "request rule:" and extract rules into regular expressions 117 | 3. Write a custom profile file "supersecure" and start the software with it, e.g.: `docker run -it --rm -e PROFILE=supersecure -e LOG_REQUESTS=true -p127.0.0.1:2375:2375 -v /var/run/docker.sock:/var/run/docker.sock -v $PWD/supersecure:/profiles/supersecure:ro knrdl/docker-socket-protector` 118 | 4. Test the profile, e.g.: `sudo DOCKER_HOST=tcp://localhost:2375 docker ps` 119 | 120 | ## FAQ 121 | 122 | ### Why not just mount the docker socket as read only? 123 | Mounting as `/var/run/docker.sock:/var/run/docker.sock:ro` (**ro** = readonly) just prevents the container from changing file permissions on the socket file. The socket as pipe object stays writable, so you can still send arbitrary requests to the socket. Nevertheless, using **ro** mode for socket mount is not wrong, but won't solve the security problem! 124 | 125 | ### Alternatives 126 | 127 | https://docs.docker.com/engine/extend/plugins_authorization/ 128 | -------------------------------------------------------------------------------- /profiles/portainer-ro: -------------------------------------------------------------------------------- 1 | # Profile for Portainer CE which limits Portainer to read-only operations. Consider that additional 2 | # rules may be required if you operate Portainer with Docker Swarm to manage secrets, configs 3 | # or services not available on standalone Docker hosts 4 | 5 | # Method Path 6 | 7 | ### Read only operations ### 8 | HEAD /_ping 9 | GET /(v\d+\.\d+/)?containers/[0-9a-fA-F]+/(json|top|logs|stats)(\?.*|/.*)? 10 | GET /(v\d+\.\d+/)?containers/json\?all=.* 11 | GET /(v\d+\.\d+/)?(_ping|events|images|info|networks|plugins|version|volumes)(\?.*|/.*)? 12 | -------------------------------------------------------------------------------- /profiles/portainer-rw: -------------------------------------------------------------------------------- 1 | # Profile for Portainer CE which allows destructive / disruptive operations to containers, images 2 | # networks and volumes. Consider that additional rules may be required if you operate Portainer 3 | # with Docker Swarm to manage secrets, configs or services not available on standalone Docker hosts 4 | 5 | # Method Path 6 | 7 | ### Read only operations ### 8 | HEAD /_ping 9 | GET /(v\d+\.\d+/)?containers/[0-9a-fA-F]+/(json|top|logs|stats)(\?.*|/.*)? 10 | GET /(v\d+\.\d+/)?containers/json\?all=.* 11 | GET /(v\d+\.\d+/)?(_ping|events|images|info|networks|plugins|version|volumes)(\?.*|/.*)? 12 | 13 | ### Disruptive and create operations 14 | POST /(v\d+\.\d+/)?containers/[0-9a-fA-F]+(\?.*|/.*)? 15 | POST /(v\d+\.\d+/)?(containers|images|networks|volumes)/create(\?.*|/.*)? 16 | POST /(v\d+\.\d+/)?(exec|networks)/[0-9a-fA-F]+(\?.*|/.*)? 17 | POST /(v\d+\.\d+/)?(exec|networks)(/bridge)?/[0-9a-fA-F]+(\?.*|/.*)? 18 | 19 | ### Destructive operations 20 | DELETE /(v\d+\.\d+/)?(containers|images|networks|volumes)(\?.*|/.*)? -------------------------------------------------------------------------------- /profiles/promtail: -------------------------------------------------------------------------------- 1 | # Method Path 2 | HEAD /_ping 3 | GET /(v\d+\.\d+/)?containers/[0-9a-fA-F]+/json 4 | GET /(v\d+\.\d+/)?containers/[0-9a-fA-F]+/logs\?.+ 5 | GET /(v\d+\.\d+/)?containers/json(\?.+|) 6 | GET /(v\d+\.\d+/)?networks -------------------------------------------------------------------------------- /profiles/readonly: -------------------------------------------------------------------------------- 1 | # Only allow reading operations 2 | # Be aware that clients still can e.g. read secrets from environment variables 3 | 4 | # Method Path 5 | (GET|HEAD) /.* -------------------------------------------------------------------------------- /profiles/telegraf: -------------------------------------------------------------------------------- 1 | # Method Path 2 | HEAD /_ping 3 | GET /(v\d+\.\d+/)?info 4 | GET /(v\d+\.\d+/)?containers/[0-9a-fA-F]+/json(\?.+|) 5 | GET /(v\d+\.\d+/)?containers/[0-9a-fA-F]+/logs(\?.+|) 6 | GET /(v\d+\.\d+/)?containers/[0-9a-fA-F]+/stats(\?.+|) 7 | GET /(v\d+\.\d+/)?containers/json(\?.+|) 8 | -------------------------------------------------------------------------------- /profiles/traefik: -------------------------------------------------------------------------------- 1 | # Method Path 2 | GET /v\d+\.\d+/containers/json\?limit=\d+ 3 | GET /v\d+\.\d+/containers/[0-9a-fA-F]+/json 4 | GET /v\d+\.\d+/events\?filters=%7B%22type%22%3A%7B%22container%22%3Atrue%7D%7D 5 | GET /v\d+\.\d+/networks 6 | GET /v\d+\.\d+/networks\?filters=.+ 7 | GET /v\d+\.\d+/version 8 | GET /v\d+\.\d+/services 9 | GET /v\d+\.\d+/tasks 10 | -------------------------------------------------------------------------------- /profiles/traefik-swarm: -------------------------------------------------------------------------------- 1 | # Profile for Traefik with assisting logrotate container running in docker swarm. 2 | # The profile allows the logrotate to POST USR1 signal to notify Traefik 3 | # to recreate/reopen log files. 4 | 5 | ### Read only operations ### 6 | HEAD /_ping 7 | GET /(v\d+\.\d+/)(version|services|networks|tasks|events)(\?.*|/.*)? 8 | GET /(v\d+\.\d+/)containers/json(\?.*|/.*)? 9 | 10 | ### Allow to send USR1 signal ### 11 | POST /(v\d+\.\d+)/containers/([a-z0-9]+)/kill\?signal=USR1 12 | -------------------------------------------------------------------------------- /profiles/trivy: -------------------------------------------------------------------------------- 1 | # Profile for Aquasec Trivy (https://aquasecurity.github.io/trivy/) image scans 2 | 3 | # Method Path 4 | HEAD /_ping 5 | GET /v\d+\.\d+/images/.+/json 6 | GET /v\d+\.\d+/images/.+/history 7 | GET /v\d+\.\d+/images/get\?names=.+ 8 | 9 | GET /v\d+\.\d+/containers/json\?all=1 -------------------------------------------------------------------------------- /profiles/unprotected: -------------------------------------------------------------------------------- 1 | # Method Path 2 | [A-Z]+ /.* -------------------------------------------------------------------------------- /src/go.mod: -------------------------------------------------------------------------------- 1 | module docker-socket-protector 2 | 3 | go 1.23 4 | -------------------------------------------------------------------------------- /src/go.sum: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/knrdl/docker-socket-protector/96eadb0d8d501506911f6eda52dd8afde6336972/src/go.sum -------------------------------------------------------------------------------- /src/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io/fs" 5 | "log" 6 | "net/http" 7 | "os" 8 | "strconv" 9 | ) 10 | 11 | func IsSocket(path string) bool { 12 | fileInfo, err := os.Stat(path) 13 | if err != nil { 14 | return false 15 | } 16 | return fileInfo.Mode().Type() == fs.ModeSocket 17 | } 18 | 19 | func main() { 20 | logRequests, err := strconv.ParseBool(os.Getenv("LOG_REQUESTS")) 21 | if err != nil { 22 | log.Fatal("Environment variable LOG_REQUESTS must be 'true' or 'false'.") 23 | } 24 | profileName := os.Getenv("PROFILE") 25 | 26 | if profileName == "" { 27 | log.Fatal("No profile given. Use 'unprotected' to allow all requests.") 28 | } 29 | if profileName == "unprotected" && !logRequests { 30 | log.Fatal("Aborting. Allowing all requests (as profile is 'unprotected') and not logging requests is not recommended.") 31 | } 32 | 33 | if !IsSocket("/var/run/docker.sock") { 34 | log.Fatal("No docker socket provided at '/var/run/docker.sock'.") 35 | } 36 | 37 | profileRules := getProfile(profileName) 38 | 39 | handler := &FilterProxy{ 40 | Rules: &profileRules, 41 | LogRequests: logRequests, 42 | Forwarder: NewForwarder(), 43 | } 44 | 45 | if err := http.ListenAndServe(":2375", handler); err != nil { 46 | log.Fatal("error listing on port: ", err) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/profile.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "log" 6 | "os" 7 | "path/filepath" 8 | "regexp" 9 | "strings" 10 | ) 11 | 12 | type ProfileRule struct { 13 | MethodRegex *regexp.Regexp 14 | UrlRegex *regexp.Regexp 15 | } 16 | 17 | func getProfile(profileName string) (rules []ProfileRule) { 18 | profilePath := filepath.Join("/profiles/", profileName) 19 | 20 | if _, err := os.Stat(profilePath); err != nil { 21 | log.Fatal("Could not read profile file: ", err) 22 | } 23 | 24 | profileHandle, err := os.Open(profilePath) 25 | if err != nil { 26 | log.Fatal("Error opening profile file: ", err) 27 | } 28 | defer profileHandle.Close() 29 | 30 | scanner := bufio.NewScanner(profileHandle) 31 | configFormatRegex := regexp.MustCompile(`^(\S+)\s+(/.+)$`) 32 | for scanner.Scan() { 33 | line := strings.TrimSpace(scanner.Text()) 34 | if !strings.HasPrefix(line, "#") && line != "" { 35 | parsed := configFormatRegex.FindStringSubmatch(line) 36 | if parsed == nil { 37 | log.Fatal("Profile rule is invalid: ", line) 38 | } 39 | methodRegex, err := regexp.Compile("^" + strings.TrimSuffix(strings.TrimPrefix(parsed[1], "^"), "$") + "$") 40 | if err != nil { 41 | log.Fatal("Profile rule is invalid: ", err) 42 | } 43 | urlRegex, err := regexp.Compile("^" + strings.TrimSuffix(strings.TrimPrefix(parsed[2], "^"), "$") + "$") 44 | if err != nil { 45 | log.Fatal("Profile rule is invalid: ", err) 46 | } 47 | rules = append(rules, ProfileRule{MethodRegex: methodRegex, UrlRegex: urlRegex}) 48 | } 49 | } 50 | if scanner.Err() != nil { 51 | log.Fatal("Error reading profile file: ", scanner.Err()) 52 | } 53 | if len(rules) == 0 { 54 | log.Fatal("Profile file doesn't contain rules. Aborting.") 55 | } 56 | return 57 | } 58 | -------------------------------------------------------------------------------- /src/proxy.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "html" 7 | "net" 8 | "net/http" 9 | "net/http/httputil" 10 | "os" 11 | "regexp" 12 | ) 13 | 14 | type FilterProxy struct { 15 | Rules *[]ProfileRule 16 | Forwarder *httputil.ReverseProxy 17 | LogRequests bool 18 | } 19 | 20 | func NewForwarder() *httputil.ReverseProxy { 21 | director := func(req *http.Request) { 22 | req.URL.Scheme = "http" 23 | req.URL.Host = "docker" 24 | } 25 | 26 | proxy := &httputil.ReverseProxy{ 27 | Director: director, 28 | Transport: &http.Transport{ 29 | DialContext: func(_ context.Context, _, _ string) (net.Conn, error) { 30 | return net.Dial("unix", "/var/run/docker.sock") 31 | }, 32 | }, 33 | } 34 | 35 | return proxy 36 | } 37 | 38 | /* 39 | a log message should not contain line breaks from arbitrary inputs as an attacker could otherwise fake log messages 40 | */ 41 | var newLineRegex = regexp.MustCompile(`\r?\n`) 42 | 43 | func (p *FilterProxy) ServeHTTP(res http.ResponseWriter, req *http.Request) { 44 | 45 | if p.LogRequests { 46 | msg := "request rule: " + regexp.QuoteMeta(req.Method) + " " + regexp.QuoteMeta(req.URL.String()) 47 | fmt.Fprintln(os.Stdout, newLineRegex.ReplaceAllString(msg, " ")) 48 | } 49 | 50 | if req.URL.Scheme != "" { 51 | msg := "unsupported protocol scheme: " + req.URL.String() 52 | http.Error(res, html.EscapeString(msg), http.StatusBadRequest) 53 | fmt.Fprintln(os.Stderr, newLineRegex.ReplaceAllString(msg, " ")) 54 | return 55 | } 56 | 57 | reqOk := false 58 | for _, rule := range *p.Rules { 59 | if rule.UrlRegex.MatchString(req.URL.String()) && rule.MethodRegex.MatchString(req.Method) { 60 | reqOk = true 61 | break 62 | } 63 | } 64 | 65 | if !reqOk { 66 | msg := "request denied: " + req.Method + " " + req.URL.String() 67 | http.Error(res, html.EscapeString(msg), http.StatusForbidden) 68 | fmt.Fprintln(os.Stderr, newLineRegex.ReplaceAllString(msg, " ")) 69 | return 70 | } 71 | 72 | p.Forwarder.ServeHTTP(res, req) 73 | } 74 | --------------------------------------------------------------------------------