Cryptpad¶
Cryptpad is an end-to-end encrypted cloud office suite that enables collaboration and includes Rich Text, Spreadsheets, Code/Markdown, Kanban, Slides, Whiteboard, Polls, and Cloud Storage.
Preparation¶
sudo -s
Update apt and upgrade:
apt update && apt upgrade
Make sure git and curl are installed:
apt install git curl
If you haven't previously done so, install Node.js v12:
curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash -
apt install -y nodejs
Upgrade npm:
npm install -g npm
Install Bower with npm:
npm install -g bower
Create the user for the service:
useradd -m -d /var/www/cryptpad.example.com -s /bin/bash -p cryptpad cryptpad
Installing Cryptpad¶
Change to the newly created home directory for the user:
cd /var/www/cryptpad.example.com
Clone the git repository:
sudo -u cryptpad git clone https://github.com/xwiki-labs/cryptpad.git cryptpad
Move into the cryptpad directory and install all dependencies:
cd cryptpad
npm install
bower install
Copy the example config file and edit:
sudo -u cryptpad cp config/config.example.js config/config.js
At this point, I uncommented and set the following vavues:
httpUnsafeOrigin: 'https://cryptpad.example.com',
httpSafeOrigin: "https://cpsb.example.com",
httpPort: 3001,
httpSafePort: 3002,
adminEmail: 'admin@example.com',
By default, the ports are set to 3000 and 3001. I amended them slightly as I already had Gitea running on port 3000
Creating the certificates¶
After creating the DNS records in ClouDNS for cryptpad.example.com, cpsb.example.com, files.cryptpad.example.com, api.cryptpad.example.com (I ended up not using the latter two), create the certificates as follows:
Issue the RSA certificates with:
~/.acme.sh/acme.sh --issue --dns dns_cloudns -d cryptpad.example.com -d cpsb.example.com -d files.cryptpad.example.com -d api.cryptpad.example.com --keylength 4096 --key-file /etc/letsencrypt/rsa-certs/cryptpad.example.com/privkey.pem --ca-file /etc/letsencrypt/rsa-certs/cryptpad.example.com/chain.pem --cert-file /etc/letsencrypt/rsa-certs/cryptpad.example.com/cert.pem --fullchain-file /etc/letsencrypt/rsa-certs/cryptpad.example.com/fullchain.pem --pre-hook "mkdir -p /etc/letsencrypt/rsa-certs/cryptpad.example.com" --post-hook "find /etc/letsencrypt/rsa-certs/cryptpad.example.com/ -name '*.pem' -type f -exec chmod 600 {} \;" --renew-hook "find /etc/letsencrypt/rsa-certs/cryptpad.example.com/ -name '*.pem' -type f -exec chmod 600 {} \; -exec service nginx reload \;" --dnssleep 60
and ECC certificates with:
~/.acme.sh/acme.sh --issue --dns dns_cloudns -d cryptpad.example.com -d cpsb.example.com -d files.cryptpad.example.com -d api.cryptpad.example.com --keylength ec-384 --key-file /etc/letsencrypt/ecc-certs/cryptpad.example.com/privkey.pem --ca-file /etc/letsencrypt/ecc-certs/cryptpad.example.com/chain.pem --cert-file /etc/letsencrypt/ecc-certs/cryptpad.example.com/cert.pem --fullchain-file /etc/letsencrypt/ecc-certs/cryptpad.example.com/fullchain.pem --pre-hook "mkdir -p /etc/letsencrypt/ecc-certs/cryptpad.example.com" --post-hook "find /etc/letsencrypt/ecc-certs/cryptpad.example.com/ -name '*.pem' -type f -exec chmod 600 {} \;" --renew-hook "find /etc/letsencrypt/ecc-certs/cryptpad.example.com/ -name '*.pem' -type f -exec chmod 600 {} \; -exec service nginx reload \;" --dnssleep 60
Setting up Nginx¶
Create the server config as follows:
nano /etc/nginx/sites-available/cryptpad.example.com
Enter the following:
server {
listen 443 ssl http2;
set $main_domain "cryptpad.example.com";
set $sandbox_domain "cpsb.example.com";
set $api_domain "cryptpad.example.com";
set $files_domain "cryptpad.example.com";
server_name cryptpad.example.com cpsb.example.com;
ssl_certificate /etc/letsencrypt/ecc-certs/cryptpad.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/ecc-certs/cryptpad.example.com/privkey.pem;
ssl_certificate /etc/letsencrypt/rsa-certs/cryptpad.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/rsa-certs/cryptpad.example.com/privkey.pem;
ssl_trusted_certificate /etc/letsencrypt/ecc-certs/cryptpad.example.com/chain.pem;
ssl_dhparam /etc/ssl/certs/dhparam.pem; # openssl dhparam -out /etc/nginx/dhparam.pem 4096
ssl_session_timeout 5m;
ssl_session_cache shared:SSL_CP:5m;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers EECDH+AESGCM:EDH+AESGCM;
ssl_ecdh_curve secp384r1; # Requires nginx >= 1.1.0
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-XSS-Protection "1; mode=block";
add_header X-Content-Type-Options nosniff;
add_header Access-Control-Allow-Origin "*";
# Nginx Bad Bot Blocker Includes
include /etc/nginx/bots.d/ddos.conf;
include /etc/nginx/bots.d/blockbots.conf;
if ($allowed_country = no) {
set $blockreason '[geo_blocked]';
return 403;
}
if ( $bad_querystring !~* "\[OK\]" ) {
set $blockreason $bad_querystring;
return 403;
}
if ( $bad_request !~* "\[OK\]" ) {
set $blockreason $bad_request;
return 403;
}
if ( $bad_request_method !~* "\[OK\]" ) {
set $blockreason $bad_request_method;
return 403;
}
# End Bad Bot Blocker Includes
set $coop '';
if ($uri ~ ^\/sheet\/.*$) { set $coop 'same-origin'; }
add_header Cross-Origin-Resource-Policy cross-origin;
add_header Cross-Origin-Opener-Policy $coop;
add_header Cross-Origin-Embedder-Policy require-corp;
root /var/www/cryptpad.example.com/cryptpad;
index index.html;
error_page 404 /customize.dist/404.html;
# any static assets loaded with "ver=" in their URL will be cached for a year
if ($args ~ ver=) {
set $cacheControl max-age=31536000;
}
if ($uri ~ ^/.*(\/|\.html)$) {
set $cacheControl no-cache;
}
add_header Cache-Control $cacheControl;
set $styleSrc "'unsafe-inline' 'self' ${main_domain}";
set $connectSrc "'self' https://${main_domain} ${main_domain} https://${api_domain} blob: wss://${api_domain} ${api_domain} ${files_domain}";
set $fontSrc "'self' data: ${main_domain}";
set $imgSrc "'self' data: * blob: ${main_domain}";
set $frameSrc "'self' ${sandbox_domain} blob:";
set $mediaSrc "'self' data: * blob: ${main_domain}";
set $childSrc "https://${main_domain}";
set $workerSrc "https://${main_domain}";
set $scriptSrc "'self' resource: ${main_domain}";
set $unsafe 0;
if ($uri = "/sheet/inner.html") { set $unsafe 1; }
if ($uri ~ ^\/common\/onlyoffice\/.*\/index\.html.*$) { set $unsafe 1; }
if ($host != $sandbox_domain) { set $unsafe 0; }
if ($unsafe) {
set $scriptSrc "'self' 'unsafe-eval' 'unsafe-inline' resource: ${main_domain}";
}
add_header Content-Security-Policy "default-src 'none'; child-src $childSrc; worker-src $workerSrc; media-src $mediaSrc; style-src $styleSrc; script-src $scriptSrc; connect-src $connectSrc; font-src $fontSrc; img-src $imgSrc; frame-src $frameSrc;";
location ^~ /cryptpad_websocket {
proxy_pass http://localhost:3001;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# WebSocket support (nginx 1.4)
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection upgrade;
}
# Deny access to registration page
#location /register {
#deny all;
#}
location ^~ /customize.dist/ {
# This is needed in order to prevent infinite recursion between /customize/ and the root
}
location ^~ /customize/ {
rewrite ^/customize/(.*)$ $1 break;
try_files /customize/$uri /customize.dist/$uri;
}
location = /api/config {
proxy_pass http://localhost:3001;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
location ^~ /blob/ {
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range';
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Type' 'application/octet-stream; charset=utf-8';
add_header 'Content-Length' 0;
return 204;
}
add_header Cache-Control max-age=31536000;
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range,Content-Length';
add_header 'Access-Control-Expose-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range,Content-Length';
try_files $uri =404;
}
# the "block-store" serves encrypted payloads containing users' drive keys
# these payloads are unlocked via login credentials. They are mutable
# and are thus never cached. They're small enough that it doesn't matter, in any case.
location ^~ /block/ {
add_header Cache-Control max-age=0;
try_files $uri =404;
}
location ~ ^/(register|login|settings|user|pad|drive|poll|slide|code|whiteboard|file|media|profile|contacts|todo|filepicker|debug|kanban|sheet|support|admin|notifications|teams)$ {
rewrite ^(.*)$ $1/ redirect;
}
try_files /www/$uri /www/$uri/index.html /customize/$uri;
}
If you need a greater understanding of the above, an extensively commented example ngix config is available to view at https://github.com/xwiki-labs/cryptpad/blob/main/docs/example.nginx.conf
I have also added a section to disallow access to the registrtion page (comented out for now), and a section to disallow access to bad bots (you will need to have followed my earlier LEAMP Server guides for this to work)
Apparmor Profiles¶
Add the following line to your Nginx profile (I'm assuming you have one)
nano /etc/apparmor.d/usr.sbin.nginx
/var/www/cryptpad.example.com/cryptpad/blob/** r,
/var/www/cryptpad.example.com/cryptpad/block/** r,
/var/www/cryptpad.example.com/cryptpad/customize.dist/** r,
/var/www/cryptpad.example.com/cryptpad/customize/** r,
/var/www/cryptpad.example.com/cryptpad/www/** r,
Now create a cryptpad profile:
nano /etc/apparmor.d/cryptpad
Enter the following:
#include <tunables/global>
profile cryptpad flags=(complain, attach_disconnected) {
#include <abstractions/base>
#include <abstractions/nameservice>
#include <abstractions/ssl_keys>
#include <abstractions/user-tmp>
/sys/fs/cgroup/memory/memory.limit_in_bytes r,
/usr/bin/node mrix,
owner /proc/*/fd/ r,
owner /var/www/cryptpad.example.com/cryptpad/ r,
owner /var/www/cryptpad.example.com/cryptpad/**/ r,
owner /var/www/cryptpad.example.com/cryptpad/blob/** w,
owner /var/www/cryptpad.example.com/cryptpad/block/** w,
owner /var/www/cryptpad.example.com/cryptpad/config/config.js r,
owner /var/www/cryptpad.example.com/cryptpad/data/**.ndjson rw,
owner /var/www/cryptpad.example.com/cryptpad/data/archive/datastore/ w,
owner /var/www/cryptpad.example.com/cryptpad/data/archive/datastore/*/ w,
owner /var/www/cryptpad.example.com/cryptpad/data/blobstage/** rw,
owner /var/www/cryptpad.example.com/cryptpad/data/logs/*/ rw,
owner /var/www/cryptpad.example.com/cryptpad/data/pins/** w,
owner /var/www/cryptpad.example.com/cryptpad/datastore/**.ndjson rw,
owner /var/www/cryptpad.example.com/cryptpad/datastore/*/ rw,
owner /var/www/cryptpad.example.com/cryptpad/lib/**.js r,
owner /var/www/cryptpad.example.com/cryptpad/node_modules/**.js r,
owner /var/www/cryptpad.example.com/cryptpad/node_modules/**.json r,
owner /var/www/cryptpad.example.com/cryptpad/package.json r,
owner /var/www/cryptpad.example.com/cryptpad/server.js r,
owner /var/www/cryptpad.example.com/cryptpad/www/** r,
}
Reload apparmor:
service apparmor reload
After thoroughly testing, you can enforce the profile with:
aa-enforce cryptpad
Systemd Unit¶
Now we create a systemd unit in order to manage the service
nano /etc/systemd/system/cryptpad.service
Enter the following:
[Unit]
Description=CryptPad (The Zero Knowledge Cloud)
After=syslog.target network.target
Requires=nginx.service
[Service]
Type=simple
User=cryptpad
Group=cryptpad
Environment='PWD="/var/www/cryptpad.example.com/cryptpad"'
WorkingDirectory=/var/www/cryptpad.example.com/cryptpad
ExecStart=/usr/bin/node /var/www/cryptpad.example.com/cryptpad/server.js
AppArmorProfile=cryptpad
TimeoutSec=30
RestartSec=2
StandardOutput=syslog
StandardError=syslog
SyslogIdentifier=cryptpad
Restart=always
### Modify these two values and uncomment them if you have lots of files and get an HTTP error 500 because of that
#LimitMEMLOCK=infinity
#LimitNOFILE=65535
### If you want to bind CryptPad to a port below 1024 uncomment the two values below
#CapabilityBoundingSet=CAP_NET_BIND_SERVICE
#AmbientCapabilities=CAP_NET_BIND_SERVICE
# Some extra security directives
ProtectSystem=full
PrivateDevices=true
NoNewPrivileges=true
ProtectHome=true
CapabilityBoundingSet=~CAP_SYS_ADMIN
[Install]
WantedBy=multi-user.target
Reload the systemd daemon:
systemctl daemon-reload
Enable the service, start, and then check:
systemctl enable cryptpad.service
service cryptpad start
service cryptpad status
Final steps¶
Enable the nginx site and reload:
ln -s /etc/nginx/sites-available/cryptpad.example.com /etc/nginx/sites-enabled/
service nginx reload
You can check for errors in the nginx config with:
nginx -t
Access the site at cryptpad.example.com and create an account. Once logged in, click the button in the top right (should be the first letter of your username by default) and go to settings. Copy your public key from that page.
Now edit the cryptpad config file:
nano /var/www/cryptpad.example.com/cryptpad/config/config.js
Uncomment the line to enable the administration panel for your account, replacing the example public key with your own. For example:
adminKeys: [
"[user@cryptpad.example.com/asdbca23s3kajh$234098dalkjas79mnxcas=]",
],
Logout of cryptpad and restart the cryptpad service
service cryptpad restart
After logging back in, you should now see an administration entry in the menu