Securely serving webapps using uWSGI
socket activation
webapp encapsulation and jailing
self-healing
being able to separetely manage services
exit after idle
Preparing nginx
Hardening uWSGI
-
/usr/lib/systemd/system/uwsgi-secure@.service
[Unit] Description=uWSGI service unit After=syslog.target [Service] ExecStart=/usr/bin/uwsgi --ini /etc/uwsgi/%I.ini ExecReload=/bin/kill -HUP $MAINPID ExecStop=/bin/kill -INT $MAINPID Restart=always Type=notify StandardError=syslog NotifyAccess=all KillSignal=SIGQUIT [Install] WantedBy=multi-user.target
-
/usr/lib/systemd/system/uwsgi-secure@.socket
-
/etc/uwsgi/mywebapp.ini
[uwsgi] # name the process procname-master = mywebapp # define the plugin plugins = php # define a master process for this app master = true # this is where the socket resides socket = /run/uwsgi/%n.sock # we want to use this user and group (or any other) uid = http gid = http # give this application a maximum of 10 processes processes = 10 # dynamic scaling # minimum amount of workers/processes to keep at all times cheaper = 2 # increase workers/processes by step cheaper-step = 1 # mark as idle after 10 minutes idle = 600 # kill the webapp when it is idle die-on-idle = true # allow no other extenseion than .php php-allowed-ext = .php # fix our application in this directory php-docroot = /usr/share/webapps/mywebapp # set the standard index php-index = index.php php-set = date.timezone=Europe/Berlin # the application needs access to the following directories php-set = open_basedir=/tmp/:/usr/share/webapps/mywebapp:/etc/webapps/mywebapp # this is where we save our sessions php-set = session.save_path=/tmp # mywebapp needs the following PHP extensions php-set = extension=curl.so php-set = extension=gd.so php-set = extension=imagick.so php-set = extension=intl.so php-set = extension=mysqli.so php-set = extension=pdo_mysql.so
-
uwsgi-2.0.14/uwsgi.h
-
/etc/systemd/system/uwsgi-private@.service
[Unit] Description=uWSGI service unit After=syslog.target [Service] ExecStart=/usr/bin/uwsgi --ini /etc/uwsgi/%I.ini Type=notify SuccessExitStatus=15 17 29 30 StandardError=syslog NotifyAccess=all KillSignal=SIGQUIT PrivateDevices=yes PrivateTmp=yes ProtectSystem=full ReadWriteDirectories=/etc/webapps /var/lib/ ProtectHome=yes NoNewPrivileges=yes [Install] WantedBy=multi-user.target
-
/etc/systemd/system/uwsgi-private@.service
Webapps
MantisBT
-
/etc/nginx/mantisbt.conf
# ... location ~ ^/(admin|core|doc|lang) { deny all; access_log off; log_not_found off; } location / { index index.php; try_files $uri $uri/ @mantisbt; } location @mantisbt { include uwsgi_params; uwsgi_modifier1 14; uwsgi_pass unix:/run/uwsgi/mantisbt.sock; } location ~ \.php?$ { include uwsgi_params; uwsgi_modifier1 14; uwsgi_pass unix:/run/uwsgi/mantisbt.sock; } # Deny serving files beginning with a dot, but allow letsencrypt acme-challenge location ~ /\.(?!well-known/acme-challenge) { access_log off; log_not_found off; deny all; } # ...
-
/etc/uwsgi/mantisbt.ini
[uwsgi] procname-master = mantisbt plugins = php master = true socket = /run/uwsgi/%n.sock uid = http gid = http processes = 10 cheaper = 2 cheaper-step = 1 idle = 600 die-on-idle = true php-allowed-ext = .php php-docroot = /usr/share/webapps/mantisbt php-index = index.php php-set = date.timezone=Europe/Berlin php-set = open_basedir=/tmp/:/usr/share/fonts/TTF:/usr/share/webapps/mantisbt:/usr/share/webapps/mantisbt/core:/etc/webapps/mantisbt php-set = session.save_path=/tmp php-set = session.gc_maxlifetime 21600 php-set = session.gc_divisor 500 php-set = session.gc_probability 1 php-set = post_max_size=64M php-set = upload_max_filesize=64M php-set = always_populate_raw_post_data=-1 php-set = extension=curl.so php-set = extension=gd.so php-set = extension=imagick.so php-set = extension=intl.so php-set = extension=mysqli.so php-set = extension=pdo_mysql.so
Roundcube
-
/etc/nginx/roundcube.conf
# ... location / { index index.php; try_files $uri $uri/$args @roundcubemail; } location @roundcubemail { include uwsgi_params; uwsgi_modifier1 14; uwsgi_pass unix:/run/uwsgi/roundcubemail.sock; } location ~ ^/favicon.ico$ { root /usr/share/webapps/roundcubemail/skins/classic/images; log_not_found off; access_log off; expires max; } location = /robots.txt { allow all; log_not_found off; access_log off; expires 30d; } # Deny serving some files location ~ ^/(composer\.json-dist|composer\.json|package\.xml|CHANGELOG|INSTALL|LICENSE|README\.md|UPGRADING|bin|config|installer|program\/(include|lib|localization|steps)|SQL|tests)$ { deny all; } # Deny serving files beginning with a dot, but allow letsencrypt acme-challenge location ~ /\.(?!well-known/acme-challenge) { deny all; access_log off; log_not_found off; } # ...
-
/etc/uwsgi/roundcubemail.ini
[uwsgi] procname-master = roundcubemail plugins = php socket = /run/uwsgi/%n.sock master = true uid = http gid = http processes = 10 cheaper = 2 cheaper-step = 1 idle = 60 die-on-idle = true ; create a cache with 1000 items named roundcube cache2 = name=roundcube,items=1000 php-allowed-ext = .php php-docroot = /usr/share/webapps/roundcubemail php-index = index.php php-set = date.timezone=Europe/Berlin php-set = session.save_path=/tmp php-set = session.save_handler=uwsgi php-set = session.gc_maxlifetime 21600 php-set = session.gc_divisor 500 php-set = session.gc_probability 1 php-set = open_basedir=/tmp/:/usr/share/webapps/roundcubemail/:/etc/webapps/roundcubemail/:/var/cache/roundcubemail/:/var/log/roundcubemail/:/secure/location/of/gnupg/keys/for/enigma:/usr/bin/gpg:/usr/bin/gpg-agent php-set = post_max_size=64M php-set = upload_max_filesize=64M php-set = error_reporting=E_ALL php-set = log_errors=On php-set = extension=exif.so php-set = extension=iconv.so php-set = extension=intl.so php-set = extension=imap.so php-set = extension=mcrypt.so php-set = extension=pdo_mysql.so php-set = extension=pspell.so php-set = extension=zip.so
ownCloud
-
/etc/nginx/owncloud.conf
# ... location = /robots.txt { allow all; log_not_found off; access_log off; } location ~ ^/(?:\.htaccess|data|config|db_structure\.xml|README) { deny all; log_not_found off; access_log off; } location ~ ^(.+\.php)(.*)$ { include uwsgi_params; uwsgi_modifier1 14; uwsgi_pass unix:/run/uwsgi/owncloud.sock; uwsgi_intercept_errors on; } location / { root /usr/share/webapps/owncloud; index index.php; rewrite ^/.well-known/host-meta /public.php?service=host-meta last; rewrite ^/.well-known/host-meta.json /public.php?service=host-meta-json last; rewrite ^/.well-known/carddav /remote.php/dav/ redirect; rewrite ^/.well-known/caldav /remote.php/dav/ redirect; rewrite ^(/core/doc/[^\/]+/)$ $1/index.html; rewrite ^/caldav(.*)$ /remote.php/dav$1 redirect; rewrite ^/carddav(.*)$ /remote.php/dav$1 redirect; rewrite ^/webdav(.*)$ /remote.php/dav$1 redirect; try_files $uri $uri/ /index.php; } location ~ ^/.(?:jpg|jpeg|gif|bmp|ico|png|css|js|swf)$ { expires 30d; access_log off; } # ...
-
/etc/uwsgi/owncloud.ini
[uwsgi] procname-master = owncloud plugins = php master = true socket = /run/uwsgi/%n.sock uid = http gid = http processes = 10 cheaper = 2 cheaper-step = 1 idle = 600 die-on-idle = true owncloud_data_dir = /absolute/path/to/where/your/data/resides owncloud_writable_apps_dir = /absolute/path/to/writable/apps chdir = %(owncloud_data_dir) php-allowed-ext = .php php-docroot = /usr/share/webapps/owncloud php-index = index.php php-set = date.timezone=Europe/Berlin php-set = open_basedir=%(owncloud_data_dir):%(owncloud_writable_apps_dir):/tmp/:/usr/share/webapps/owncloud:/etc/webapps/owncloud:/dev/urandom:/run/redis/redis.sock php-set = session.save_path=/tmp php-set = session.gc_maxlifetime 21600 php-set = session.gc_divisor 500 php-set = session.gc_probability 1 php-set = post_max_size=1000M php-set = upload_max_filesize=1000M php-set = always_populate_raw_post_data=-1 php-set = max_input_time=120 php-set = max_execution_time=60 php-set = memory_limit=256M php-set = extension=bz2.so php-set = extension=curl.so php-set = extension=exif.so php-set = extension=gd.so php-set = extension=imagick.so php-set = extension=intl.so php-set = extension=gmp.so php-set = extension=iconv.so php-set = extension=mcrypt.so php-set = extension=pdo_mysql.so php-set = extension=redis.so php-set = extension=sockets.so php-set = extension=xmlrpc.so php-set = extension=xsl.so php-set = extension=zip.so cron = -15 -1 -1 -1 -1 curl --silent https://owncloud.domain.tld/cron.php 1>/dev/null
Mailman
-
/etc/nginx/mailman.conf
# ... # Send all access to / to uwsgi location / { gzip off; include uwsgi_params; uwsgi_modifier1 9; uwsgi_pass unix:/run/uwsgi/mailman.sock; } # Set alias for accessing /icons location /icons { alias /usr/lib/mailman/icons; autoindex on; } # Set alias for accessing /archives location /archives { alias /var/lib/mailman/archives/public; autoindex on; } # Deny serving files beginning with a dot, but allow letsencrypt acme-challenge location ~ /\.(?!well-known/acme-challenge) { access_log off; log_not_found off; deny all; } # ...
-
/etc/mailman/mm_cfg.py
-
/etc/uwsgi/mailman.ini
-
/etc/systemd/system/uwsgi@.service
[Unit] Description=uWSGI service unit After=syslog.target [Service] ExecStart=/usr/bin/uwsgi --ini /etc/uwsgi/%I.ini Type=notify SuccessExitStatus=15 17 29 30 StandardError=syslog NotifyAccess=all KillSignal=SIGQUIT PrivateDevices=yes PrivateTmp=yes ProtectSystem=full ReadWriteDirectories=/etc/webapps ProtectHome=yes [Install] WantedBy=multi-user.target
Stikked
-
/etc/nginx/stikked.conf
# ... location / { index index.php; try_files $uri $uri/ @stikked; } location @stikked { rewrite ^/(.*)$ /index.php?/$1 last; } location ~ \.php?$ { include uwsgi_params; uwsgi_modifier1 14; uwsgi_pass unix:/run/uwsgi/stikked.sock; } # Deny serving some directories location ^~ ^/(application|system)/ { deny all; } # Serve some static files location ~* ^.+(favicon.ico|static|robots.txt) { expires 30d; } # Deny serving files beginning with a dot, but allow letsencrypt acme-challenge location ~ /\.(?!well-known/acme-challenge) { access_log off; log_not_found off; deny all; } # ...
-
/etc/uwsgi/stikked.ini
[uwsgi] procname-master = stikked plugins = php master = true socket = /run/uwsgi/%n.sock uid = http gid = http processes = 10 cheaper = 2 cheaper-step = 1 idle = 120 die-on-idle = true cache2 = name=stikked,items=1000 php-allowed-ext = .php php-index = index.php php-docroot = /usr/share/webapps/Stikked php-set = date.timezone=Europe/Berlin php-set = open_basedir=/tmp/:/usr/share/webapps/Stikked/:/etc/webapps/stikked/ php-set = session.save_path=stikked php-set = session.save_handler=uwsgi php-set = session.gc_maxlifetime 21600 php-set = session.gc_divisor 500 php-set = session.gc_probability 1 php-set = extension=gd.so php-set = extension=mysqli.so # cleanup pastes every 5 minutes cron = -5 -1 -1 -1 -1 curl --silent https://stikked.domain.tld/index.php/cron/stringFromConfig
Wordpress
-
/etc/nginx/wordpress.conf
# ... index index.php; ## Global restrictions location = /favicon.ico { log_not_found off; access_log off; } location = /robots.txt { allow all; log_not_found off; access_log off; } # Deny all attempts to access hidden files such as .htaccess, .htpasswd, .DS_Store (Mac). # Keep logging the requests to parse later (or to pass to firewall utilities such as fail2ban) location ~ /\. { deny all; } # Deny access to any files with a .php extension in the uploads directory # Works in sub-directory installs and also in multisite network # Keep logging the requests to parse later (or to pass to firewall utilities such as fail2ban) location ~* /(?:uploads|files)/.*\.php$ { deny all; } ## WordPress multisite subdirectory rules. # This order might seem weird - this is attempted to match last if rules below fail. # http://wiki.nginx.org/HttpCoreModule location / { try_files $uri $uri/ /index.php?$args; } # Directives to send expires headers and turn off 404 error logging. location ~* \.(js|css|png|jpg|jpeg|gif|ico)$ { expires 24h; log_not_found off; } # Add trailing slash to */wp-admin requests. rewrite /wp-admin$ $scheme://$host$uri/ permanent; # Directives to send expires headers and turn off 404 error logging. location ~* ^.+\.(ogg|ogv|svg|svgz|eot|otf|woff|mp4|ttf|rss|atom|jpg|jpeg|gif|png|ico|zip|tgz|gz|rar|bz2|doc|xls|exe|ppt|tar|mid|midi|wav|bmp|rtf)$ { access_log off; log_not_found off; expires max; } # Uncomment one of the lines below for the appropriate caching plugin (if used). #include global/wordpress-ms-subdir-wp-super-cache.conf; #include global/wordpress-ms-subdir-w3-total-cache.conf; # Rewrite multisite '.../wp-.*' and '.../*.php'. if (!-e $request_filename) { rewrite /wp-admin$ $scheme://$host$uri/ permanent; rewrite ^/[_0-9a-zA-Z-]+(/wp-.*) $1 last; rewrite ^/[_0-9a-zA-Z-]+(/.*\.php)$ $1 last; } # Pass all .php files on to uwsgi location ~ \.php$ { include uwsgi_params; uwsgi_modifier1 14; uwsgi_pass unix:/run/uwsgi/wordpress.sock; } ## Errors # redirect server error pages to the static page /50x.html error_page 500 502 503 504 /50x.html; location = /50x.html { root /usr/share/nginx/html; } # ...
-
/etc/uwsgi/wordpress.ini
[uwsgi] procname-master = wordpress plugins = php master = true socket = /run/uwsgi/%n.sock uid = http gid = http processes = 10 cheaper = 2 cheaper = 1 idle = 360 die-on-idle = true cache2 = name=wordpress,items=1000 php-allowed-ext = .php php-docroot = /srv/http/websites/domain.tld php-index = index.php php-set = date.timezone=Europe/Berlin php-set = open_basedir=/srv/http/websites/domain.tld:/tmp/:/usr/share/pear/ php-set = upload_max_filesize=24M php-set = post_max_filesize=64M php-set = post_max_size=64M php-set = session.save_path=/tmp php-set = session.save_handler=uwsgi php-set = session.gc_maxlifetime 21600 php-set = session.gc_divisor 500 php-set = session.gc_probability 1 php-set = extension=gd.so php-set = extension=iconv.so php-set = extension=mysqli.so ; run wp-cron.php job for wordpress every 10 minutes cron = -10 -1 -1 -1 -1 curl --silent https://domain.tld/wp-cron.php 1>/dev/null
PostfixAdmin
-
/etc/nginx/postfixadmin.conf
# ... location / { index index.php; } # pass all .php or .php/path urls to uWSGI location ~ ^(.+\.php)(.*)$ { include uwsgi_params; uwsgi_modifier1 14; uwsgi_pass unix:/run/uwsgi/postfixadmin.sock; } location ~ ^/(config|installer|composer.json-dist|.htaccess|CHANGELOG|INSTALL|LICENSE|README.md|UPGRADING) { access_log off; log_not_found off; deny all; } # Serve some static files location ~* ^.+(robots.txt) { allow all; log_not_found off; access_log off; expires 30d; } # Deny serving files beginning with a dot, but allow letsencrypt acme-challenge location ~ /\.(?!well-known/acme-challenge) { access_log off; log_not_found off; deny all; } # ...
-
/etc/uwsgi/postfixadmin.ini
[uwsgi] procname-master = postfixadmin master = true plugins = php socket = /run/uwsgi/%n.sock uid = http gid = http processes = 10 cheaper = 2 cheaper-step = 1 idle = 120 die-on-idle = true php-allowed-ext = .php php-docroot = /usr/share/webapps/postfixAdmin php-index = index.php php-set = date.timezone=Europe/Berlin php-set = open_basedir=/tmp/:/usr/share/webapps/postfixAdmin/:/etc/webapps/postfixadmin/:/usr/share/doc/postfixadmin/ php-set = session.save_path=/tmp php-set = session.gc_maxlifetime 21600 php-set = session.gc_divisor 500 php-set = session.gc_probability 1 php-set = extension=mysqli.so php-set = extension=imap.so
phpMyAdmin
-
/etc/nginx/phpmyadmin.conf
# ... client_max_body_size 200M; location / { index index.php; } location ~ ^(.+\.php)(.*)$ { include uwsgi_params; uwsgi_modifier1 14; uwsgi_pass unix:/run/uwsgi/phpmyadmin.sock; } # Serve some static files location ~* ^.+(print.css|favicon.ico|robots.txt) { expires 30d; } location ~ ^/(setup|CONTRIBUTING.md|ChangeLog|DCO|LICENSE|README|RELEASE-DATE*|composer.json) { deny all; } # Deny serving files beginning with a dot, but allow letsencrypt acme-challenge location ~ /\.(?!well-known/acme-challenge) { access_log off; log_not_found off; deny all; } # ...
-
/etc/uwsgi/phpmyadmin.ini
[uwsgi] procname-master = phpmyadmin plugins = php master = true socket = /run/uwsgi/%n.sock uid = http gid = http processes = 10 cheaper = 2 cheaper-step = 1 idle = 600 die-on-idle = true php-allowed-ext = .php php-docroot = /usr/share/webapps/phpMyAdmin php-index = index.php php-set = date.timezone=Europe/Berlin php-set = open_basedir=/tmp/:/usr/share/webapps/phpMyAdmin:/etc/webapps/phpmyadmin php-set = session.save_path=/tmp php-set = session.gc_maxlifetime 21600 php-set = session.gc_divisor 500 php-set = session.gc_probability 1 php-set = post_max_size=64M php-set = upload_max_filesize=64M php-set = extension=bz2.so php-set = extension=mysqli.so php-set = extension=mcrypt.so php-set = extension=zip.so
cgit
-
/etc/nginx/cgit.conf
# ... location ~* ^.+(cgit.(css|png)|favicon.ico|robots.txt|\.well-known/acme-challenge) { expires 30d; } location / { try_files $uri @cgit; } location @cgit { gzip off; include uwsgi_params; uwsgi_modifier1 9; uwsgi_pass unix:/run/uwsgi/cgit.sock; } location = /50x.html { root /usr/share/nginx/html; } # ...
-
/etc/uwsg/cgit.ini
Mediawiki
-
/etc/nginx/mediawiki.conf
# ... location / { index index.php; try_files $uri $uri/ @mediawiki; } location @mediawiki { rewrite ^/(.*)$ /index.php?title=$1&$args; } location ~ \.php5?$ { include uwsgi_params; uwsgi_modifier1 14; uwsgi_pass unix:/run/uwsgi/mediawiki.sock; } location ~* \.(js|css|png|jpg|jpeg|gif|ico)$ { try_files $uri /index.php; expires max; log_not_found off; } # Restrictions based on the .htaccess files location ^~ ^/(cache|includes|maintenance|languages|serialized|tests|images/deleted)/ { deny all; } location ^~ ^/(bin|docs|extensions|includes|maintenance|mw-config|resources|serialized|tests)/ { internal; } location ^~ /images/ { try_files $uri /index.php; } # Deny serving files beginning with a dot, but allow letsencrypt acme-challenge location ~ /\.(?!well-known/acme-challenge) { access_log off; log_not_found off; deny all; } # ...
-
/etc/uwsgi/mediawiki.ini
[uwsgi] procname-master = mediawiki plugins = php master = true socket = /run/uwsgi/%n.sock uid = http gid = http processes = 10 cheaper = 2 cheaper-step = 1 idle = 360 die-on-idle = true cache2 = name=mediawiki,items=1000 php-allowed-ext = .php php-docroot = /usr/share/webapps/mediawiki php-index = index.php php-set = date.timezone=Europe/Berlin php-set = open_basedir=/tmp/:/usr/share/pear/:/usr/share/webapps/mediawiki/:/etc/webapps/mediawiki/:/var/lib/mediawiki/:/usr/bin/ php-set = include_path=.:/usr/share/pear php-set = log_errors=On php-set = display_errors=Off php-set = error_reporting=E_ALL php-set = upload_max_filesize=128M php-set = post_max_filesize=128M php-set = post_max_size=128M php-set = session.save_path=/tmp php-set = session.gc_maxlifetime 21600 php-set = session.gc_divisor 500 php-set = session.gc_probability 1 php-set = extension=gd.so php-set = extension=iconv.so php-set = extension=intl.so php-set = extension=mysqli.so php-set = extension=redis.so
Etherpad
-
/etc/nginx/etherpad-lite.conf
location ~ ^/p/lac2016(.*) { include pad.sleepmap-auth-lac2016.conf; try_files $uri @etherpad-lite; } location / { try_files $uri @etherpad-lite; } location @etherpad-lite { proxy_pass http://localhost:9001; proxy_redirect off; proxy_buffering on; proxy_request_buffering on; proxy_read_timeout 150; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; }