Created
July 27, 2026 03:24
-
-
Save ssokolow/87fc077dac29ad396a6ec87062633759 to your computer and use it in GitHub Desktop.
Example of how to tightly sandbox miniserve as a companion to a private Mumble server
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| [Unit] | |
| Description=HTTPS Server to assist mumble-service | |
| After=network.target | |
| Wants=network.target | |
| [Service] | |
| Type=simple | |
| ExecStart=/usr/local/bin/mumble_companion_https.sh | |
| # `miniserve-sys` to avoid confusion with any `cargo install`-ed copy | |
| BindReadOnlyPaths=/usr/local/bin/miniserve-sys /var/www-mumble_companion | |
| CapabilityBoundingSet= | |
| CPUQuota=20% | |
| DevicePolicy=closed | |
| DynamicUser=yes | |
| Environment= | |
| ExecPaths=/usr/local/bin | |
| ExitType=cgroup | |
| IPAddressDeny=link-local multicast | |
| KeyringMode=private | |
| # Store your username-password credentials in /etc/mumble_companion_auth | |
| LoadCredential=auth:/etc/mumble_companion_auth | |
| LoadCredential=fullchain.pem:/etc/letsencrypt/live/YOUR.DOMAIN.NAME/fullchain.pem | |
| LoadCredential=privkey.pem:/etc/letsencrypt/live/YOUR.DOMAIN.NAME/privkey.pem | |
| LockPersonality=yes | |
| MemoryDenyWriteExecute=yes | |
| MemoryMax=512M | |
| Nice=10 | |
| NoNewPrivileges=yes | |
| PassEnvironment= | |
| PrivateDevices=yes | |
| PrivateIPC=yes | |
| ProcSubset=pid | |
| ProtectProc=invisible | |
| PrivateMounts=yes | |
| PrivateTmp=yes | |
| PrivateUsers=yes | |
| ProtectClock=yes | |
| ProtectControlGroups=yes | |
| ProtectHome=yes | |
| ProtectHostname=yes | |
| ProtectKernelLogs=yes | |
| ProtectKernelModules=yes | |
| ProtectKernelTunables=yes | |
| ProtectSystem=strict | |
| ReadOnlyPaths=/usr/local/bin/miniserve-sys | |
| ReadOnlyDirectories=/var/www-mumble_companion | |
| RemoveIPC=yes | |
| RestrictAddressFamilies=AF_INET AF_INET6 | |
| RestrictNamespaces=yes | |
| RestrictRealtime=yes | |
| RestrictSUIDSGID=yes | |
| SocketBindDeny=any | |
| # Set this to the port you want to listen on | |
| SocketBindAllow=tcp:8088 | |
| SystemCallArchitectures=native | |
| SystemCallErrorNumber=EPERM | |
| SystemCallFilter=@system-service | |
| SystemCallFilter=~@chown @privileged @resources @setuid connect | |
| StandardOutput=journal | |
| TemporaryFileSystem=/usr/local/bin:ro | |
| UMask=0077 | |
| # All irrelevant paths not covered by Private* options that the FHS says we | |
| # should be able to rely on existing (systemd will error out if one doesn't) | |
| InaccessiblePaths=/boot /etc /media /mnt /opt /srv /usr/include /usr/share /usr/src | |
| [Install] | |
| WantedBy=multi-user.target |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/sh | |
| # Customize as desired. See miniserve's --help output for details. | |
| # I recommend changing the port to make port-scanning bots work to find it. | |
| # | |
| # See miniserve's README for how to generate the file for --auth-file | |
| # | |
| # This set of headers helps to limit the risk if some HTML with shady stuff in | |
| # it makes its way into your server, and tries to minimize the odds that random | |
| # bots wandering the web become aware of your server. | |
| # | |
| # While the spec calls for shared caching to be disabled as a side-effect of | |
| # --auth-file, there are tons of broken proxies and caches out there, so the | |
| # pile of Cache-Control directives, combined with Pragma: no-cache are | |
| # intended to hedge against that without going the nuclear option of disabling | |
| # the user's local cache with `no-store`. Feel free to remove the | |
| # `no-transform` directive if you're OK with your users potentially having | |
| # files silently recompressed by something like a mobile phone ISP proxy. | |
| # | |
| # The Permissions-Policy header denies everything that had a defined rule as of | |
| # this writing, so, if you're trying to experiment with making some kind of | |
| # fancy web app and something doesn't work, it's probably that. | |
| # | |
| # Note that you probably want to test without the Strict-Transport-Security | |
| # header first since, if you get things wrong, you have to ask your friends to | |
| # follow a guide to get their browser to forget they saw it. (It tells the | |
| # browser that your site should only ever be loaded over https:// so that, if | |
| # they connect over shady coffee-shop WiFi, a "downgrade attack" will fail.) | |
| # | |
| # (Also, be aware that it imposes an un-bypassable block on self-signed certs) | |
| exec /usr/local/bin/miniserve-sys --verbose --port=8088 \ | |
| --index=index.html --pretty-urls --title=Resources --dirs-first \ | |
| --hide-version-footer --hide-theme-selector \ | |
| --auth-file="$CREDENTIALS_DIRECTORY/auth" \ | |
| --tls-cert="$CREDENTIALS_DIRECTORY/fullchain.pem" \ | |
| --tls-key="$CREDENTIALS_DIRECTORY/privkey.pem" \ | |
| --header="Strict-Transport-Security: max-age=31536000" \ | |
| --header='Referrer-Policy:no-referrer' \ | |
| --header='Cache-Control:private, no-cache, no-transform, max-age=0, must-revalidate, must-understand, proxy-revalidate' \ | |
| --header='Pragma:no-cache' \ | |
| --header='X-DNS-Prefetch-Control:off' \ | |
| --header='X-Content-Type-Options:nosniff' \ | |
| --header='X-Frame-Options:DENY' \ | |
| --header="Content-Security-Policy:default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; frame-ancestors 'self';" \ | |
| --header='Cross-Origin-Embedder-Policy:require-corp' \ | |
| --header='Cross-Origin-Opener-Policy:same-origin' \ | |
| --header='Cross-Origin-Resource-Policy:same-origin' \ | |
| --header 'X-Permitted-Cross-Domain-Policies:none' \ | |
| --header='Origin-Agent-Cluster:?1' \ | |
| --header='X-Robots-Tag:noarchive, nofollow, noimageindex, noindex, nopagereadaloud, nositelinkssearchbox, nosnippet, notranslate, noyaca' \ | |
| --header='Permissions-Policy:accelerometer=(), ambient-light-sensor=(), aria-notify=(), attribution-reporting=(), autoplay=(), browsing-topics=(), captured-surface-control=(), ch-ua-high-entropy-values=(), compute-pressure=(), bluetooth=(), camera=(), cross-origin-isolated=(), deferred-fetch=(), deferred-fetch-minimal=(), display-capture=(), encrypted-media=(), fullscreen=(), gamepad=(), geolocation=(), gyroscope=(), hid=(), identity-credentials-get=(), idle-detection=(), language-detector=(), local-fonts=(), local-network=(), local-network-access=(), loopback-network=(), magnetometer=(), microphone=(), midi=(), otp-credentials=(), on-device-speech-recognition=(), payment=(), picture-in-picture=(), private-state-token-issuance=(), private-state-token-redemption=(), publickey-credentials-create=(), publickey-credentials-get=(), screen-wake-lock=(), serial=(), speaker-selection=(), storage-access=(), translator=(), summarizer=(), usb=(), web-share=(), window-management=(), xr-spatial-tracking=()' \ | |
| /var/www-mumble_companion/http-root |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment