Setting up a tilde
This is a living document, describing my progress on setting up my own tilde/pubnix server. Keep in mind that this is very much a Work-In-Progress(tm).
So, I decided I wanted to host my own pubnix/tilde (I still don’t know what to call it LMAO). Basically, it’s gonna be a server where members can log in via SSH to collaborate on code, experiment with Linux and play around on the CLI.
I described the main idea in my previous post, so go read that if you’re interested.
Creating an account
After I set up the root account of my Debian server and logged in, I added my own account, and gave myself root privileges:
adduser robin
usermod -aG members robin # group that all members will be in
usermod -aG sudo robin
I also turned off root login over SSH in
/etc/ssh/sshd_config
:
PermitRootLogin no
And finally I set the timezone and hostname:
timedatectl set-timezone Europe/Amsterdam
hostnamectl set-hostname dupunkto.org
Welcome messages
I wanted a nice welcome message to be printed when logging in, instead
of the garbage that was printed before. I started out by emptying
/etc/motd
, and then disabling all scripts in
/etc/update-motd.d
.
I added a few new scripts in /etc/update-motd.d
. The
first two digits specify the order that the files get executed in. I
added two files:
sudoedit /etc/update-motd.d/05-welcome
sudo chmod +x /etc/update-motd.d/05-welcome
sudoedit /etc/update-motd.d/10-currently-online
sudo chmod +x /etc/update-motd.d/10-currently-online
#!/bin/sh
echo "Welcome to {du}punkto, a simple invite-only pubnix!"
echo "We hope you enjoy your stay here :)"
echo
#!/bin/sh
echo "These users are currently online:"
echo "you"
users | tr ' ' \\n | uniq
echo
Member directories
I wanted members to have a nice default config when they joined, so I
editted some files in /etc/skel
:
sudoedit /etc/skel/.bashrc
# ~/.bashrc: executed by bash(1) for non-login shells.
# see /usr/share/doc/bash/examples/startup-files (in the package bash-doc)
# for examples
# If not running interactively, don't do anything
case $- in
*i*) ;;
*) return;;
esac
# Don't put duplicate lines or lines starting with space in the history.
# See bash(1) for more options
HISTCONTROL=ignoreboth
# Append to the history file, don't overwrite it
shopt -s histappend
# for setting history length see HISTSIZE and HISTFILESIZE in bash(1)
HISTSIZE=1000
HISTFILESIZE=2000
# Check the window size after each command and, if necessary,
# update the values of LINES and COLUMNS.
shopt -s checkwinsize
# If set, the pattern "**" used in a pathname expansion context will
# match all files and zero or more directories and subdirectories.
#shopt -s globstar
# If the terminal used specifies colors, set a fancy prompt
case "$TERM" in
xterm-color|*-256color) colors=yes;;
esac
# Set variable identifying the chroot you work in (used in the prompt below)
if [ -z "${debian_chroot:-}" ] && [ -r /etc/debian_chroot ]; then
debian_chroot=$(cat /etc/debian_chroot)
fi
if [ "$colors" = yes ]; then
PS1='${debian_chroot:+($debian_chroot)}\[\033[01;32m\]\u@\h\[\033[00m\]:\[>
else
PS1='${debian_chroot:+($debian_chroot)}\u@\h:\w\$ '
fi
# Color support in ls and other utils
if [ "$colors" = yes ]; then
test -r ~/.dircolors && eval "$(dircolors -b ~/.dircolors)" || eval "$(dir>
alias ls='ls --color=auto'
alias dir='dir --color=auto'
alias grep='grep --color=auto'
fi
# Colored GCC warnings and errors
#export GCC_COLORS='error=01;31:warning=01;35:note=01;36:caret=01;32:locus=01:>
alias ll='ls -l'
alias la='ls -A'
alias l='ls -CF'
alias ..="cd .."
alias rm="rm -r" # Allow to remove directories
alias cp="cp -i" # Warn before overwriting existing files
alias mv="mv -i"
# Alias definitions.
# You may want to put all your additions into a separate file like
# ~/.bash_aliases, instead of adding them here directly.
# See /usr/share/doc/bash-doc/examples in the bash-doc package.
if [ -f ~/.bash_aliases ]; then
. ~/.bash_aliases
fi
# Enable programmable completion features (you don't need to enable
# this, if it's already enabled in /etc/bash.bashrc and /etc/profile
# sources /etc/bash.bashrc).
if ! shopt -oq posix; then
if [ -f /usr/share/bash-completion/bash_completion ]; then
. /usr/share/bash-completion/bash_completion
elif [ -f /etc/bash_completion ]; then
. /etc/bash_completion
fi
fi
Setting up Gemini hosting
I wanted members to be able to host Gemini sites, by putting
.gmi
files in the pub/gem
directory.
After a bit of searching I found the
molly-brown
server, which does exactly what I want. One
go install ...
later, I copied it to
/usr/local/bin
.
It is very straight forward to setup. First generate a TLS cert (as root):
openssl req -x509 -out /etc/molly.crt -keyout /etc/molly.key \
-newkey rsa:2048 -nodes -sha256 \
-days 36500 \
-subj '/CN=localhost' -extensions EXT -config <( \
printf "[dn]\nCN=dupunkto.org\n[req]\ndistinguished_name = dn\n[EXT]\nsubjectAltName=DNS:dupunkto.org,DNS:git.dupunkto.org\nkeyUsage=digitalSignature\nextendedKeyUsage=serverAuth")
Then edit the /etc/molly.conf
:
Port = 1965
Hostname = "dupunkto.org"
DocBase = "/var/gemini/"
HomeDocBase = "users"
CertPath = "/etc/molly.cert"
KeyPath = "/etc/molly.key"
DirectoryListing = true
DirectorySort = "Time"
DirectorySubdirsFirst = true
I also created a user for it:
adduser --system --group molly --no-create-home --shell /usr/sbin/nologin --comment "Molly gemini server" molly
sudo chown molly:molly /etc/molly.*
However, I don’t want members to have to edit files in
/var/gemini/users/username
, I want them to have
pub/gem
. Luckily ChatGPT came up with this wonderful
script that symlinks ~/pub/gem
to the corresponding
directory in /var/gemini
:
#!/bin/bash
# Define the base directory for repositories
web_base_dir="/var/gemini/users"
# Loop through all the user directories in the 'members' group
for username in $(getent group members | cut -d: -f4 | tr ',' ' '); do
# Create the repository directory with the user's permissions
web_dir="$web_base_dir/$username"
if [ ! -d "$web_dir" ]; then
mkdir -m 0755 -p "$web_dir"
fi
# Ensure that the directory is owned by the user
chown $username:$username "$web_dir"
# Create a symbolic link to the user's home directory
if [ ! -d "/home/$username/pub" ]; then
mkdir -p "/home/$username/pub"
fi
if [ ! -h "/home/$username/pub/gem" ]; then
ln -s "$web_dir" "/home/$username/pub/gem"
fi
echo "Created directory and symlink for $username."
done
echo "Symlink creation completed."
The only thing left to do, is to setup a systemd unit and start the
server. The molly-brown
repo already provided a service
file, which I gratefully copied:
[Unit]
Description=Molly Brown gemini server
After=network.target
[Service]
Type=simple
Restart=always
User=molly
ExecStart=/usr/local/bin/molly-brown -c /etc/molly.conf
[Install]
WantedBy=multi-user.target
I put it in /etc/systemd/system/molly-brown.service
. Then
simply run:
systemctl enable molly-brown.service --now
(as root), and
kadabing kadaboom. A gemini server.
Webhosting
I also wanted normal webhosting, which would work the same as Gemini
hosting, but uses the pub/web
directory, instead of
pub/gem
. For that I chose nginx
, which I
installed with APT.
sudo apt install nginx
I added this in /etc/nginx/conf.d/01-userdirs.conf
:
server {
server_name dupunkto.org;
listen 80 default_server;
listen [::]:80 default_server;
# SSL configuration
listen 443 ssl default_server;
listen [::]:443 ssl default_server;
root /var/www;
index index.html index.htm;
fancyindex on;
fancyindex_header /header.html;
fancyindex_footer /footer.html;
fancyindex_ignore "header.html" "footer.html";
fancyindex_time_format "%Y-%m-%d";
try_files $uri $uri.html $uri.htm $uri/ =404;
location ~ (README|\.md)$ {
default_type text/plain;
charset utf-8;
}
# Special config for me :)
location /~robin {
fancyindex_header /~robin/header.html;
try_files $uri $uri.html $uri/ =404;
}
}
Again, I used a fancy script to symlink the directories:
#!/bin/bash
# Define the base directory for repositories
web_base_dir="/var/www"
# Loop through all the user directories in the 'members' group
for username in $(getent group members | cut -d: -f4 | tr ',' ' '); do
# Create the repository directory with the user's permissions
web_dir="$web_base_dir/~$username"
if [ ! -d "$web_dir" ]; then
mkdir -m 0755 -p "$web_dir"
fi
# Ensure that the directory is owned by the user
chown $username:$username "$web_dir"
# Create a symbolic link to the user's home directory
if [ ! -d "/home/$username/pub" ]; then
mkdir -p "/home/$username/pub"
fi
if [ ! -h "/home/$username/pub/web" ]; then
ln -s "$web_dir" "/home/$username/pub/web"
fi
echo "Created directory and symlink for $username."
done
echo "Symlink creation completed."
Oh, and to setup SSL (as root):
apt install certbot python3-certbot-nginx
certbot certonly -d dupunkto.org,git.dupunkto.org,matrix.dupunkto.org,irc.dupunkto.org,chat.dupunkto.org
If it says Nginx isn’t properly configured, that’s probably because it expects SSL to be there but it isn’t (yet). If so, just choose the “standalone server” option.
I also added this to my /etc/nginx/nginx.conf
, which I
generated using the
Mozilla SSL Configuration Generator:
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305;
ssl_prefer_server_ciphers off;
ssl_session_timeout 1d;
ssl_session_cache shared:MozSSL:10m; # about 40000 sessions
ssl_session_tickets off;
ssl_certificate /etc/letsencrypt/live/dupunkto.org/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/dupunkto.org/privkey.pem;
ssl_trusted_certificate /etc/letsencrypt/live/dupunkto.org/chain.pem
# OCSP stapling
ssl_stapling on;
ssl_stapling_verify on;
Git hosting
Setting up git hosting vas very straight forward after setting up
Nginx. Just install cgit
and edit the config file in
/etc/cgitrc
:
root-readme=/usr/share/cgit/README
css=/custom.css
logo=/dupunkto.png
virtual-root=/
# Dumb clones
enable-http-clone=1
clone-url= http://$HTTP_HOST/$CGIT_REPO_URL
enable-subject-links=1
enable-tree-linenumbers=1
enable-commit-graph=1
enable-blame=1
enable-log-filecount=1
enable-log-linecount=1
enable-git-config=1
enable-index-owner=0
side-by-side-diffs=1
summary-branches=5
# Enable READMEs
about-filter=/usr/lib/cgit/filters/about-formatting.sh
#source-filter=/usr/lib/cgit/filters/syntax-highlighting.sh
readme=:README.md
# Prevent spam
noplainemail=1
branch-sort=age
repository-sort=age
root-title={du}punkto
root-desc=git hosting for {du}puntko members
robots=noindex, nofollow
scan-path=/var/git/repositories
I stole some CSS from
https://git.oriole.systems/custom.css
and put it in /usr/share/cgit
, along with a logo titled
dupunkto.png
.
@import "cgit.css";
html {
background-color: white;
}
* {
line-height: 1.35em;
}
@media (max-width: 480px) {
html {
font-size: 12px;
}
}
@media (min-width: 480px) {
html {
font-size: 13px;
}
}
@media (min-width: 786px) {
html {
font-size: 14px;
}
}
@media (max-width: 786px) {
div#cgit table#header td.logo {
display: none;
}
}
@media (min-width: 992px) {
html {
font-size: 15px;
}
}
@media (min-width: 1200px) {
html {
font-size: 16px;
}
}
@media (min-width: 1600px) {
html {
font-size: 18px;
}
}
div#cgit {
/*font-family: monospace;*/
font-size: 1em;
}
pre, div#cgit table.diff td, div#cgit table.ssdiff td {
font-size: 1.1em;
font-family: monospace;
}
code {
font-family: inherit;
}
#summary code {
font-family: monospace;
}
div#cgit div.commit-msg {
font-family: monospace;
}
div#cgit a.ls-blob, div#cgit a.ls-dir, div#cgit .ls-mod, div#cgit td.ls-mode, div#cgit td.ls-size {
font-family: monospace;
}
div#cgit table.list td.commitgraph, div#cgit table.list td.logmsg, div#cgit table.list td.logsubject{
font-family: monospace;
}
div#cgit .sha1 {
font-family: monospace;
}
div#cgit table.ssdiff td {
font-size: 14px;
}
.highlight pre {
font-size: inherit;
}
div#cgit .commit-subject, div#cgit div#summary {
max-width: 80ch;
}
div#cgit div.footer, div#cgit div.footer a {
color: #aaa;
}
div#cgit table.list tr:nth-child(even) {
background: inherit;
}
div#cgit table.list tr:hover {
background: inherit;
}
div#cgit table.list tr.nohover-highlight:hover:nth-child(even) {
background: inherit;
}
div#cgit table.commit-info th {
color: #888;
}
div#cgit table.blob td.linenumbers a:target {
color: white;
background-color: black;
}
div#cgit table.list td a.ls-dir {
font-weight: normal;
}
div#cgit table.tabs td.form form, div#cgit table#header td.form {
padding-bottom: 4px;
}
td.logo img {
width: 100%;
border-radius: 15px;
}
table.head, table.foot { width: 100%; }
td.head-rtitle, td.foot-os { text-align: right; }
td.head-vol { text-align: center; }
div.Pp { margin: 1ex 0ex; }
div.Nd, div.Bf, div.Op { display: inline; }
span.Pa, span.Ad { font-style: italic; }
span.Ms { font-weight: bold; }
dl.Bl-diag > dt { font-weight: bold; }
code.Nm, code.Fl, code.Cm, code.Ic, code.In, code.Fd, code.Fn,
code.Cd { font-weight: bold; font-family: inherit; }
table.Nm td:first-child { padding-right: 1ch; }
code.Fl { white-space: nowrap; }
span.RsT { font-style: italic; }
dl.Bl-tag:not(.Bl-compact) dt { margin-top: 1em; }
ul.Bl-bullet:not(.Bl-compact) li { margin-top: 1em; }
div.Bd-indent { margin-left: 4ch; }
table.Bl-column { width: 100%; }
table.foot { margin-top: 1em; }
table.tbl td:not(:last-child) { border-right: solid 1px; }
table.tbl tr:first-child { border-bottom: 2px solid black; }
table.tbl td { padding: 0 1em; }
div.manual-text a.Xr {
color: inherit !important;
}
div.manual-text a.Xr:hover {
text-decoration: none !important;
}
I then created the /var/git
and
/var/git/repositories
directories, and ran an adaptation
of the script used to setup gemini hosting:
#!/bin/bash
# Define the base directory for repositories
repo_base_dir="/var/git/repositories"
# Loop through all the user directories in the 'members' group
for username in $(getent group members | cut -d: -f4 | tr ',' ' '); do
# Create the repository directory with the user's permissions
repo_dir="$repo_base_dir/~$username"
if [ ! -d "$repo_dir" ]; then
mkdir -m 0755 -p "$repo_dir"
fi
# Ensure that the directory is owned by the user
chown $username:$username "$repo_dir"
# Create a symbolic link to the user's home directory
if [ ! -h "/home/$username/git" ]; then
ln -s "$repo_dir" "/home/$username/git"
fi
echo "Created directory and symlink for $username."
done
echo "Symlink creation completed."
You also need a package called fcgiwrap
, just install it
and then run:
systemctl enable fcgiwrap --now
And I added the relevant section to my Nginx config
(/etc/nginx/conf.d/02-git.conf
):
server {
server_name git.dupunkto.org;
listen 80;
listen [::]:80;
# SSL configuration
listen 443 ssl;
listen [::]:443 ssl;
access_log /var/log/nginx/cgit-access.log;
error_log /var/log/nginx/cgit-error.log;
root /usr/share/cgit;
try_files $uri @cgit;
location @cgit {
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME /usr/lib/cgit/cgit.cgi;
fastcgi_pass unix:/run/fcgiwrap.socket;
fastcgi_param PATH_INFO $uri;
fastcgi_param QUERY_STRING $args;
fastcgi_param HTTP_HOST $server_name;
}
}
Notes on Git
For some reason, the repositories I exported from Gitea all contained a file called “description”, which contained a long message telling you to edit it. cgit automatically uses the contents of this file as repo description. If you remove the files, it just shows “[no description]”, which is way cleaner in my opinion.
find . -type f -name "description" -exec rm -r {} \;
Edit: this is normal. When you initialize a bare Git repo, this
file will always be created (at least for me). I decided to delete
it and use the [gitweb]
section in
.config
instead.
Also, for READMEs to work, you have to install these dependencies:
sudo apt install python3-markdown python3-docutils groff
And, last of all, for mirroring your repos to Codeberg, I will post something very soon.
Security
I installed the UFW firewall:
sudo apt install ufw
ufw allow 20/tcp # SSH
ufw allow 80/tcp # HTTP
ufw allow 443/tcp # SSL
ufw allow 8448/tcp # Matrix (federation)
ufw allow 1965/tcp # Gemini
ufw allow 5222/tcp # XMPP
ufw allow 5223/tcp # XMPP SSL
ufw allow 5296/tcp # XMPP server
ufw allow 8010/tcp # XMPP file transfers
ufw enable
I also installed fail2ban
, which automatically bans bots
spamming port 20:
sudo apt install fail2ban
systemctl enable --now fail2ban
Admin permissions
I created the group mod
and gave it write permissions
over /var/www
and /var/gemini
, for
maintaining the website from a non-sudo account.
addgroup mod
usermod -aG mod <username>
chown root:mod <somedirectory>
chmod g+w <somedirectory>
Credits
These articles helped a lot: