~gitpocket/gitpocket

1

build(docker): add a hardened container image

commit
2c3fc6b065076eac0c6af6d944191a4726d86201 browse tree
parent
05701a4801f8776fd20963cf4e3b4d41ae24ce82
author
obscurejames <354576047+obscurejames@users.noreply.github.com>
signature
Unverified signed with an unregistered key
date
2026-11-01 10:33 UTC

6 files changed, +90 −2

.dockerignore added

@@ -0,0 +1,4 @@
1+.git
2+gitpocket
3+*.db
4+deploy

Dockerfile added

@@ -0,0 +1,15 @@
1+FROM golang:1.27-alpine AS build
2+WORKDIR /src
3+COPY go.mod go.sum ./
4+RUN go mod download
5+COPY . .
6+RUN CGO_ENABLED=0 go build -trimpath -ldflags='-s -w' -o /gitpocket .
7+
8+FROM alpine:3.22
9+RUN apk add --no-cache git gnupg openssh-keygen ca-certificates
10+COPY --from=build /gitpocket /usr/local/bin/gitpocket
11+ENV HOME=/tmp GOMEMLIMIT=192MiB
12+USER 65532:65532
13+EXPOSE 3200
14+ENTRYPOINT ["/usr/local/bin/gitpocket", "-root", "/srv/gitpocket"]
15+CMD ["serve", "-listen", ":3200"]

deploy/compose.yaml added

@@ -0,0 +1,26 @@
1+services:
2+ gitpocket:
3+ build: ..
4+ image: gitpocket
5+ command: ["serve", "-listen", ":3200", "-host", "${GITPOCKET_HOST:?set GITPOCKET_HOST}", "-proxy", "172.31.255.1/32"]
6+ user: "${GITPOCKET_UID:?set GITPOCKET_UID}:${GITPOCKET_GID:?set GITPOCKET_GID}"
7+ ports:
8+ - "127.0.0.1:3200:3200"
9+ volumes:
10+ - /srv/gitpocket:/srv/gitpocket
11+ read_only: true
12+ tmpfs:
13+ - /tmp:size=64m,mode=1777,noexec,nosuid,nodev
14+ cap_drop: [ALL]
15+ security_opt:
16+ - no-new-privileges:true
17+ mem_limit: 512m
18+ pids_limit: 256
19+ restart: unless-stopped
20+ networks: [gitpocket]
21+networks:
22+ gitpocket:
23+ ipam:
24+ config:
25+ - subnet: 172.31.255.0/28
26+ gateway: 172.31.255.1

limit.go modified

@@ -55,12 +55,26 @@ func (l *limiter) fail(key string) {
5555 l.hits[key] = append(l.prune(key, now), now)
5656 }
5757
58+var trustedProxies []*net.IPNet
59+
60+func trustedProxy(ip net.IP) bool {
61+ if ip.IsLoopback() {
62+ return true
63+ }
64+ for _, n := range trustedProxies {
65+ if n.Contains(ip) {
66+ return true
67+ }
68+ }
69+ return false
70+}
71+
5872 func clientIP(r *http.Request) string {
5973 host, _, err := net.SplitHostPort(r.RemoteAddr)
6074 if err != nil {
6175 host = r.RemoteAddr
6276 }
63- if ip := net.ParseIP(host); ip != nil && ip.IsLoopback() {
77+ if ip := net.ParseIP(host); ip != nil && trustedProxy(ip) {
6478 if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
6579 hops := strings.Split(xff, ",")
6680 return strings.TrimSpace(hops[len(hops)-1])

main.go modified

@@ -4,6 +4,7 @@ import (
44 "flag"
55 "fmt"
66 "log"
7+ "net"
78 "net/http"
89 "os"
910 "path/filepath"
@@ -16,7 +17,7 @@ func main() {
1617 flag.Usage = func() {
1718 fmt.Fprintln(os.Stderr, `usage: gitpocket [-root dir] command
1819
19- serve [-listen addr] [-host name] [-name text] [-ssh-user name] run the web server
20+ serve [-listen addr] [-host name] [-name text] [-ssh-user name] [-proxy cidr,...] run the web server
2021 keys user home type key sshd AuthorizedKeysCommand
2122 shell user forced command for ssh git access
2223 invite print a single use registration code
@@ -218,7 +219,18 @@ func serve(root string, args []string) {
218219 host := fs.String("host", "localhost", "public hostname used in clone urls")
219220 name := fs.String("name", "gitpocket", "site name shown in the header")
220221 sshUser := fs.String("ssh-user", "git", "unix user that serves git over ssh, shown in clone urls")
222+ proxies := fs.String("proxy", "", "comma separated networks besides loopback whose X-Forwarded-For is trusted")
221223 fs.Parse(args)
224+ for _, cidr := range strings.Split(*proxies, ",") {
225+ if cidr = strings.TrimSpace(cidr); cidr == "" {
226+ continue
227+ }
228+ _, n, err := net.ParseCIDR(cidr)
229+ if err != nil {
230+ log.Fatalf("-proxy %s: %v", cidr, err)
231+ }
232+ trustedProxies = append(trustedProxies, n)
233+ }
222234 store := mustStore(root)
223235 srv := &http.Server{
224236 Addr: *listen,

unit_test.go modified

@@ -1,6 +1,7 @@
11 package main
22
33 import (
4+ "net"
45 "net/http/httptest"
56 "strings"
67 "testing"
@@ -130,3 +131,19 @@ func TestRelativeLinkBaseEscaped(t *testing.T) {
130131 t.Errorf("base broke out of the attribute: %s", out)
131132 }
132133 }
134+
135+func TestTrustedProxy(t *testing.T) {
136+ _, n, _ := net.ParseCIDR("172.31.255.0/28")
137+ trustedProxies = []*net.IPNet{n}
138+ defer func() { trustedProxies = nil }()
139+ r := httptest.NewRequest("GET", "/", nil)
140+ r.Header.Set("X-Forwarded-For", "203.0.113.9, 198.51.100.7")
141+ r.RemoteAddr = "172.31.255.1:5000"
142+ if got := clientIP(r); got != "198.51.100.7" {
143+ t.Errorf("trusted proxy hop ignored: %s", got)
144+ }
145+ r.RemoteAddr = "192.0.2.4:5000"
146+ if got := clientIP(r); got != "192.0.2.4" {
147+ t.Errorf("untrusted peer could choose its address: %s", got)
148+ }
149+}