diff --git a/docker/.gitignore b/docker/.gitignore new file mode 100644 index 00000000..6320cd24 --- /dev/null +++ b/docker/.gitignore @@ -0,0 +1 @@ +data \ No newline at end of file diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 00000000..8d3eac4a --- /dev/null +++ b/docker/README.md @@ -0,0 +1,11 @@ + +# Docker deployment + +Read [README.md](/README.md) first. +Don't forget to adapt the app ID and signature as those can only be changed by rebuilding the container. +Also you need to clone submodules as the dockerfile requires them: `git submodule update --init` + +Adapt: +- [AttestationProtocol.java:154-162](/src/main/java/app/attestation/server/AttestationProtocol.java#L154-L162) to your app ID and signature +- [AttestationServer.java:85-86](/src/main/java/app/attestation/server/AttestationServer.java#L85-L86) to your domain and protocol +- [AttestationServer.java:320](/src/main/java/app/attestation/server/AttestationServer.java#L320) to "0.0.0.0", or enable IPv6 support in docker \ No newline at end of file diff --git a/docker/attestation-server.dockerfile b/docker/attestation-server.dockerfile new file mode 100644 index 00000000..29c6f898 --- /dev/null +++ b/docker/attestation-server.dockerfile @@ -0,0 +1,19 @@ +FROM archlinux:base-20240101.0.204074 + +WORKDIR /app +RUN pacman --noconfirm -Sy jdk-openjdk + +COPY . /app +RUN ./gradlew build + +FROM fedora:39 + +RUN dnf -y update +RUN dnf -y install java-latest-openjdk-headless + +WORKDIR /data +RUN mkdir /app +COPY --from=0 /app/build/libs/ /app +COPY --from=0 /app/libs/sqlite4java-prebuilt/ /usr/lib + +CMD [ "/usr/bin/java", "-cp", "/app/*", "app.attestation.server.AttestationServer" ] \ No newline at end of file diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 00000000..b3ae8ece --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,17 @@ +version: '3' + +services: + attestation-server: + build: + context: .. + dockerfile: docker/attestation-server.dockerfile + container_name: attestation-server + volumes: + - ./data:/data + attestation-proxy: + build: + context: .. + dockerfile: docker/nginx-server.dockerfile + container_name: nginx + ports: + - 5000:80 \ No newline at end of file diff --git a/docker/nginx-server.dockerfile b/docker/nginx-server.dockerfile new file mode 100644 index 00000000..5f4422d3 --- /dev/null +++ b/docker/nginx-server.dockerfile @@ -0,0 +1,37 @@ +# Copying same dockerfile content, as they are built only once +FROM archlinux:base-20240101.0.204074 + +# Installing dev dependencies +# moreutils is required for packages: sponge +RUN pacman --noconfirm -Sy jdk-openjdk zopfli parallel yajl brotli nginx-mod-brotli python3 python-pip nodejs npm libxml2 moreutils + +ENV GITHUB_ACTIONS="true" +ENV PATH="/opt/venv/bin:$PATH" +ENV SKIP_REMOTE_PUBLISHING="1" + + +WORKDIR /app +COPY . /app + +RUN npm i +RUN python -m venv /opt/venv +RUN pip install -r requirements.txt + +RUN ./process-static + +FROM archlinux:base-20240101.0.204074 + +RUN pacman --noconfirm -Sy nginx nginx-mod-brotli + +COPY --from=0 /app/nginx-tmp/nginx.conf /etc/nginx/ +COPY --from=0 /app/nginx-tmp/mime.types /etc/nginx/ +COPY --from=0 /app/nginx-tmp/root_attestation.app.conf /etc/nginx/ +COPY --from=0 /app/nginx-tmp/snippets /etc/nginx/snippets +COPY --from=0 /app/static-tmp /srv/attestation.app_a +COPY ./docker/nginx/nginx.conf /etc/nginx/nginx.conf + +RUN mkdir -p /etc/nginx/modules/ +RUN ln -s /usr/lib/nginx/modules/ngx_http_brotli_filter_module.so /etc/nginx/modules/ngx_http_brotli_filter_module.so +RUN ln -s /usr/lib/nginx/modules/ngx_http_brotli_static_module.so /etc/nginx/modules/ngx_http_brotli_static_module.so + +CMD [ "nginx", "-g", "daemon off;" ] \ No newline at end of file diff --git a/docker/nginx/nginx.conf b/docker/nginx/nginx.conf new file mode 100644 index 00000000..c41a7c7c --- /dev/null +++ b/docker/nginx/nginx.conf @@ -0,0 +1,341 @@ +# nginx 1.24.x + +load_module modules/ngx_http_brotli_filter_module.so; +load_module modules/ngx_http_brotli_static_module.so; + +error_log syslog:server=unix:/dev/log,nohostname; +# leave stderr open but minimize duplicate logging to it +error_log stderr emerg; + +worker_processes auto; +worker_rlimit_nofile 16384; + +events { + worker_connections 4096; +} + +http { + root /var/empty; + + include mime.types; + default_type application/octet-stream; + + charset utf-8; + charset_types text/css text/javascript text/plain text/xml application/atom+xml; + + sendfile on; + sendfile_max_chunk 256k; + tcp_nopush on; + keepalive_requests 256; + keepalive_timeout 3m; + server_tokens off; + msie_padding off; + + client_max_body_size 1k; + client_body_buffer_size 1k; + client_header_buffer_size 1k; + large_client_header_buffers 2 1k; + http2_chunk_size 4k; + + client_body_timeout 15s; + client_header_timeout 15s; + send_timeout 30s; + + max_ranges 1; + + resolver [::1]; + resolver_timeout 15s; + + proxy_connect_timeout 5s; + proxy_read_timeout 15s; + proxy_send_timeout 15s; + + map $request_method $post_binary_remote_addr { + POST $binary_remote_addr; + } + + http2_max_concurrent_streams 16; + limit_conn_status 429; + limit_conn_zone $binary_remote_addr zone=http-limit:10m; + limit_conn http-limit 128; + limit_req_status 429; + limit_req_zone $post_binary_remote_addr zone=backend-limit:10m rate=256r/s; + limit_req_zone $post_binary_remote_addr zone=sample-limit:10m rate=6r/m; + limit_req_zone $post_binary_remote_addr zone=auth-limit:10m rate=1r/s; + + log_format main '$connection-$connection_requests $remote_addr $remote_user $ssl_protocol $server_protocol ' + '$host $request_method "$request_uri" $status $request_length $body_bytes_sent/$bytes_sent ' + '$request_time $upstream_connect_time/$upstream_header_time/$upstream_response_time ' + '$upstream_cache_status "$http_referer" "$http_user_agent"'; + + # access_log syslog:server=unix:/dev/log,nohostname main; + # Piping to docker + access_log /dev/stdout; + + log_subrequest on; + log_not_found off; + + gzip_proxied any; + gzip_vary on; + + if_modified_since before; + + aio threads; + aio_write on; + + upstream backend { + zone backend 32k; + server attestation-server:8080 max_conns=1024 max_fails=0; + } + + map $uri $preload_resources_uri { + /index.html ", <{{path|/monitoring.js}}>; rel=modulepreload; integrity={{integrity|/monitoring.js}}"; + } + + server { + listen 80 default_server backlog=4096; + listen [::]:80 default_server backlog=4096; + include root_attestation.app.conf; + + keepalive_timeout 0; + + # https://trac.nginx.org/nginx/ticket/2012 + # location / { + # return 404; + # } + + error_page 403 =404 /404; + error_page 404 405 /404; + + open_file_cache max=2048 inactive=1d; + open_file_cache_valid 1d; + + include snippets/security-headers.conf; + add_header Cross-Origin-Resource-Policy "same-origin" always; + gzip_static on; + brotli_static on; + + if ($request_uri ~ ^[^?]+//) { + rewrite ^(.*)$ $1 permanent; + } + + location = /security.txt { + return 301 /.well-known/security.txt; + } + + location = /bitcoin-address.png { + return 301 /donate-bitcoin.png; + } + + location = /bitcoin-donation.png { + return 301 /donate-bitcoin.png; + } + + location = /monero-donation.png { + return 301 /donate-monero.png; + } + + location = /ic_launcher-web.png { + return 301 /opengraph.png; + } + + location = /privacy_policy { + return 301 /privacy-policy; + } + + location = /404 { + internal; + include snippets/security-headers.conf; + add_header Cross-Origin-Resource-Policy "same-origin" always; + include snippets/preload.conf; + try_files $uri.html =404; + } + + location = /favicon.ico { + if ($http_accept ~ "image/svg\+xml") { + rewrite ^ /favicon.svg last; + } + include snippets/security-headers.conf; + # avoid breaking image hotlinking such as https://github.com/TryGhost/Ghost/issues/12880 + add_header Cross-Origin-Resource-Policy "cross-origin" always; + add_header Cache-Control "public, max-age=604800"; + } + + location = /favicon.svg { + include snippets/security-headers.conf; + # avoid breaking image hotlinking such as https://github.com/TryGhost/Ghost/issues/12880 + add_header Cross-Origin-Resource-Policy "cross-origin" always; + add_header Cache-Control "public, max-age=604800"; + } + + location = /.well-known/traffic-advice { + default_type application/trafficadvice+json; + } + + location = /placeholder.gif { + include snippets/security-headers.conf; + add_header Cross-Origin-Resource-Policy "same-origin" always; + add_header Cache-Control "public, max-age=31536000, immutable"; + gzip_static off; + brotli_static off; + } + + location = /submit { + if ($request_method != POST) { + return 405; + } + client_max_body_size 64k; + client_body_buffer_size 16k; + proxy_pass http://backend; + limit_req zone=backend-limit burst=32 nodelay; + limit_req zone=sample-limit burst=10 nodelay; + max_ranges 0; + } + + location = /challenge { + if ($request_method != POST) { + return 405; + } + proxy_pass http://backend; + limit_req zone=backend-limit burst=32 nodelay; + max_ranges 0; + } + + location = /verify { + if ($request_method != POST) { + return 405; + } + client_max_body_size 4k; + client_body_buffer_size 4k; + proxy_pass http://backend; + limit_req zone=backend-limit burst=32 nodelay; + max_ranges 0; + } + + location = /api/create-account { + if ($request_method != POST) { + return 405; + } + proxy_pass http://backend; + limit_req zone=backend-limit burst=32 nodelay; + limit_req zone=auth-limit burst=10 nodelay; + max_ranges 0; + } + + location = /api/login { + if ($request_method != POST) { + return 405; + } + proxy_pass http://backend; + limit_req zone=backend-limit burst=32 nodelay; + limit_req zone=auth-limit burst=10 nodelay; + max_ranges 0; + } + + location ^~ /api/ { + if ($request_method != POST) { + return 405; + } + gzip on; + gzip_types application/json; + brotli on; + brotli_comp_level 1; + brotli_types application/json; + proxy_pass http://backend; + limit_req zone=backend-limit burst=32 nodelay; + max_ranges 0; + } + + location ^~ /fonts/ { + include snippets/security-headers.conf; + add_header Cross-Origin-Resource-Policy "same-origin" always; + add_header Cache-Control "public, max-age=31536000, immutable"; + gzip_static off; + brotli_static off; + } + + location ~ "/$" { + include snippets/security-headers.conf; + add_header Cross-Origin-Resource-Policy "same-origin" always; + add_header Cache-Control "public, no-cache"; + include snippets/preload.conf; + try_files ${uri}index.html @noslash; + } + + # redirect /path/ to /path if /path.html exists + location @noslash { + rewrite ^(.*)/$ $1; + if (-f $request_filename.html) { + rewrite ^(.*) $1 permanent; + } + return 404; + } + + location ~ "\.(?:css|js)$" { + include snippets/security-headers.conf; + add_header Cross-Origin-Resource-Policy "same-origin" always; + add_header Cache-Control "public, max-age=31536000, immutable"; + } + + location ~ "\.svg$" { + include snippets/security-headers.conf; + # avoid breaking image hotlinking such as https://github.com/TryGhost/Ghost/issues/12880 + add_header Cross-Origin-Resource-Policy "cross-origin" always; + add_header Cache-Control "public, max-age=31536000"; + } + + location ~ "\.webmanifest$" { + include snippets/security-headers.conf; + add_header Cross-Origin-Resource-Policy "same-origin" always; + add_header Cache-Control "public, max-age=604800"; + } + + location ~ "\.png$" { + include snippets/security-headers.conf; + # avoid breaking image hotlinking such as https://github.com/TryGhost/Ghost/issues/12880 + add_header Cross-Origin-Resource-Policy "cross-origin" always; + add_header Cache-Control "public, max-age=31536000"; + gzip_static off; + brotli_static off; + } + + location ~ "\.(?:json|txt|xml)$" { + include snippets/security-headers.conf; + add_header Cross-Origin-Resource-Policy "same-origin" always; + add_header Cache-Control "public, max-age=1800"; + } + + # https://www.twipu.com/GrapheneOS doesn't handle links with fragments properly + location ~ "^/([^\s]*)