From 16069b795002971e403fe657639001d60b668029 Mon Sep 17 00:00:00 2001 From: juvdiaz Date: Mon, 25 May 2026 14:36:44 -0600 Subject: [PATCH] Harden app and registry workloads --- .../registry-deployment.yaml | 16 ++++++++ apps/demos-static/Dockerfile | 4 +- apps/demos-static/nginx.conf | 2 +- apps/demos-static/web-app.yaml | 30 ++++++++++++++- apps/website/Dockerfile | 20 ++++++++-- apps/website/lang_helper.php | 22 ++++++++--- apps/website/save_lang.php | 15 ++++++-- apps/website/web-app.yaml | 37 ++++++++++++++++++- 8 files changed, 127 insertions(+), 19 deletions(-) diff --git a/apps/container-registry/registry-deployment.yaml b/apps/container-registry/registry-deployment.yaml index b36ff59..41589ee 100644 --- a/apps/container-registry/registry-deployment.yaml +++ b/apps/container-registry/registry-deployment.yaml @@ -24,9 +24,21 @@ spec: operator: In values: - debian + securityContext: + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 + fsGroupChangePolicy: OnRootMismatch containers: - name: registry image: registry:2 + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL ports: - containerPort: 5000 name: http @@ -51,10 +63,14 @@ spec: volumeMounts: - name: registry-vol mountPath: /var/lib/registry + - name: tmp + mountPath: /tmp volumes: - name: registry-vol persistentVolumeClaim: claimName: registry-pvc + - name: tmp + emptyDir: {} --- apiVersion: v1 kind: Service diff --git a/apps/demos-static/Dockerfile b/apps/demos-static/Dockerfile index 4372ba7..0847580 100644 --- a/apps/demos-static/Dockerfile +++ b/apps/demos-static/Dockerfile @@ -3,4 +3,6 @@ FROM nginx:1.27-alpine COPY nginx.conf /etc/nginx/conf.d/default.conf COPY public/ /usr/share/nginx/html/ -EXPOSE 80 +USER nginx + +EXPOSE 8080 diff --git a/apps/demos-static/nginx.conf b/apps/demos-static/nginx.conf index 484b717..e2c6db1 100644 --- a/apps/demos-static/nginx.conf +++ b/apps/demos-static/nginx.conf @@ -1,5 +1,5 @@ server { - listen 80; + listen 8080; server_name _; root /usr/share/nginx/html; index index.html; diff --git a/apps/demos-static/web-app.yaml b/apps/demos-static/web-app.yaml index 4f6205e..e82c0d9 100644 --- a/apps/demos-static/web-app.yaml +++ b/apps/demos-static/web-app.yaml @@ -17,12 +17,24 @@ spec: spec: nodeSelector: kubernetes.io/hostname: raspberry + securityContext: + runAsNonRoot: true + runAsUser: 101 + runAsGroup: 101 + fsGroup: 101 + fsGroupChangePolicy: OnRootMismatch containers: - name: demos-static image: 192.168.100.68:30500/demos-static:latest imagePullPolicy: Always + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL ports: - - containerPort: 80 + - containerPort: 8080 name: http readinessProbe: httpGet: @@ -42,6 +54,20 @@ spec: memory: 32Mi limits: memory: 128Mi + volumeMounts: + - name: nginx-cache + mountPath: /var/cache/nginx + - name: nginx-run + mountPath: /var/run + - name: tmp + mountPath: /tmp + volumes: + - name: nginx-cache + emptyDir: {} + - name: nginx-run + emptyDir: {} + - name: tmp + emptyDir: {} --- apiVersion: v1 kind: Service @@ -53,7 +79,7 @@ spec: externalTrafficPolicy: Local ports: - port: 80 - targetPort: 80 + targetPort: http nodePort: 30081 selector: app: demos-static diff --git a/apps/website/Dockerfile b/apps/website/Dockerfile index 99bf3d4..bf85ca4 100644 --- a/apps/website/Dockerfile +++ b/apps/website/Dockerfile @@ -17,7 +17,15 @@ RUN ln -sf /usr/bin/php82 /usr/bin/php # Alpine keeps Apache site configs here instead of a2enmod RUN sed -i 's/#LoadModule rewrite_module/LoadModule rewrite_module/' /etc/apache2/httpd.conf && \ sed -i 's/#LoadModule headers_module/LoadModule headers_module/' /etc/apache2/httpd.conf && \ - sed -i 's/DirectoryIndex index.html/DirectoryIndex index.php index.html/' /etc/apache2/httpd.conf + sed -i 's/DirectoryIndex index.html/DirectoryIndex index.php index.html/' /etc/apache2/httpd.conf && \ + sed -i 's/^Listen 80$/Listen 8080/' /etc/apache2/httpd.conf && \ + sed -i 's#^ErrorLog .*#ErrorLog /proc/self/fd/2#' /etc/apache2/httpd.conf && \ + sed -i 's#^CustomLog .*#CustomLog /proc/self/fd/1 combined#' /etc/apache2/httpd.conf && \ + if grep -q '^PidFile ' /etc/apache2/httpd.conf; then \ + sed -i 's#^PidFile .*#PidFile /tmp/httpd.pid#' /etc/apache2/httpd.conf; \ + else \ + printf '\nPidFile /tmp/httpd.pid\n' >> /etc/apache2/httpd.conf; \ + fi # Copy files directly into Alpine's default web root COPY . /var/www/localhost/htdocs/ @@ -31,9 +39,15 @@ RUN mkdir -p /var/www/localhost/htdocs/db && \ # Match local user permissions for the runtime user (Alpine uses 'apache' instead of 'www-data') RUN usermod -u 1000 apache && \ - groupmod -g 1000 apache + groupmod -g 1000 apache && \ + mkdir -p /run/apache2 /var/log/apache2 /tmp/website-lang && \ + chown -R apache:apache /run/apache2 /var/log/apache2 /tmp/website-lang /var/www/localhost/htdocs/db -EXPOSE 80 +ENV WEBSITE_LANG_WRITE_DIR=/tmp/website-lang + +USER apache + +EXPOSE 8080 # Start Apache in the foreground CMD ["/usr/sbin/httpd", "-D", "FOREGROUND"] diff --git a/apps/website/lang_helper.php b/apps/website/lang_helper.php index 43d45a8..fa93acc 100644 --- a/apps/website/lang_helper.php +++ b/apps/website/lang_helper.php @@ -3,10 +3,17 @@ // Include this at the top of every page. // Provides: $lang, $text, $en, $availableLangs -$availableLangs = array_map( +$staticLangDir = __DIR__ . '/lang'; +$runtimeLangDir = getenv('WEBSITE_LANG_WRITE_DIR') ?: null; +$langFiles = glob($staticLangDir . '/*.php') ?: []; +if ($runtimeLangDir && is_dir($runtimeLangDir)) { + $langFiles = array_merge($langFiles, glob($runtimeLangDir . '/*.php') ?: []); +} + +$availableLangs = array_values(array_unique(array_map( fn($f) => basename($f, '.php'), - glob(__DIR__ . '/lang/*.php') -); + $langFiles +))); function getLang($supported) { if (isset($_GET['lang']) && in_array($_GET['lang'], $supported)) { @@ -18,12 +25,15 @@ function getLang($supported) { $lang = getLang($availableLangs); -$file = __DIR__ . "/lang/$lang.php"; +$file = $runtimeLangDir ? "$runtimeLangDir/$lang.php" : ''; +if (!$file || !file_exists($file)) { + $file = "$staticLangDir/$lang.php"; +} if (!file_exists($file)) { $lang = 'nah'; - $file = __DIR__ . "/lang/nah.php"; + $file = "$staticLangDir/nah.php"; } // Always load English as translation source -$en = include __DIR__ . '/lang/en.php'; +$en = include "$staticLangDir/en.php"; $text = array_replace($en, include $file); diff --git a/apps/website/save_lang.php b/apps/website/save_lang.php index 208d58d..7b87d56 100644 --- a/apps/website/save_lang.php +++ b/apps/website/save_lang.php @@ -48,11 +48,18 @@ foreach ($base as $key => $value) { $lines[] = "];"; $content = implode("\n", $lines) . "\n"; -$path = __DIR__ . "/lang/$lang.php"; -if (file_put_contents($path, $content) === false) { +$langDir = getenv('WEBSITE_LANG_WRITE_DIR') ?: (__DIR__ . '/lang'); +if (!is_dir($langDir) && !mkdir($langDir, 0755, true)) { http_response_code(500); - echo json_encode(['error' => 'Could not write file — check permissions on lang/']); + echo json_encode(['error' => 'Could not create language directory']); exit; } -echo json_encode(['success' => true, 'lang' => $lang, 'path' => "lang/$lang.php"]); +$path = "$langDir/$lang.php"; +if (file_put_contents($path, $content) === false) { + http_response_code(500); + echo json_encode(['error' => 'Could not write language file']); + exit; +} + +echo json_encode(['success' => true, 'lang' => $lang]); diff --git a/apps/website/web-app.yaml b/apps/website/web-app.yaml index 0182f44..2a07f36 100644 --- a/apps/website/web-app.yaml +++ b/apps/website/web-app.yaml @@ -22,6 +22,12 @@ spec: spec: nodeSelector: kubernetes.io/hostname: raspberry + securityContext: + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 + fsGroupChangePolicy: OnRootMismatch affinity: podAntiAffinity: preferredDuringSchedulingIgnoredDuringExecution: # requiredDuringSchedulingIgnoredDuringExecution: @@ -38,8 +44,17 @@ spec: - name: php-app image: 192.168.100.68:30500/php-website:latest imagePullPolicy: Always + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + env: + - name: WEBSITE_LANG_WRITE_DIR + value: /tmp/website-lang ports: - - containerPort: 80 + - containerPort: 8080 name: http readinessProbe: httpGet: @@ -59,6 +74,24 @@ spec: memory: 64Mi limits: memory: 256Mi + volumeMounts: + - name: apache-run + mountPath: /run/apache2 + - name: apache-logs + mountPath: /var/log/apache2 + - name: website-db + mountPath: /var/www/localhost/htdocs/db + - name: tmp + mountPath: /tmp + volumes: + - name: apache-run + emptyDir: {} + - name: apache-logs + emptyDir: {} + - name: website-db + emptyDir: {} + - name: tmp + emptyDir: {} --- apiVersion: v1 kind: Service @@ -70,7 +103,7 @@ spec: externalTrafficPolicy: Local ports: - port: 80 - targetPort: 80 + targetPort: http nodePort: 30080 selector: app: php-website