diff --git a/contrib/docker/Dockerfile b/contrib/docker/Dockerfile new file mode 100644 index 0000000..25470b0 --- /dev/null +++ b/contrib/docker/Dockerfile @@ -0,0 +1,45 @@ +# Build stage +FROM golang:1.23-bookworm AS build + +WORKDIR /src +COPY . . + +RUN go mod download +RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /gosh . + +# Runtime stage +FROM gcr.io/distroless/static-debian12:latest + +# gosh needs root at startup to chroot() and setuid()/setgid() +# It drops privileges itself to the configured user/group + +# Copy the binary +COPY --from=build /gosh /gosh + +# Create passwd/group files for user.Lookup() to work +# Using UID/GID 65532 which is the "nonroot" user in distroless +COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ + +# We need to add passwd and group files for gosh's user.Lookup() +# distroless has no shell, so we create these in the build stage +FROM golang:1.23-bookworm AS passwd-builder +RUN echo 'root:x:0:0:root:/root:/sbin/nologin' > /tmp/passwd && \ + echo 'gosh:x:65532:65532:gosh:/nonexistent:/sbin/nologin' >> /tmp/passwd +RUN echo 'root:x:0:' > /tmp/group && \ + echo 'gosh:x:65532:' >> /tmp/group + +# Final stage +FROM gcr.io/distroless/static-debian12:latest + +COPY --from=build /gosh /gosh +COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ +COPY --from=passwd-builder /tmp/passwd /etc/passwd +COPY --from=passwd-builder /tmp/group /etc/group + +# Create store directory (will be owned by root initially, gosh chowns it) +WORKDIR /data + +EXPOSE 8080 + +ENTRYPOINT ["/gosh"] +CMD ["-config", "/config/gosh.yml"] diff --git a/contrib/docker/config/gosh.yml b/contrib/docker/config/gosh.yml new file mode 100644 index 0000000..44a0c78 --- /dev/null +++ b/contrib/docker/config/gosh.yml @@ -0,0 +1,53 @@ +--- +# Container configuration for gosh +# +# Mount structure: +# /config/gosh.yml - this file +# /config/index.html - custom index template (optional) +# /config/favicon.ico - favicon (optional) +# /config/custom.css - custom styles (optional) +# /data/ - persistent storage + +user: "gosh" +group: "gosh" + +store: + path: "/data/store" + + id_generator: + type: "random" + length: 8 + +webserver: + listen: + protocol: "tcp" + bound: ":8080" + + protocol: "http" + + url_prefix: "" + + # Custom index.html template + custom_index: "/config/index.html" + + # Optional: static files served by gosh + # Uncomment and adjust as needed + # static_files: + # "/favicon.ico": + # path: "/config/favicon.ico" + # mime: "image/vnd.microsoft.icon" + # "/custom.css": + # path: "/config/custom.css" + # mime: "text/css" + + item_config: + max_size: "10MiB" + max_lifetime: "24h" + + mime_drop: + - "application/vnd.microsoft.portable-executable" + - "application/x-msdownload" + mime_map: + "text/html": "text/plain" + + contact: "admin@example.com" diff --git a/contrib/docker/config/index.html b/contrib/docker/config/index.html new file mode 100644 index 0000000..2ddf407 --- /dev/null +++ b/contrib/docker/config/index.html @@ -0,0 +1,146 @@ + + + + gosh! Go Share + + + + + + + +

# gosh! Go Share

+

+ Upload your files to this server and share them with your friends or, if + non-existent, shady people from the Internet. +

+

+ Your file will expire after {{.Expires}} or earlier, if explicitly + specified. Optionally, the file can be deleted directly after the first + retrieval. For each upload, a deletion URL will also be generated which + can be used to delete the file before expiration. In addition, the + maximum file size is {{.Size}}. +

+

+ This is no place to share questionable or illegal data. Please use another + service or stop it completely. Get some help. +

+

+ The gosh software can be obtained from + https://github.com/oxzi/gosh +

+ +

## Posting

+ +

### curl

+ + HTTP POST your file: + +
$ curl -F 'file=@foo.png' {{.Proto}}://{{.Hostname}}{{.Prefix}}/
+ + Burn after reading: + +
$ curl -F 'file=@foo.png' -F 'burn=1' {{.Proto}}://{{.Hostname}}{{.Prefix}}/
+ + Set a custom expiry date, e.g., one minute: + +
$ curl -F 'file=@foo.png' -F 'time=1m' {{.Proto}}://{{.Hostname}}{{.Prefix}}/
+ + Or all together: + +
$ curl -F 'file=@foo.png' -F 'time=1m' -F 'burn=1' {{.Proto}}://{{.Hostname}}{{.Prefix}}/
+ + Print only URL as response: + +
$ curl -F 'file=@foo.png' -F {{.Proto}}://{{.Hostname}}{{.Prefix}}/?onlyURL
+ +

### form

+ +
+
+ + + + + + +
+ +
+ +

## Privacy

+ + This software stores the IP address for each upload. This information is + stored as long as the file is available. A normal download is logged without + user information. + +

## Abuse

+ + If, for whatever reason, you would like to have a file removed prematurely, + please write an e-mail to + <{{.EMail}}>. Please allow me a + certain amount of time to react and work on your request. + + diff --git a/contrib/docker/docker-compose.yml b/contrib/docker/docker-compose.yml new file mode 100644 index 0000000..680e32d --- /dev/null +++ b/contrib/docker/docker-compose.yml @@ -0,0 +1,17 @@ +services: + gosh: + build: + context: ../.. + dockerfile: contrib/docker/Dockerfile + image: gosh:distroless + ports: + - "8080:8080" + volumes: + # Config directory with gosh.yml and optional static files + - ./config:/config:ro + # Persistent data storage + - gosh-data:/data + restart: unless-stopped + +volumes: + gosh-data: diff --git a/contrib/docker/gosh.container.yml b/contrib/docker/gosh.container.yml new file mode 100644 index 0000000..4c77e1c --- /dev/null +++ b/contrib/docker/gosh.container.yml @@ -0,0 +1,54 @@ +--- +# Container configuration for gosh +# +# Mount structure: +# /config/gosh.yml - this file +# /config/index.html - custom index template (optional) +# /config/favicon.ico - favicon (optional) +# /config/custom.css - custom styles (optional) +# /data/ - persistent storage + +user: "gosh" +group: "gosh" + +store: + path: "/data/store" + + id_generator: + type: "random" + length: 8 + +webserver: + listen: + protocol: "tcp" + bound: ":8080" + + protocol: "http" + + url_prefix: "" + + # Optional: custom index.html template + # Uncomment if you provide /config/index.html + # custom_index: "/config/index.html" + + # Optional: static files served by gosh + # Uncomment and adjust as needed + # static_files: + # "/favicon.ico": + # path: "/config/favicon.ico" + # mime: "image/vnd.microsoft.icon" + # "/custom.css": + # path: "/config/custom.css" + # mime: "text/css" + + item_config: + max_size: "10MiB" + max_lifetime: "24h" + + mime_drop: + - "application/vnd.microsoft.portable-executable" + - "application/x-msdownload" + mime_map: + "text/html": "text/plain" + + contact: "admin@example.com" diff --git a/gosh_store.go b/gosh_store.go index b40146a..f4d2546 100644 --- a/gosh_store.go +++ b/gosh_store.go @@ -4,6 +4,7 @@ import ( "log/slog" "os" "os/signal" + "runtime" "golang.org/x/sys/unix" ) @@ -37,7 +38,7 @@ func ensureStoreDir(path, username, groupname string) error { } func mainStore(conf Config) { - slog.Debug("Starting store child", slog.Any("config", conf.Store)) + slog.Debug("Starting store child", slog.Any("config", conf.Store), slog.String("arch", runtime.GOARCH)) var idGenerator func() (string, error) switch conf.Store.IdGenerator.Type { @@ -70,26 +71,31 @@ func mainStore(conf Config) { os.Exit(1) } - err = restrict(restrict_linux_seccomp, - []string{ - "@system-service", - "~@chown", - "~@clock", - "~@cpu-emulation", - "~@debug", - "~@keyring", - "~@memlock", - "~@module", - "~@mount", - "~@privileged", - "~@reboot", - "~@sandbox", - "~@setuid", - "~@swap", - /* @process */ "~execve", "~execveat", "~fork", "~kill", - /* @network-io */ "~bind", "~connect", "~listen", - "fstatat", // for aarch64, same as newfstatat - }) + seccompFilter := []string{ + "@system-service", + "~@chown", + "~@clock", + "~@cpu-emulation", + "~@debug", + "~@keyring", + "~@memlock", + "~@module", + "~@mount", + "~@privileged", + "~@reboot", + "~@sandbox", + "~@setuid", + "~@swap", + /* @process */ "~execve", "~execveat", "~fork", "~kill", + /* @network-io */ "~bind", "~connect", "~listen", + } + // fstatat syscall name differs per architecture + if runtime.GOARCH == "arm64" { + seccompFilter = append(seccompFilter, "fstatat") + } else { + seccompFilter = append(seccompFilter, "newfstatat") + } + err = restrict(restrict_linux_seccomp, seccompFilter) if err != nil { slog.Error("Failed to apply seccomp-bpf filter", slog.Any("error", err)) os.Exit(1)