diff --git a/machines/kat-son/default.nix b/machines/kat-son/default.nix index 31ab59b..771864d 100644 --- a/machines/kat-son/default.nix +++ b/machines/kat-son/default.nix @@ -10,6 +10,7 @@ imports = [ ./hardware-configuration.nix ./disks.nix + ./zulip ]; boot.loader.systemd-boot.enable = true; @@ -88,7 +89,6 @@ virtualHosts = { "son.katvayor.net" = { enableACME = true; - addSSL = true; }; }; }; diff --git a/machines/kat-son/zulip/default.nix b/machines/kat-son/zulip/default.nix new file mode 100644 index 0000000..8c62319 --- /dev/null +++ b/machines/kat-son/zulip/default.nix @@ -0,0 +1,41 @@ +{ pkgs, ... }: +let + home = "/var/lib/zulip"; +in +{ + imports = [ ./postgres.nix ./nginx.nix ]; + +# profile/base.pp + users = { + groups.zulip = { }; + users.zulip = { + isSystemUser = true; + group = "zulip"; + inherit home; + homeMode = "755"; + }; + }; + + systemd = { + tmpfiles.rules = [ + ''f ${home}/zulip.conf 644 zulip zulip ""'' + ''f ${home}/settings.py 644 zulip zulip ""'' + ''f ${home}/zulip-secrets.conf 640 zulip zulip ""'' + ]; + }; + + #security.pam.loginLimits = [ + # { + # domain = "zulip"; + # type = "soft"; + # item = "nofile"; + # value = "1000000"; + # } + # { + # domain = "zulip"; + # type = "hard"; + # item = "nofile"; + # value = "1048576"; + # } + #]; +} diff --git a/machines/kat-son/zulip/nginx.nix b/machines/kat-son/zulip/nginx.nix new file mode 100644 index 0000000..21594e1 --- /dev/null +++ b/machines/kat-son/zulip/nginx.nix @@ -0,0 +1,278 @@ +{ pkgs, lib, ... }: +let + zulip-includes = pkgs.stdenv.mkDerivation rec { + pname = "zulip-include"; + version = "8.4"; + + src = pkgs.fetchFromGitHub { + owner = "zulip"; + repo = "zulip"; + rev = version; + hash = "sha256-wsdVlJ0RDWsKwpvT0TsqqT8v5bubjsPDaGebRiIoQoQ="; + }; + installPhase = '' + mkdir $out + cp puppet/zulip/files/nginx/{dhparam.pem,uwsgi_params,zulip-include-frontend/*,zulip-include-common/*} $out + sed -i "s/\/etc\/nginx\/zulip-include/\/nix\/store\/$(basename $out)/" $out/* + ''; + }; + host = "son.katvayor.net"; + home = "/var/lib/zulip"; + loadbalancers = [ ]; +in +{ + # nginx.pp + services.nginx = { + enable = true; + recommendedGzipSettings = true; + sslDhparam = zulip-includes + "/dhparam.pem"; + upstreams = { + django.servers."unix:${home}/deployments/uwsgi-socket" = { }; + localhost_sso.servers."127.0.0.1:8888" = { }; + camo.servers."127.0.0.1:9292" = { }; + # TODO later : mess with tornado + tornado.servers."127.0.0.1:9800" = { + keepalive = 10000; + }; + }; + commonHttpConfig = + let + trusted-proto = + if loadbalancers == [ ] then + '' + map $remote_addr $trusted_x_forwarded_proto { + default $scheme; + } + map $http_x_forwarded_for $x_proxy_misconfiguration { + default ""; + "~." "No proxies configured in Zulip, but proxy headers detected from proxy at $remote_addr; see https://zulip.readthedocs.io/en/latest/production/reverse-proxies.html"; + } + '' + else + ''''; # TODO later : loadbalancers + tornado_map = # TODO later : mess with tornado + '' + map "" $tornado_server { + default http://tornado; + } + ''; + + in + '' + ${trusted-proto} + ${tornado_map} + ''; + virtualHosts.${host} = { + forceSSL = true; + enableACME = true; + extraConfig = '' + error_page 502 503 504 /static/webpack-bundles/5xx.html; + ''; + locations = { + # app + "/local-static/" = { + alias = "${home}/local-static/"; + }; + "/static/" = { + alias = "${home}/prod-static/"; + extraConfig = '' + include ${zulip-includes}/headers; + add_header Access-Control-Allow-Origin *; + add_header Timing-Allow-Origin *; + error_page 404 /django_static_404.html; + location ~ '\.[0-9a-f]{12}\.|[./][0-9a-f]{20}\.' { + include ${zulip-includes}/headers; + add_header Access-Control-Allow-Origin *; + add_header Timing-Allow-Origin *; + add_header Cache-Control "public, max-age=31536000, immutable"; + } + ''; + }; + "/json/events" = { + extraConfig = '' + if ($request_method = 'OPTIONS') { + # add_header does not propagate into/out of blocks, so this + # include cannot be factored out + include ${zulip-includes}/headers; + add_header Allow 'OPTIONS, GET, DELETE' always; + return 204; + } + + if ($request_method !~ ^(GET|DELETE)$ ) { + # add_header does not propagate into/out of blocks, so this + # include cannot be factored out + include ${zulip-includes}/headers; + add_header Allow 'OPTIONS, GET, DELETE' always; + return 405; + } + + proxy_pass $tornado_server; + include ${zulip-includes}/proxy_longpolling; + ''; + }; + "/api/v1/events" = { + extraConfig = '' + if ($request_method = 'OPTIONS') { + include ${zulip-includes}/tornado_cors_headers; + add_header Allow 'OPTIONS, GET, DELETE' always; + return 204; + } + + if ($request_method !~ ^(GET|DELETE)$ ) { + include ${zulip-includes}/headers; + add_header Allow 'OPTIONS, GET, DELETE' always; + return 405; + } + + include ${zulip-includes}/tornado_cors_headers; + proxy_pass $tornado_server; + include ${zulip-includes}/proxy_longpolling; + ''; + }; + "~ ^/internal/tornado/(\\d+)(/.*)$" = { + extraConfig = '' + internal; + proxy_pass http://tornado$1$2$is_args$args; + include ${zulip-includes}/proxy_longpolling; + ''; + }; + "/" = { + extraConfig = '' + include ${zulip-includes}/uwsgi_params; + ''; + }; + "/thumbnail" = { + extraConfig = '' + include ${zulip-includes}/api_headers; + include ${zulip-includes}/uwsgi_params; + ''; + }; + "/avatar" = { + extraConfig = '' + include ${zulip-includes}/api_headers; + include ${zulip-includes}/uwsgi_params; + ''; + }; + "/user_uploads" = { + extraConfig = '' + include ${zulip-includes}/api_headers; + include ${zulip-includes}/uwsgi_params; + ''; + }; + "/api/" = { + extraConfig = '' + include ${zulip-includes}/api_headers; + include ${zulip-includes}/uwsgi_params; + ''; + }; + # uploads-internal + "~ ^/internal/s3/(?[^/\\s]+)/(?[^\\s]*)" = { + extraConfig = '' + internal; + include ${zulip-includes}/headers; + add_header Content-Security-Policy "default-src 'none'; media-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self'; object-src 'self'; plugin-types application/pdf;"; + + # The components of this path are originally double-URI-escaped + # (see zerver/view/upload.py). "location" matches are on + # unescaped values, which fills $s3_path with a properly + # single-escaped path to pass to the upstream server. + # (see associated commit message for more details) + set $download_url https://$s3_hostname/$s3_path; + proxy_set_header Host $s3_hostname; + proxy_ssl_name $s3_hostname; + proxy_ssl_server_name on; + + # Strip off X-amz-cf-id header, which otherwise the request has to + # have been signed over, leading to signature mismatches. + proxy_set_header x-amz-cf-id ""; + + # Strip off any auth request headers which the Zulip client might + # have sent, as they will not work for S3, and will report an error due + # to the signed auth header we also provide. + proxy_set_header Authorization ""; + proxy_set_header x-amz-security-token ""; + + # These headers are only valid if there is a body, but better to + # strip them to be safe. + proxy_set_header Content-Length ""; + proxy_set_header Content-Type ""; + proxy_set_header Content-MD5 ""; + proxy_set_header x-amz-content-sha256 ""; + proxy_set_header Expect ""; + + # Ensure that we only get _one_ of these response headers: the one + # that Django added, not the one from S3. + proxy_hide_header Cache-Control; + proxy_hide_header Expires; + proxy_hide_header Set-Cookie; + # We are _leaving_ S3 to provide Content-Type, + # Content-Disposition, and Accept-Ranges headers, which are the + # three remaining headers which nginx would also pass through from + # the first response. Django explicitly unsets the first, and + # does not set the latter two. + + proxy_pass $download_url$is_args$args; + proxy_cache uploads; + # If the S3 response doesn't contain Cache-Control headers (which + # we don't expect it to) then we assume they are valid for a very + # long time. The size of the cache is controlled by + # `s3_disk_cache_size` and read frequency, set via + # `s3_cache_inactive_time`. + proxy_cache_valid 200 1y; + + # We only include the requested content-disposition in the cache + # key, so that we cache "Content-Disposition: attachment" + # separately from the inline version. + proxy_cache_key $download_url$s3_disposition_cache_key; + ''; + }; + "/internal/local/uploads/" = { + extraConfig = '' + internal; + include ${zulip-includes}/headers; + add_header Content-Security-Policy "default-src 'none'; media-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self'; object-src 'self'; plugin-types application/pdf;"; + + # Django handles setting Content-Type, Content-Disposition, and Cache-Control. + + alias ${home}/uploads/files/; + ''; + }; + "/internal/local/user_avatars/" = { + extraConfig = '' + internal; + include ${zulip-includes}/headers; + add_header Content-Security-Policy "default-src 'none' img-src 'self'"; + include ${zulip-includes}/uploads.types; + alias ${home}/uploads/avatars/; + ''; + }; + # external_sso + "/accounts/login/sso/" = { + extraConfig = '' + proxy_pass https://localhost_sso; + include ${zulip-includes}/proxy; + ''; + }; + # camo + "/external_content/metrics" = { + extraConfig = '' + return 404; + ''; + }; + "/external_content/" = { + extraConfig = '' + rewrite /external_content/(.*) /$1 break; + proxy_pass http://camo; + include ${zulip-includes}/proxy; + ''; + }; + + "/health/" = { + extraConfig = lib.concatStringsSep "\n" ( + map (host: "accept ${host};") loadbalancers ++ [ "deny all;" ] + ); + }; + }; + }; + }; +} diff --git a/machines/kat-son/zulip/postgres.nix b/machines/kat-son/zulip/postgres.nix new file mode 100644 index 0000000..49506d4 --- /dev/null +++ b/machines/kat-son/zulip/postgres.nix @@ -0,0 +1,27 @@ +{ ... }: +{ + # postgresql_common.pp + users.users.postgres.extraGroups = [ "zulip" ]; + # process_fts_updates.pp + systemd.services.process-fts-updates = { + script = ''''; # TODO : puppet/zulip/files/postgresql/process_fts_updates + serviceConfig = { + User = "zulip"; + Group = "zulip"; + }; + }; + # postgresql_base.pp + services.nagios.plugins = [ + # TODO : puppet/zulip/files/nagios_plugins/zulip_postgresql + ]; + services.postgresql = { + enable = true; + ensureUsers = [ + { + name = "zulip"; + ensureDBOwnership = true; + } + ]; + ensureDatabases = [ "zulip" ]; + }; +}