Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
.git
.github
dist
goup
goup-dev
goup-web
goup-dns
*.log
tests
docs
35 changes: 31 additions & 4 deletions .github/workflows/continuous.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ name: Build
on:
push:
branches: ["main"]
pull_request:
branches: ["main"]

jobs:
build:
quality:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

Expand All @@ -16,13 +17,39 @@ jobs:
with:
go-version-file: go.mod

- name: Test
run: go test -v ./...
- name: gofmt
run: |
fmtout=$(gofmt -l $(git ls-files '*.go'))
if [ -n "$fmtout" ]; then echo "Unformatted files:"; echo "$fmtout"; exit 1; fi

- name: go vet
run: go vet ./...

- name: staticcheck
run: go run honnef.co/go/tools/cmd/staticcheck@latest ./...

- name: govulncheck
run: go run golang.org/x/vuln/cmd/govulncheck@latest ./...

- name: Test (race)
run: go test -race ./...

build:
needs: quality
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod

- name: Build
run: go build -o goup cmd/goup/main.go

- uses: softprops/action-gh-release@v1
if: github.event_name == 'push'
with:
token: "${{ secrets.GITHUB_TOKEN }}"
tag_name: "continuous"
Expand Down
10 changes: 8 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: goreleaser
name: release

on:
push:
Expand Down Expand Up @@ -46,7 +46,13 @@ jobs:
CGO_ENABLED: 0
run: |
mkdir -p dist
GOOS=${{ matrix.goos }} GOARCH=${{ matrix.goarch }} go build -o dist/goup-${{ matrix.goos }}-${{ matrix.goarch }}${{ matrix.ext }} cmd/goup/main.go
VERSION="${GITHUB_REF_NAME}"
COMMIT="$(git rev-parse --short HEAD)"
DATE="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
LDFLAGS="-s -w -X github.com/mirkobrombin/goup/internal/cli.Version=${VERSION} -X github.com/mirkobrombin/goup/internal/cli.Commit=${COMMIT} -X github.com/mirkobrombin/goup/internal/cli.Date=${DATE}"
OUT="dist/goup-${{ matrix.goos }}-${{ matrix.goarch }}${{ matrix.ext }}"
GOOS=${{ matrix.goos }} GOARCH=${{ matrix.goarch }} go build -ldflags "${LDFLAGS}" -o "${OUT}" cmd/goup/main.go
sha256sum "${OUT}" | awk '{print $1}' > "${OUT}.sha256"

- uses: softprops/action-gh-release@v1
with:
Expand Down
48 changes: 48 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Changelog

All notable changes to this project are documented here. The format is based on
[Keep a Changelog](https://keepachangelog.com/), and the project aims to follow
[Semantic Versioning](https://semver.org/).

## [Unreleased]

### Added
- Automatic TLS via ACME/Let's Encrypt (TLS-ALPN-01) with on-disk certificate
caching and renewal (`ssl.acme`, `ssl.email`, `ssl.cache_dir`).
- Reverse-proxy load balancing across multiple upstreams with passive health
checks, ejection and failover (`proxy_upstreams`).
- Per-IP rate limiting (`rate_limit_rps`, `rate_limit_burst`).
- IP allow/deny lists (`allow_ips`, `deny_ips`).
- Request body size limit (`max_body_bytes`).
- CORS handling (`cors`) and configurable security headers (`security_headers`,
`hsts`, `hsts_max_age`).
- HTTP-to-HTTPS redirect (`force_https`) and `Cache-Control` for static assets
(`cache_control`).
- `goup version` command and `--version` flag, with version metadata injected at
release time.
- Configuration hot reload on `SIGHUP` (and `systemctl reload`).
- Log retention purge (`log_retention_days`).
- systemd unit, Dockerfile, and a production deployment guide.
- Bind-address options for the API and Dashboard (`api_bind`, `dashboard_bind`).

### Changed
- `goup validate` now performs real semantic validation: rejects unknown JSON
fields, checks certificate/root paths, port ranges, and cross-site conflicts.
- CI now runs gofmt, vet, staticcheck, govulncheck, and race-enabled tests;
release builds embed version metadata and publish SHA-256 checksums.

### Fixed
- Authentication for the API and Dashboard now fails closed when credentials are
unset, and refuses to start without them.
- Virtual-host servers select the correct TLS certificate by SNI instead of
silently serving plaintext.
- Auth-plugin sessions are bound to a random cookie token instead of the
spoofable client IP.
- Plugin child processes (Node.js, Python, Docker) are terminated on shutdown and
restart instead of being orphaned.
- On-the-fly gzip no longer breaks HTTP 304 revalidation.
- API and Dashboard servers now set read/idle timeouts (slowloris hardening).
- Fixed a goroutine/buffer leak when logging spawned-process output.
- DNS zone matching respects label boundaries; the forwarder enforces a
recursion ACL and truncates oversized UDP responses.
- Path traversal hardening across the API, config loading, and static serving.
22 changes: 22 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Build stage
FROM golang:1.26 AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
ARG VERSION=docker
ARG COMMIT=none
ARG DATE=unknown
RUN CGO_ENABLED=0 go build \
-ldflags "-s -w -X github.com/mirkobrombin/goup/internal/cli.Version=${VERSION} -X github.com/mirkobrombin/goup/internal/cli.Commit=${COMMIT} -X github.com/mirkobrombin/goup/internal/cli.Date=${DATE}" \
-o /out/goup cmd/goup/main.go

# Runtime stage
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=build /out/goup /usr/local/bin/goup
# Config in /etc/goup, logs/state in /var/lib/goup (XDG roots set to /etc and /var/lib).
ENV XDG_CONFIG_HOME=/etc
ENV XDG_DATA_HOME=/var/lib
VOLUME ["/etc/goup", "/var/lib/goup"]
ENTRYPOINT ["/usr/local/bin/goup"]
CMD ["start"]
70 changes: 60 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,15 @@ GoUP! is a minimal, tweakable web server written in Go. You can use it to serve
## Features

- Serve static files from a specified root directory
- Set up reverse proxies to backend services
- Support for SSL/TLS with custom certificates
- Custom headers for HTTP responses
- Reverse proxy to a single backend or load-balance across several (`proxy_upstreams`) with health checks and failover
- SSL/TLS with custom certificates or automatic Let's Encrypt certificates (`ssl.acme`)
- HTTP-to-HTTPS redirect, HSTS, and configurable security headers
- Edge hardening: per-IP rate limiting, IP allow/deny lists, request body limits, CORS
- Custom headers and `Cache-Control` for HTTP responses
- Support for multiple domains and virtual hosting
- Native Authoritative DNS Server (A, AAAA, CNAME, TXT, MX, NS)
- Logging to both console and files - JSON formatted (structured logs)
- Logging to both console and files - JSON formatted (structured logs), with date rotation and retention
- Zero-downtime config reload (`SIGHUP`), graceful shutdown, and a memory watchdog (SafeGuard)
- Optional TUI interface for real-time monitoring
- HTTP/2 and HTTP/3 support (not configurable, HTTP/1.1 is used for unencrypted connections, HTTP/2 and HTTP/3 for encrypted connections)

Expand Down Expand Up @@ -182,13 +185,21 @@ goup start --tui
- **Stop the Server:**

```bash
goup stop // Not implemented yet, use <Ctrl+C> to stop the server
goup stop
```

- **Restart the Server:**

```bash
goup restart // Not implemented yet, use <Ctrl+C> to stop the server and start it again
goup restart
# Or, if running under systemd, reload without downtime:
# systemctl reload goup (sends SIGHUP)
```

- **Print the Version:**

```bash
goup version
```

- **Generate Password Hash:**
Expand Down Expand Up @@ -230,11 +241,50 @@ Each site configuration is represented by a JSON file and meets the following st
- **root_directory**: Path to the directory containing static files. Leave empty if using `proxy_pass`
- **custom_headers**: Key-value pairs of custom headers to include in responses
- **proxy_pass**: URL to the backend service for reverse proxying. Leave empty if serving static files
- **proxy_upstreams**: List of backend URLs to load-balance across (round-robin with failover). Takes precedence over `proxy_pass`
- **ssl**:
- **enabled**: Set to `true` to enable SSL/TLS
- **certificate**: Path to the SSL certificate file
- **key**: Path to the SSL key file
- **request_timeout**: Timeout for client requests in seconds
- **acme**: Set to `true` to obtain and renew a Let's Encrypt certificate automatically (ignores certificate/key). Requires the domain to resolve to this host and port 443 to be reachable
- **email**: ACME account email (recommended)
- **cache_dir**: Where issued certificates are cached
- **request_timeout**: Read timeout for client requests in seconds (default 60; `-1` disables it)

**Additional site fields (all optional):**

| Field | Type | Description |
|---|---|---|
| `read_header_timeout` | int (s) | Header-read timeout (default 10) |
| `idle_timeout` | int (s) | Keep-alive idle timeout (default 120) |
| `max_header_bytes` | int | Maximum request header size |
| `max_body_bytes` | int | Maximum request body size (0 = unlimited) |
| `proxy_flush_interval` | duration | Reverse-proxy flush interval (e.g. `"100ms"`) |
| `buffer_size_kb` | int | Reverse-proxy copy buffer size in KB |
| `max_concurrent_connections` | int | Cap on in-flight requests (503 when exceeded) |
| `enable_logging` | bool | Per-site access logging (default true) |
| `file_server_mode` | bool | Plain directory listing, no branded pages |
| `force_https` | bool | Redirect plain HTTP to HTTPS (put on the :80 site) |
| `hsts` | bool | Send `Strict-Transport-Security` when served over TLS |
| `hsts_max_age` | int (s) | HSTS max-age (default 31536000) |
| `security_headers` | bool | Add `X-Content-Type-Options`, `X-Frame-Options`, `Referrer-Policy` |
| `cache_control` | string | `Cache-Control` value for static responses |
| `allow_ips` / `deny_ips` | []CIDR | IP allow/deny lists |
| `rate_limit_rps` | float | Per-IP requests/second (0 = disabled) |
| `rate_limit_burst` | int | Per-IP burst size |
| `cors` | object | CORS: `allowed_origins`, `allowed_methods`, `allowed_headers`, `allow_credentials`, `max_age` |
| `plugin_configs` | object | Per-plugin configuration (see Plugins) |

Global settings (`conf.global.json`) also support `api_bind` / `dashboard_bind`
(bind addresses, empty = all interfaces) and `log_retention_days` (auto-purge
logs older than N days, 0 = keep forever).

Run `goup validate` to check every config file: it reports JSON typos (unknown
fields), missing certificate/root paths, invalid ports, and cross-site conflicts
(duplicate domains, or a port mixing SSL and non-SSL sites).

See [Running in production](docs/production.md) for systemd, Docker, non-root
port binding, and zero-downtime reloads.

## Logging

Expand Down Expand Up @@ -431,6 +481,6 @@ GoUP! is released under the [MIT License](LICENSE).

---

**Note:** This project is for educational purposes and may not be suitable
for production environments without additional security and performance
considerations, yet.
**Note:** Review the [production guide](docs/production.md) before deploying:
run GoUp as a non-root service, secure the API/Dashboard with credentials, and
enable TLS, HSTS, and rate limiting for internet-facing sites.
84 changes: 84 additions & 0 deletions docs/production.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Running GoUp in production

This guide covers running GoUp as a long-lived service: as a systemd unit, in a
container, binding privileged ports without root, reloading configuration, and
log retention.

## Directory layout

GoUp resolves its paths from the XDG environment variables:

- Configuration: `$XDG_CONFIG_HOME/goup` (per-site `*.json` plus `conf.global.json`)
- Logs and state: `$XDG_DATA_HOME/goup/logs`

For a system service the recommended layout is:

- `XDG_CONFIG_HOME=/etc` so configuration lives in `/etc/goup`
- `XDG_DATA_HOME=/var/lib` so logs live in `/var/lib/goup/logs`

## systemd

A ready-to-use unit is provided at [`init/goup.service`](../init/goup.service).

```bash
# Create a dedicated service user and directories
sudo useradd --system --home /var/lib/goup --shell /usr/sbin/nologin goup
sudo mkdir -p /etc/goup /var/lib/goup
sudo chown -R goup:goup /etc/goup /var/lib/goup

# Install the binary and the unit
sudo install -m 0755 goup /usr/local/bin/goup
sudo install -m 0644 init/goup.service /etc/systemd/system/goup.service

sudo systemctl daemon-reload
sudo systemctl enable --now goup
```

The unit grants `CAP_NET_BIND_SERVICE`, so GoUp can bind ports 80 and 443
without running as root.

Reload configuration and certificates without downtime:

```bash
sudo systemctl reload goup # sends SIGHUP
```

## Binding ports 80/443 as non-root

If you do not use the systemd unit, grant the capability directly to the binary:

```bash
sudo setcap 'cap_net_bind_service=+ep' /usr/local/bin/goup
```

## Docker

```bash
docker build -t goup .
docker run -d --name goup \
-p 80:80 -p 443:443 \
-v /etc/goup:/etc/goup \
-v /var/lib/goup:/var/lib/goup \
goup
```

The image is based on distroless `nonroot`; mount your configuration into
`/etc/goup`.

## HTTP to HTTPS redirect

Create two site configs for the same domain: one on port 80 with
`"force_https": true`, and one on port 443 with TLS enabled. Requests to the
plain-HTTP listener are 301-redirected to HTTPS. Enable `"hsts": true` on the
HTTPS site to send `Strict-Transport-Security`.

## Log retention

Set `log_retention_days` in `conf.global.json` to automatically delete log files
(and heap dumps) older than N days. `0` (the default) keeps logs forever.

## Health checks

Every site exposes an unauthenticated `/up` endpoint that returns `200 ok` when
ready and `503` while the server is draining during shutdown. Point your load
balancer or orchestrator liveness/readiness probe at it.
35 changes: 35 additions & 0 deletions init/goup.service
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
[Unit]
Description=GoUp web server
Documentation=https://github.com/mirkobrombin/goup
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=goup
Group=goup

# Configuration lives in /etc/goup, state/logs in /var/lib/goup.
Environment=XDG_CONFIG_HOME=/etc
Environment=XDG_DATA_HOME=/var/lib

ExecStart=/usr/local/bin/goup start
# Reload configuration and certificates without a full restart.
ExecReload=/bin/kill -HUP $MAINPID

Restart=on-failure
RestartSec=2

# Bind privileged ports (80/443) without running as root.
AmbientCapabilities=CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
NoNewPrivileges=true

# Filesystem hardening.
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
ReadWritePaths=/etc/goup /var/lib/goup

[Install]
WantedBy=multi-user.target
Loading
Loading