From 6c22a6e48e8ff49a69434eca7a7b78158576cb7b Mon Sep 17 00:00:00 2001 From: Thomas Vanbesien Date: Wed, 11 Feb 2026 21:00:52 +0100 Subject: Initial import --- .gitignore | 2 + build.command | 5 ++ compose.yaml | 72 +++++++++++++++++++++ create_radicale_user.command | 4 ++ example.env | 10 +++ generate_self_signed_cert.bash | 7 +++ readme.md | 39 ++++++++++++ services/cgit/Dockerfile | 40 ++++++++++++ services/cgit/cgit.conf | 1 + services/cgit/httpd.conf | 63 +++++++++++++++++++ services/cgit/readme.md | 73 ++++++++++++++++++++++ services/nginx/Dockerfile | 3 + .../fs/etc/nginx/templates/default.conf.template | 45 +++++++++++++ .../nginx/templates/services/cgit.conf.template | 17 +++++ .../templates/services/radicale.conf.template | 19 ++++++ services/nginx/fs/sbin/cmd.bash | 11 ++++ services/radicale/Dockerfile | 3 + services/radicale/fs/etc/radicale/conf.ini | 14 +++++ services/radicale/fs/sbin/cmd.sh | 33 ++++++++++ 19 files changed, 461 insertions(+) create mode 100644 .gitignore create mode 100755 build.command create mode 100644 compose.yaml create mode 100755 create_radicale_user.command create mode 100644 example.env create mode 100755 generate_self_signed_cert.bash create mode 100644 readme.md create mode 100644 services/cgit/Dockerfile create mode 100644 services/cgit/cgit.conf create mode 100644 services/cgit/httpd.conf create mode 100644 services/cgit/readme.md create mode 100644 services/nginx/Dockerfile create mode 100644 services/nginx/fs/etc/nginx/templates/default.conf.template create mode 100644 services/nginx/fs/etc/nginx/templates/services/cgit.conf.template create mode 100644 services/nginx/fs/etc/nginx/templates/services/radicale.conf.template create mode 100755 services/nginx/fs/sbin/cmd.bash create mode 100644 services/radicale/Dockerfile create mode 100644 services/radicale/fs/etc/radicale/conf.ini create mode 100755 services/radicale/fs/sbin/cmd.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..41a9d3b --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/.env +**/fs.tar.gz diff --git a/build.command b/build.command new file mode 100755 index 0000000..d307f88 --- /dev/null +++ b/build.command @@ -0,0 +1,5 @@ +#!/usr/bin/bash + +for srv in nginx radicale; do + tar -czf services/"$srv"/fs.tar.gz -C services/"$srv"/fs . +done diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..9b148eb --- /dev/null +++ b/compose.yaml @@ -0,0 +1,72 @@ +name: net_services + +services: + nginx: + image: nginx:${COMPOSE_PROJECT_NAME} + build: + context: services/nginx + environment: + - NGINX__HOST=${NGINX__HOST} + networks: + - cgit + - radicale + ports: + - ${HOST__HTTP_PORT:?}:80 + - ${HOST__HTTPS_PORT:?}:443 + tmpfs: + - /run/secrets:mode=400 + volumes: + - ${HOST__CERT_DIR:?}:/run/host_secrets:ro + depends_on: + - cgit + - radicale + + cgit: + image: cgit:${COMPOSE_PROJECT_NAME} + build: + context: services/cgit + networks: + - cgit + volumes: + - ${HOST__CGITRC_DIR:?}:/etc/cgit:ro + - ${HOST__CGIT_FILTER_DIR:?}:/usr/local/lib/cgit/filters/commit + - ${HOST__CGIT_ABOUT_DIR:?}:/srv/cgit:ro + - ${HOST__GIT_REPO_DIR:?}:/srv/git:ro + + radicale: + image: tomsquest/docker-radicale:tvcloud + build: + context: services/radicale + environment: + - TAKE_FILE_OWNERSHIP=false + init: true + read_only: true + security_opt: + - no-new-privileges:true + cap_drop: + - ALL + cap_add: + - SETUID + - SETGID + - CHOWN + - KILL + deploy: + resources: + limits: + memory: 256M + pids: 50 + healthcheck: + test: curl -f http://127.0.0.1:5232 || exit 1 + start_period: 5s + networks: + - radicale + volumes: + - ${HOST__RADICALE_USERS_DIR:?}:/etc/radicale/users:ro + - radicale_data:/data + +networks: + cgit: + radicale: + +volumes: + radicale_data: diff --git a/create_radicale_user.command b/create_radicale_user.command new file mode 100755 index 0000000..0b072e6 --- /dev/null +++ b/create_radicale_user.command @@ -0,0 +1,4 @@ +#!/usr/bin/bash + +username=${1:?missing argument username} +htpasswd -nBC 12 "$username" diff --git a/example.env b/example.env new file mode 100644 index 0000000..6bf613f --- /dev/null +++ b/example.env @@ -0,0 +1,10 @@ +HOST__HTTP_PORT=80 +HOST__HTTPS_PORT=443 +HOST__CERT_DIR=/home/USER/.local/net_services/certs +HOST__GIT_REPO_DIR=/home/USER/.local/net_services/git +HOST__CGITRC_DIR=/home/USER/.local/net_services/cgit/cgitrc +HOST__CGIT_FILTER_DIR=/home/USER/.local/net_services/cgit/filter +HOST__CGIT_ABOUT_DIR=/home/USER/.local/net_services/cgit/about +HOST__RADICALE_USERS_DIR=/home/USER/.local/net_services/radicale + +NGINX__HOST=localhost diff --git a/generate_self_signed_cert.bash b/generate_self_signed_cert.bash new file mode 100755 index 0000000..379ea13 --- /dev/null +++ b/generate_self_signed_cert.bash @@ -0,0 +1,7 @@ +#!/usr/bin/bash + +host=${1:?missing host argument} +subdomains=(www git dav) + +mkcert -install +mkcert "${subdomains[@]/%/.$host}" "$host" diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..0e199b6 --- /dev/null +++ b/readme.md @@ -0,0 +1,39 @@ +# net_services + +This is personal project about services on the network I provide for myself. + +I want to handle as much of my data myself. For privacy and for the challenge. I also want to serve apps for my friends. + +I set this up on a VPS with a static IP. I also bought a domain name `tvcloud.fr` to point to the VPS. Some services run on the VPS itself. Some others are run with Docker. Nginx is used as endpoint. + +## How-to + +1. Create a `.env`. See `example.env`. + +1. Build and run the services. + + ``` + ./build.command + docker compose up + ``` + +## Handling data + +Data of various types has to be handled in different ways. + +* **Passwords**: A KeePassXC database shared with Syncthing. + +* **Git repositories**: A remote server accesible over SSH for push. And also a web front-end (cgit). + +* **Calendars, to-dos, journals, and contacts**: A Radicale server. + I could just synchronize the `.ics`/`.vcf` files, but a CalDAV/CarDAV server is compatible with mobile applications. + +* **Remote storage**: SFTP for large files. Syncthing for moderately large data that is better synchronized than downloaded manually. + +## Security + +TODO (sensitive data in tmpfs) + +### Firewalls + +TODO (OVH, iptables, docker+iptables+reboot bug) diff --git a/services/cgit/Dockerfile b/services/cgit/Dockerfile new file mode 100644 index 0000000..4c23eb2 --- /dev/null +++ b/services/cgit/Dockerfile @@ -0,0 +1,40 @@ +FROM debian:13.3-slim AS build + +ARG CGIT_COMMIT=09d24d7cd0b7e85633f2f43808b12871bb209d69 + +# Install build dependencies +RUN apt-get update \ + && apt-get install --assume-yes --no-install-recommends \ + make gcc pkg-config curl xz-utils ca-certificates libzip-dev libssl-dev liblua5.2-dev \ + && rm -rf /var/lib/apt/lists/* + +# Build cgit +ADD --unpack=true https://git.zx2c4.com/cgit/snapshot/cgit-${CGIT_COMMIT}.tar.xz /usr/src +WORKDIR /usr/src/cgit-${CGIT_COMMIT} +COPY cgit.conf . +RUN make get-git && make LUA_PKGCONFIG=lua5.2 && make install && rm -rf $(pwd) + +FROM httpd:2.4.66 AS final + +ARG UID=1000 GID=1000 + +# Create cgit user (used by Apache) +RUN groupadd --gid ${GID} cgit && useradd --uid ${UID} --groups cgit --no-user-group cgit + +# Copy cgit built in previous stage +COPY --from=build /var/www/htdocs/cgit /var/www/htdocs +COPY --from=build /usr/local/lib/cgit/filters /usr/local/lib/cgit/filters +RUN mkdir /var/cache/cgit && chown cgit:cgit /var/cache/cgit + +# Install runtime dependencies +RUN apt-get update \ + && apt-get install --assume-yes --no-install-recommends \ + python3 python3-pygments python3-markdown \ + && rm -rf /var/lib/apt/lists/* + +# HTTP server configuration +COPY httpd.conf /usr/local/apache2/conf/ + +WORKDIR /var/www/htdocs +EXPOSE 80 +VOLUME /srv/git /etc/cgit /usr/local/lib/cgit/filters/commit /srv/cgit diff --git a/services/cgit/cgit.conf b/services/cgit/cgit.conf new file mode 100644 index 0000000..446a846 --- /dev/null +++ b/services/cgit/cgit.conf @@ -0,0 +1 @@ +CGIT_CONFIG = /etc/cgit/cgitrc diff --git a/services/cgit/httpd.conf b/services/cgit/httpd.conf new file mode 100644 index 0000000..451603c --- /dev/null +++ b/services/cgit/httpd.conf @@ -0,0 +1,63 @@ +# +# Apache HTTP server configuration +# + +LoadModule rewrite_module modules/mod_rewrite.so +LoadModule mpm_event_module modules/mod_mpm_event.so +LoadModule authn_file_module modules/mod_authn_file.so +LoadModule authn_core_module modules/mod_authn_core.so +LoadModule authz_host_module modules/mod_authz_host.so +LoadModule authz_groupfile_module modules/mod_authz_groupfile.so +LoadModule authz_user_module modules/mod_authz_user.so +LoadModule authz_core_module modules/mod_authz_core.so +LoadModule access_compat_module modules/mod_access_compat.so +LoadModule auth_basic_module modules/mod_auth_basic.so +LoadModule reqtimeout_module modules/mod_reqtimeout.so +LoadModule filter_module modules/mod_filter.so +LoadModule mime_module modules/mod_mime.so +LoadModule log_config_module modules/mod_log_config.so +LoadModule env_module modules/mod_env.so +LoadModule headers_module modules/mod_headers.so +LoadModule setenvif_module modules/mod_setenvif.so +LoadModule version_module modules/mod_version.so +LoadModule unixd_module modules/mod_unixd.so +LoadModule status_module modules/mod_status.so +LoadModule autoindex_module modules/mod_autoindex.so + + LoadModule cgid_module modules/mod_cgid.so + + + LoadModule cgi_module modules/mod_cgi.so + +LoadModule dir_module modules/mod_dir.so +LoadModule alias_module modules/mod_alias.so + +ServerName localhost +ServerRoot "/usr/local/apache2" +Listen 80 +User cgit +Group cgit + +DocumentRoot "/var/www/htdocs" + + Options +ExecCGI + AddHandler cgi-script .cgi + RewriteEngine on + # Serve regular files + RewriteCond %{REQUEST_FILENAME} -f + RewriteRule ^ - [L] + # URLs not starting with "cgit.cgi" are internally prefixed with it + RewriteRule "^(?!cgit\.cgi)(.*)" "/cgit.cgi/$1" [L] + + + + Require all denied + + +ErrorLog /proc/self/fd/2 +LogLevel warn +# Uncomment to see rewrite module trace +# LogLevel info rewrite_module:trace1 +LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined +LogFormat "%h %l %u %t \"%r\" %>s %b" common +CustomLog /proc/self/fd/1 common diff --git a/services/cgit/readme.md b/services/cgit/readme.md new file mode 100644 index 0000000..bef4b9b --- /dev/null +++ b/services/cgit/readme.md @@ -0,0 +1,73 @@ +# cgit + +This project is a [cgit](https://git.zx2c4.com/cgit/about/) docker image. + +It aims to be as simple as possible. No authentication, no SSH, just browsing repositories on a web page. The container doesn't write to the repositories so they can be read-only. + +# Build + +``` +docker build --tag cgit . +``` + +# Run + +## cgit configuration and runtime + +Examples are provided in the `examples` directory. + +* `CGITRC`: Host directory containing a `cgitrc` configuration, see [cgitrc manual](https://manpages.debian.org/trixie/cgit/cgitrc.5.en.html). +* `COMMIT_FILTER`: Host directory containing an executable `commit-filter.sh` script to format Git commit messages. See the `commit-filter` section of the [cgitrc manual](https://manpages.debian.org/trixie/cgit/cgitrc.5.en.html). +* `ABOUT`: Host directory containing `about.md` for the front page "about" section. +* `REPOSITORIES`: Host directory containing your Git repositories. + +## Run with `docker` + +``` +docker run \ + --rm \ + --name cgit \ + --publish 8080:80 \ + --mount type=bind,src=CGITRC,dst=/etc/cgit,ro \ + --mount type=bind,src=COMMIT_FILTER,dst=/usr/local/lib/cgit/filters/commit \ + --mount type=bind,src=ABOUT,dst=/srv/cgit,ro \ + --mount type=bind,src=REPOSITORIES,dst=/srv/git,ro \ + cgit +``` + +Browse the website [here](http://localhost:8080). + +## Run with `docker compose` + +*Example `compose.yaml`:* + +``` +services: + cgit: + build: . + image: cgit + container_name: cgit + ports: + - 8080:80 + volumes: + - CGITRC:/etc/cgit:ro + - COMMIT_FILTER:/usr/local/lib/cgit/filters/commit + - ABOUT:/srv/cgit:ro + - REPOSITORIES:/srv/git:ro +``` + +Browse the website [here](http://localhost:8080). + +# Configuration + +## Repository specific `cgitrc` + +Add a `cgitrc` file at the root of a repository to configure it for cgit. Note that this only works with the `scan-path` setting. + +*Example `cgitrc`:* + +``` +desc=Repository description +owner=Repository owner +section=Repository section +``` diff --git a/services/nginx/Dockerfile b/services/nginx/Dockerfile new file mode 100644 index 0000000..bb8e645 --- /dev/null +++ b/services/nginx/Dockerfile @@ -0,0 +1,3 @@ +FROM nginx:1.29.4-trixie +ADD fs.tar.gz / +CMD ["/sbin/cmd.bash"] diff --git a/services/nginx/fs/etc/nginx/templates/default.conf.template b/services/nginx/fs/etc/nginx/templates/default.conf.template new file mode 100644 index 0000000..306a074 --- /dev/null +++ b/services/nginx/fs/etc/nginx/templates/default.conf.template @@ -0,0 +1,45 @@ +server { + listen 80; + listen [::]:80; + + server_name ${NGINX__HOST} + www.${NGINX__HOST} + dav.${NGINX__HOST} + git.${NGINX__HOST}; + + # Prevent nginx HTTP Server Detection + server_tokens off; + + return 301 https://$host$request_uri; +} + +server { + listen 443 ssl; + listen [::]:443 ssl; + + server_name ${NGINX__HOST} www.${NGINX__HOST}; + + ssl_certificate /run/secrets/server.crt; + ssl_certificate_key /run/secrets/server.key; + + location / { + root /srv; + } +} + +server { + listen 443 ssl default_server; + listen [::]:443 ssl default_server; + + server_name _; + + ssl_certificate /run/secrets/server.crt; + ssl_certificate_key /run/secrets/server.key; + + return 444; +} + +# Docker embedded DNS server +resolver 127.0.0.11 valid=2s; + +include /etc/nginx/conf.d/services/*.conf; diff --git a/services/nginx/fs/etc/nginx/templates/services/cgit.conf.template b/services/nginx/fs/etc/nginx/templates/services/cgit.conf.template new file mode 100644 index 0000000..c0fa070 --- /dev/null +++ b/services/nginx/fs/etc/nginx/templates/services/cgit.conf.template @@ -0,0 +1,17 @@ +server { + listen 443 ssl; + listen [::]:443 ssl; + + server_name git.${NGINX__HOST}; + + ssl_certificate /run/secrets/server.crt; + ssl_certificate_key /run/secrets/server.key; + + location / { + proxy_pass http://cgit:80; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} diff --git a/services/nginx/fs/etc/nginx/templates/services/radicale.conf.template b/services/nginx/fs/etc/nginx/templates/services/radicale.conf.template new file mode 100644 index 0000000..d6e4617 --- /dev/null +++ b/services/nginx/fs/etc/nginx/templates/services/radicale.conf.template @@ -0,0 +1,19 @@ +server { + listen 443 ssl; + listen [::]:443 ssl; + + server_name dav.${NGINX__HOST}; + + ssl_certificate /run/secrets/server.crt; + ssl_certificate_key /run/secrets/server.key; + + location / { + proxy_pass http://radicale:5232; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Port $server_port; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Host $http_host; + proxy_pass_header Authorization; + } +} diff --git a/services/nginx/fs/sbin/cmd.bash b/services/nginx/fs/sbin/cmd.bash new file mode 100755 index 0000000..e024b4f --- /dev/null +++ b/services/nginx/fs/sbin/cmd.bash @@ -0,0 +1,11 @@ +#!/usr/bin/bash +set -eu + +# Install sensitive data in tmpfs +install --mode 400 /run/host_secrets/server.crt /run/secrets/server.crt +install --mode 400 /run/host_secrets/server.key /run/secrets/server.key + +# We have to run the entrypoint again +# Because if the first positional parameter is not "nginx" or "nginx-debug" the scripts in /docker-entrypoint.d are not ran. +# https://github.com/nginx/docker-nginx/blob/master/stable/debian/docker-entrypoint.sh +exec /docker-entrypoint.sh nginx -g "daemon off;" diff --git a/services/radicale/Dockerfile b/services/radicale/Dockerfile new file mode 100644 index 0000000..d6e850b --- /dev/null +++ b/services/radicale/Dockerfile @@ -0,0 +1,3 @@ +FROM tomsquest/docker-radicale:3.5.10.0 +ADD fs.tar.gz / +CMD su-exec radicale /sbin/cmd.sh diff --git a/services/radicale/fs/etc/radicale/conf.ini b/services/radicale/fs/etc/radicale/conf.ini new file mode 100644 index 0000000..2af4af9 --- /dev/null +++ b/services/radicale/fs/etc/radicale/conf.ini @@ -0,0 +1,14 @@ +[server] + +hosts = localhost:5232, radicale:5232 + +[auth] + +type = htpasswd +htpasswd_filename = /etc/radicale/users/.htpasswd +htpasswd_encryption = bcrypt + +[storage] + +filesystem_folder = /data/collections +hook = git add -A && (git diff --cached --quiet || git commit -m "Changes by \"%(user)s\"") diff --git a/services/radicale/fs/sbin/cmd.sh b/services/radicale/fs/sbin/cmd.sh new file mode 100755 index 0000000..4d09e75 --- /dev/null +++ b/services/radicale/fs/sbin/cmd.sh @@ -0,0 +1,33 @@ +#!/bin/sh +set -eu + +conf=/etc/radicale/conf.ini + +if [ ! -d /data/collections/.git ]; then + # Initialize git repository (for storage) + echo "Starting server..." + /venv/bin/radicale --config "$conf" --logging-level error & + radicale_pid=$! + echo "Waiting for server to start..." + until curl -sf http://127.0.0.1:5232; do sleep 1; done + echo "Server started" + + cd /data/collections + git init --initial-branch=radicale + git config user.name radicale + git config user.email radicale@domain.tld + cat <.gitignore +.Radicale.cache +.Radicale.lock +.Radicale.tmp-* +EOF + git add -A && (git diff --cached --quiet || git commit -m "Initialization commit") + + echo "Restarting server..." + kill "$radicale_pid" + wait "$radicale_pid" +else + echo "Initialization skipped" +fi + +exec /venv/bin/radicale --config "$conf" -- cgit v1.2.3