• Autore:   Elia Argentieri
  • Ultima modifica:   28 nov 2022 23:08
  • Redazione:   6 dic 2020 19:16
  • Sorgente di questa pagina
  • Tempo di lettura:   10 minuti

Per installare un server Seafile, seguendo il manuale ufficiale, bisogna seguire una strana procedura non standard, un po’ complicata, specialmente se si desidera compilare il software, piuttosto che installare il pacchetto precompilato.

In questo articolo sistemo la faccenda facendo un po’ di ordine e usando alcuni pacchetti AUR. Assumo quindi che stiate usando una distribuzione basata su Arch Linux. Per altre distribuzioni bisognerà adattare alcuni passaggi. Resta comunque il fatto che questa guida, in realtà, si basa sul lavoro svolto da uno sviluppatore Debian che ha tentato di pacchettizzare Seafile server.

NB: Vi informo subito che con questa configurazione ci saranno dei problemi per quanto riguarda gli avatar, funzione a mio avviso non fondamentale. Tenterò comunque di sistemare anche questo problema, prima o poi.

È passato un bel po’ di tempo da quando vi raccontavo della [migrazione da un server Debian 9 ad un server Manjaro Stable]. Quella volta, così come le altre, ho dovuto re-installare da capo tutti i servizi richiesti e ri-configurarli copiando file di configurazione dai backup. Una procedura manuale che porta inevitabilmente ad errori e vuoti di memoria che fanno sprecare molto tempo. La prossima volta cercherò un sistema che mi consenta di replicare il server su qualsiasi macchina. Ho già messo gli occhi su Fedora CoreOS e NixOS (due sistemi molto che ottengono più o meno gli stessi risultati in 2 modi completamente diversi).

Tornando a Seafile, il modo più rapido per installarlo, sarebbe stato seguire il relativo articolo della Arch Linux Wiki. Purtroppo però l’articolo non è aggiornato e c’è bisogno di rivederlo.

Per questi motivi ho deciso di cercare un’altra strada, e dopo un bel po’ di ricerche e ore a brancolare nel buio sono arrivato ad una soluzione funzionante al 99% e, a mio avviso, molto più pulita. Per arrivarci ho cercato di capire che cosa è stato fatto in questi 3 repository Debian di seafile, seahub e ccnet.

Iniziamo con lo stabilire che Seafile è un software ad architettura modulare. Ci sono almeno 3 moduli: ccnet, che si occupa dell’autenticazione degli utenti; seahub, l’interfaccia web il cui backend è scritto in python usando django; seafile-server, la parte che si occupa della gestione del file system e della sincronizzazione dei file. A questi si aggiunge un modulo opzionale, ovvero seafdav che fornisce l’accesso WebDAV alle librerie. Oltre a questi componenti è anche buona pratica aggiungere un server web con funzione di proxy inverso, come NGINX.

Adesso passiamo all’installazione pratica.

Installazione

Per prima cosa installiamo i pacchetti ccnet-server, seafile-server, seahub e python-wsgidav-seafile (quest’ultimo solo se si è interessati a Web-DAV) dall’AUR. Con yay è sufficiente un yay -S ccnet-server seafile-server seahub python-wsgidav-seafile. Questo comando installa le dipendenze, scarica i sorgenti, li ripulisce, patcha, compila, pacchettizza e infine installa, tutto in automatico. Niente male, vero?

Configurazione

Una volta installati i pacchetti passiamo alla configurazione. Per prima cosa dobbiamo creare un file contenente alcune variabili d’ambiente necessarie per fornire una struttura il più conforme possibile al FHS agli eseguibili di Seafile (che già sono stati installati nelle apposite directory grazie ai precedenti pacchetti AUR).

Creiamo la cartella seafile sotto /etc, lanciando sudo mkdir /etc/seafile.

A questo punto vanno creati una serie di file dentro questa cartella:

/etc/seafile/env

CCNET_CONF_DIR=/var/lib/seafile/ccnet
SEAFILE_CENTRAL_CONF_DIR=/etc/seafile
SEAFILE_CONF_DIR=/var/lib/seafile/storage
SEAFILE_VAR_DIR=/var/lib/seafile
SEAHUB_LOG_DIR=/var/log/seafile
SEAHUB_CACHE_DIR=/var/lib/seafile
SEAHUB_RODATA_DIR=/usr/share/seafile-server/seahub
SEAHUB_CONF_DIR=/etc/seafile
SEAFDAV_CONF=/etc/seafile/seafdav.conf
PYTHONPATH=/usr/share/seafile-server/seahub:/usr/share/seafile-server/seahub/thirdpart:/etc/seafile

In questo modo possiamo tenere ben separati i file di configurazione dai dati variabili, rispettando il FHS. Per qualche strano motivo e contro intuitivamente, la variabile SEAFILE_CONF_DIR indica la posizione su cui salvare i dati sincronizzati. Ciò non stupisce molto perché la maggior parte di queste variabili non sono per niente documentate e sono sparse nel codice sorgente dei vari moduli di Seafile.

Un altra cosa da dire riguarda la variabile SEAHUB_LOG_DIR che punta alla directory dove vengono tenuti i file di log di seafile. Tuttavia in un server gestito con systemd, preferisco piuttosto che i log vengano mantenuti da journald. Dopo molte ricerche ho scoperto come ottenere questo comportamento.

/etc/seafile/seahub_settings.py

Purtroppo questo file deve essere scrivibile dall’utente che esegue seahub e quindi va cambiato il proprietario con sudo chown seafile /etc/seafile/seahub_settings.py.

DEBUG = False
SECRET_KEY = " usa qualcosa come openssl per generare un segreto (FIXME: non ricordo come l'ho generato) "
FILE_SERVER_ROOT = 'https://cloud.elinvention.ovh/seafhttp'
TIME_ZONE = 'Europe/Rome'

DATABASES = {
    'default': {
        # 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'.
        'ENGINE': 'django.db.backends.sqlite3',

        # db name or path for sqlite3
        'NAME': '/var/lib/seafile/seahub/seahub.db',

        # not used with sqlite3
        'USER': '',
        'PASSWORD': '',
        'HOST': '',
        'PORT': '',
    }
}

PROJECT_ROOT = '/var/lib/seafile/seahub'
MEDIA_ROOT = PROJECT_ROOT + '/media'
THUMBNAIL_ROOT = MEDIA_ROOT + "/thumbnails"
#SEAHUB_DATA_ROOT = PROJECT_ROOT + '/media'
AVATAR_STORAGE_DIR = MEDIA_ROOT + "/avatars"
GROUP_AVATAR_STORAGE_DIR = AVATAR_STORAGE_DIR + "/groups"

STATIC_ROOT = '/usr/share/seafile-server/seahub/media/assets/'
STATICFILES_DIRS = (
    '/usr/share/seafile-server/seahub/static',
    '/usr/share/seafile-server/seahub/frontend/build',
)
STATICI18N_ROOT = '/usr/share/seafile-server/seahub/static/scripts'


LOGGING = {
    'version': 1,
    'disable_existing_loggers': True,
    'formatters': {
        'verbose': {
            'format': '%(process)-5d %(thread)d %(name)-50s %(levelname)-8s %(message)s'
        },
        'standard': {
            'format': '%(asctime)s [%(levelname)s] %(name)s:%(lineno)s %(funcName)s %(message)s'
        },
        'simple': {
            'format': '[%(asctime)s] %(name)s %(levelname)s %(message)s',
            'datefmt': '%d/%b/%Y %H:%M:%S'
        },
    },
    'filters': {
        'require_debug_false': {
            '()': 'django.utils.log.RequireDebugFalse',
        },
        'require_debug_true': {
            '()': 'django.utils.log.RequireDebugTrue',
        },
    },
    'handlers': {
        'console': {
            'level': 'DEBUG',
            'filters': ['require_debug_true'],
            'class': 'logging.StreamHandler',
            'formatter': 'simple'
        },
        'syslog': {
            'level': 'DEBUG',
            'class': 'logging.handlers.SysLogHandler',
            'address': '/dev/log',
            'formatter': 'standard'
        },
    },
    'loggers': {
        # root logger
        '': {
            'handlers': ['console'],
            'level': 'INFO',
            'disabled': False
        },
        'django.request': {
            'handlers': ['console'],
            'level': 'DEBUG',
            'propagate': False,
        },
    },
}

La variabile LOGGING è l’enorme configurazione necessaria a far stampare i log su standard output che viene poi catturato da journald…

/etc/seafile/ccnet.conf:

[General]
SERVICE_URL = https://cloud.elinvention.ovh

/etc/seafile/seafile.conf:

[general]
enable_syslog = false

[fileserver]
port=8082
host=127.0.0.1

/etc/seafile/seafdav.conf

Opzionale, solo se si desidera l’accesso WebDAV alle librerie di seafile.

[WEBDAV]
enabled = true
port = 6001
fastcgi = false
share_name = /seafdav

Configuriamo i servizi con systemd

A questo punto abbiamo tutte le configurazioni necessarie (fiuh) e possiamo passare a configurare i 3+1 servizi con systemd.

Ho configurato i servizi in modo da confinarli il più possibile e migliorarne il punteggio di sicurezza calcolato da sudo systemd-analyze security:

seafile-ccnet-server.service              2.1 OK        🙂
seafile-seafdav.service                   2.7 OK        🙂
seafile-seahub-uwsgi.service              2.5 OK        🙂
seafile-server.service                    2.7 OK        🙂

In realtà si può fare anche di meglio, ma per il momento penso di avervi lasciato con una base di partenza molto solida.

seafile-server.service

Partiamo con sudo systemctl edit --full --force seafile-server.service per creare un nuovo servizio. Inseriamo questa configurazione:

[Unit]
Description=Seafile fileserver daemon
ConditionPathExists=/etc/seafile/ccnet.conf
Requires=seafile-ccnet-server.service
PartOf=seafile.target

[Service]
Type=simple
User=seafile
Group=seafile
EnvironmentFile=/etc/seafile/env
ExecStart=/usr/bin/seaf-server -f \
    -F ${SEAFILE_CENTRAL_CONF_DIR} -c ${CCNET_CONF_DIR} \
    -d ${SEAFILE_CONF_DIR} \
    -l ${SEAHUB_LOG_DIR}/seafile.log
# seafile-server fails when ccnet-server is restarted
Restart=always
# sleep 2, workaround ccnet-server not saying when ready
RestartSec=2

# Hardening
ReadWritePaths=/var/log/seafile /var/lib/seafile
CapabilityBoundingSet=
NoNewPrivileges=True
SecureBits=noroot-locked
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
PrivateDevices=true
PrivateUsers=true
ProtectHostname=true
ProtectClock=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectKernelLogs=true
ProtectControlGroups=true
LockPersonality=true
MemoryDenyWriteExecute=true
RestrictRealtime=true
RestrictSUIDSGID=true
SystemCallArchitectures=native
SystemCallFilter=@system-service

[Install]
WantedBy=multi-user.target

Tutta la parte sotto hardening non è strettamente necessaria, ma impone diverse restrizioni su ciò che può fare il servizio. Nel malaugurato caso in cui qualcuno scopra una vulnerabilità nel codice di Seafile e decida di sfruttarla contro il nostro server, troverà un sistema ben difeso e i danni saranno sperabilmente limitati.

seafile-ccnet.service

sudo systemctl edit --full --force seafile-ccnet.service:

[Unit]
Description=Seafile RPC server daemon ccnet
ConditionPathExists=/etc/seafile/ccnet.conf
ConditionFileIsExecutable=/usr/bin/ccnet-server
PartOf=seafile.target

[Service]
Type=simple
User=seafile
Group=seafile
EnvironmentFile=/etc/seafile/env
ExecStart=/usr/bin/ccnet-server \
    -F ${SEAFILE_CENTRAL_CONF_DIR} -c ${CCNET_CONF_DIR} \
    -f ${SEAHUB_LOG_DIR}/ccnet.log
SuccessExitStatus=1 SIGKILL
ExecStopPost=/bin/rm -f ${CCNET_CONF_DIR}/ccnet.sock

# Hardening
ReadWritePaths=/var/log/seafile /var/lib/seafile/ccnet
AmbientCapabilities=CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
NoNewPrivileges=True
SecureBits=noroot-locked
RestrictAddressFamilies=AF_UNIX
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
PrivateDevices=true
PrivateUsers=true
ProtectHostname=true
ProtectClock=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectKernelLogs=true
ProtectControlGroups=true
LockPersonality=true
MemoryDenyWriteExecute=true
RestrictRealtime=true
RestrictSUIDSGID=true
SystemCallArchitectures=native
SystemCallFilter=@system-service

[Install]
WantedBy=multi-user.target

seafile-seahub.service

sudo systemctl edit --full --force seafile-seahub.service:

[Unit]
Description=Seafile seahub web interface using uwsgi
ConditionPathExists=/etc/seafile/ccnet.conf
Requires=seafile-ccnet-server.service seafile-server.service
PartOf=seafile.target

[Service]
Type=notify
User=seafile
Group=seafile
EnvironmentFile=/etc/seafile/env
WorkingDirectory=/usr/share/seafile-server/seahub/seahub
ExecStart=/usr/bin/uwsgi --die-on-term -c /etc/uwsgi/seafile.ini
Restart=on-failure
SuccessExitStatus=15 17 29 30
StandardError=journal
NotifyAccess=all
CacheDirectory=seahub

# Hardening
ReadWritePaths=/var/log/seafile/ /tmp/ /var/lib/seafile/
AmbientCapabilities=CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
LockPersonality=yes
MemoryDenyWriteExecute=true
NoNewPrivileges=true
PrivateDevices=true
ProtectControlGroups=yes
ProtectHome=yes
ProtectHostname=yes
ProtectKernelLogs=yes
ProtectKernelModules=yes
ProtectKernelTunables=yes
ProtectSystem=strict
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
RestrictNamespaces=true
RestrictRealtime=true
SystemCallArchitectures=native
SystemCallFilter=@system-service

[Install]
WantedBy=multi-user.target

seafile-seafdav.service

sudo systemctl edit --full --force seafile-seafdav.service (solo se ci interessa il WebDAV):

[Unit]
Description=Seafile fileserver daemon
ConditionPathExists=/etc/seafile/ccnet.conf
Requires=seafile-ccnet-server.service
PartOf=seafile.target

[Service]
Type=simple
User=seafile
EnvironmentFile=/etc/seafile/env
ExecStart=/usr/bin/python -m wsgidav.server.server_cli \
    --server gunicorn --root / --log-file /var/log/seafile/seafdav.log \
    --port 6001 --host 127.0.0.1
# seafile-server fails when ccnet-server is restarted
Restart=always
# sleep 2, workaround ccnet-server not saying when ready
RestartSec=2

# Hardening
ReadWriteDirectories=/var/log/seafile
CapabilityBoundingSet=
AmbientCapabilities=
NoNewPrivileges=True
SecureBits=noroot-locked
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
PrivateDevices=true
PrivateUsers=true
ProtectHostname=true
ProtectClock=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectKernelLogs=true
ProtectControlGroups=true
LockPersonality=true
MemoryDenyWriteExecute=true
RestrictRealtime=true
RestrictSUIDSGID=true
SystemCallArchitectures=native
SystemCallFilter=@system-service

[Install]
WantedBy=multi-user.target

seafile.target

Infine una cosa simpatica per agevolare il tutto:

sudo systemctl edit --full --force seafile.target

[Unit]
Description=Seafile server
Requires=seafile-ccnet.service seafile-seafdav.service seafile-seahub.service seafile-server.service

Con quest’ultima configurazione possiamo lanciare sudo systemctl start seafile.target per avviare tutto l’ambaradan. Sostituendo stop o restart al posto di start possiamo fermare o riavviare tutti i servizi di Seafile in un comando solo e comodo.
Una piccola nota: perché funzioni stop e restart è necessario quel PartOf=seafile.target che ho aggiunto ad ogni unità. Ciò è dovuto alla semantica di Requires= definita da systemd che vincola le unità solo al momento dell’avvio e non dell’arresto.

NGINX

Infine configuriamo il proxy inverso, che nel mio caso è NGINX. Questo ultimo componente ci permette di accedere a Seafile in modo uniforme: tramite protocollo HTTPS sulla porta 443, ovviamente protetta dal protocollo TLS.

/etc/nginx/sites-available/seafile.conf:

upstream django {
	server 127.0.0.1:3579;
}

server {
	listen 80;
	server_name cloud.elinvention.ovh;
	return 301 https://$server_name$request_uri;
}

server {
	listen 443 http2 ssl;
	server_name cloud.elinvention.ovh;

	server_tokens off;

	ssl_certificate fullchain.pem;
	ssl_certificate_key privkey.pem;

	location / {
		include uwsgi_params;
		uwsgi_pass django;
		uwsgi_param X-Real-IP $remote_addr;
		uwsgi_param X-Forwarded-For $proxy_add_x_forwarded_for;
		uwsgi_param X-Forwarded-Host $server_name;
		uwsgi_read_timeout 36000;
		
	}

	location /seafhttp {
		rewrite ^/seafhttp(.*)$ $1 break;
		proxy_pass http://127.0.0.1:8082;
		client_max_body_size 0;
		proxy_connect_timeout  36000s;
		proxy_read_timeout  36000s;
		proxy_send_timeout  36000s;
		send_timeout  36000s;
		proxy_request_buffering off;
		proxy_http_version 1.1;
		access_log off;
	}

	location /seafdav {
		proxy_pass         http://127.0.0.1:6001;
		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-Host $server_name;
		proxy_http_version 1.1;
		proxy_connect_timeout  36000s;
		proxy_read_timeout  36000s;
		proxy_send_timeout  36000s;
		send_timeout  36000s;
		client_max_body_size 0;
		proxy_request_buffering off;
	}

	location /media {
		alias /usr/share/seafile-server/seahub/media/;
	}

	location /media/var/lib/seafile/seahub/ {
		# piccolo trick per far funzionare gli avatar
		alias /var/lib/seafile/seahub/;
	}

	location /static {
		alias /usr/share/seafile-server/seahub/static;
	}
}

Conclusioni

Questo è quanto. È un sacco di roba… però penso che ne valga la pena. Ritengo che questa configurazione sia molto più robusta di quella proposta nel manuale, se non altro per il fatto di aver isolato per bene i tre servizi tra di loro e dal resto del sistema, sfruttando le funzioni fornite da systemd. Inoltre ho imparato un sacco di cose su come funzionano i vari servizi che spesso vengono utilizzati senza pensare a cosa ci sta dietro.

Il software scritto dalla squadra di Seafile non è malaccio, anzi funziona piuttosto bene. La funzione che mi piace di più è la capacità di mantenere efficientemente una istantanea dei dati per ogni modifica effettuata. Questo mi consente di ripristinare i file cancellati accidentalmente o di annullare le modifiche.

Purtroppo però il codice non è molto pulito… Ad esempio esaminando questo sorgente ci si trova di fronte ad una chiave privata RSA, così senza commenti… Ciò mi lascia non poco perplesso. Per fortuna l’unico socket creato da ccnet è un socket UNIX, quindi non dovrebbe fare troppi danni.

Un altro problema in cui sono incappato è il fatto che ccnet si interrompe in modo anomalo ogni volta che termina il servizio. Accidenti alla gestione manuale della memoria!!! Specialmente quando il piccolo miglioramento delle prestazioni non è affatto bilanciato da questo tipo di bug! Da quel che ho potuto stabilire con valgrind, si tratta di una g_free di troppo, che ho commentato sperando che questo non mi porti a perdite di memoria esagerate. Questa è la patch:

diff --git a/lib/ccnet-session-base.c b/lib/ccnet-session-base.c
index 3f8cc33..44f15fa 100644
--- a/lib/ccnet-session-base.c
+++ b/lib/ccnet-session-base.c
@@ -81,7 +81,7 @@ finalize(GObject *gobject)
 {
     CcnetSessionBase *s = CCNET_SESSION_BASE(gobject);
     
-    g_free (s->name);
+    //g_free (s->name);
     g_free (s->relay_id);
 
     G_OBJECT_CLASS(ccnet_session_base_parent_class)->finalize (gobject);

Che dire… ccnet non è esattamente uno di quei software mantenuti bene e ben documentati. Nel 2021 è scritto in C e usa autotools come sistema di compilazione. Andrebbe riscritto in Rust…

Spero che la squadra che sviluppa Seafile riesca a rendere il software più conforme alle pratiche convenzionali in ambito UNIX. Come dimostra questa configurazione, ciò non è affatto impossibile. Se poi il tutto diventasse più trasparente e documentato non sarebbe male.