diff --git a/Containerfile b/Containerfile
index a7b99d3..60fe44c 100644
--- a/Containerfile
+++ b/Containerfile
@@ -1,11 +1,15 @@
FROM php:8.4.14-apache
# Enable Apache modules and custom config as root during build
-RUN a2enmod rewrite
+RUN a2enmod rewrite headers
COPY apache.conf /etc/apache2/conf-available/custom.conf
RUN a2enconf custom
+# Override default security.conf settings
+RUN sed -i 's/^ServerTokens OS/ServerTokens Prod/' /etc/apache2/conf-available/security.conf \
+ && sed -i 's/^ServerSignature On/ServerSignature Off/' /etc/apache2/conf-available/security.conf
+
# Log to /proc/self/fd for container output
RUN sed -i 's|ErrorLog.*|ErrorLog /proc/self/fd/2|' /etc/apache2/sites-available/000-default.conf \
&& sed -i 's|CustomLog.*|CustomLog /proc/self/fd/1 combined|' /etc/apache2/sites-available/000-default.conf \
diff --git a/apache.conf b/apache.conf
index d8adda7..7bf87bb 100644
--- a/apache.conf
+++ b/apache.conf
@@ -1,5 +1,13 @@
+# Minimize server version disclosure
+ServerTokens Prod
+
+# Disable PHP version header and error display
+php_flag expose_php Off
+php_flag display_errors Off
+php_flag log_errors On
+
- Options Indexes FollowSymLinks
+ Options FollowSymLinks
AllowOverride All
Require all granted
diff --git a/content/.htaccess b/content/.htaccess
index a788fe7..379c049 100644
--- a/content/.htaccess
+++ b/content/.htaccess
@@ -1,9 +1,31 @@
DirectorySlash Off
+# Block direct access to content source files
+
+ # Allow only the entry point
+
+ Require all denied
+
+
+
+# Security headers
+
+ Header set X-Content-Type-Options "nosniff"
+ Header set X-Frame-Options "DENY"
+ Header set Referrer-Policy "strict-origin-when-cross-origin"
+ Header set Permissions-Policy "camera=(), microphone=(), geolocation=()"
+ Header unset X-Powered-By
+ Header always unset X-Powered-By
+
+
+# Restrict HTTP methods to GET, POST, HEAD
RewriteEngine On
RewriteBase /
+ RewriteCond %{REQUEST_METHOD} !^(GET|POST|HEAD)$ [NC]
+ RewriteRule .* - [F,L]
+
# Route /app requests to index.php
RewriteCond %{REQUEST_URI} ^/app/
RewriteRule ^(.*)$ /index.php [L,QSA]
diff --git a/content/.htaccess.base b/content/.htaccess.base
index 5bbdf2e..ffeacd3 100644
--- a/content/.htaccess.base
+++ b/content/.htaccess.base
@@ -1,9 +1,31 @@
DirectorySlash Off
+# Block direct access to content source files
+
+ # Allow only the entry point
+
+ Require all denied
+
+
+
+# Security headers
+
+ Header set X-Content-Type-Options "nosniff"
+ Header set X-Frame-Options "DENY"
+ Header set Referrer-Policy "strict-origin-when-cross-origin"
+ Header set Permissions-Policy "camera=(), microphone=(), geolocation=()"
+ Header unset X-Powered-By
+ Header always unset X-Powered-By
+
+
+# Restrict HTTP methods to GET, POST, HEAD
RewriteEngine On
RewriteBase /
+ RewriteCond %{REQUEST_METHOD} !^(GET|POST|HEAD)$ [NC]
+ RewriteRule .* - [F,L]
+
# Route /app requests to index.php
RewriteCond %{REQUEST_URI} ^/app/
RewriteRule ^(.*)$ /index.php [L,QSA]
diff --git a/content/index.php b/content/index.php
index 8288ad2..a5c1763 100644
--- a/content/index.php
+++ b/content/index.php
@@ -5,5 +5,12 @@ if (str_starts_with($_SERVER['REQUEST_URI'], '/app/')) {
exit;
}
+// Harden session cookie before any session starts
+ini_set('session.cookie_httponly', '1');
+ini_set('session.cookie_samesite', 'Lax');
+if (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') {
+ ini_set('session.cookie_secure', '1');
+}
+
// All other requests go to router
require __DIR__ . '/../app/router.php';
diff --git a/docs/security-cpanel.md b/docs/security-cpanel.md
new file mode 100644
index 0000000..fc71801
--- /dev/null
+++ b/docs/security-cpanel.md
@@ -0,0 +1,56 @@
+# Security Hardening — cPanel Shared Hosting
+
+The container dev environment (Containerfile + apache.conf) handles most hardening automatically. On cPanel shared hosting, some settings must be configured manually since you don't control the Apache or PHP config directly.
+
+## What's handled by .htaccess (works everywhere)
+
+These are applied automatically via `content/.htaccess` (synced from `.htaccess.base`):
+
+- Block direct access to `.ini`, `.md`, `.html`, `.php` content files
+- Security headers: `X-Content-Type-Options`, `X-Frame-Options`, `Referrer-Policy`, `Permissions-Policy`
+- Strip `X-Powered-By` header
+- Restrict HTTP methods to GET/POST/HEAD
+- Rewrite rules routing all requests through `index.php`
+
+The `custom/.htaccess` and `custom/data/.htaccess` files also deploy automatically and block direct access to config files and data.
+
+## What needs manual cPanel configuration
+
+### 1. Disable display_errors
+
+Go to **MultiPHP INI Editor** (Home > Software > MultiPHP INI Editor):
+
+- Select the domain
+- Set `display_errors` = **Off**
+- Set `log_errors` = **On**
+- Set `expose_php` = **Off**
+
+This prevents PHP errors from leaking server paths and internal details to visitors.
+
+### 2. PHP version
+
+Use **MultiPHP Manager** to ensure PHP 8.4+ is selected for the domain.
+
+### 3. Session cookie hardening
+
+Handled in `content/index.php` via `ini_set()` calls — no cPanel action needed. The entry point sets `HttpOnly`, `SameSite=Lax`, and `Secure` (when on HTTPS) before any session starts.
+
+### 4. Server version header
+
+On shared hosting you typically cannot change `ServerTokens` (it's a server-level directive). The `X-Powered-By` header is stripped by `.htaccess`, but the `Server: Apache/2.4.x` header may still show the full version. This is a low-risk issue on shared hosting since the Apache version is the hosting provider's responsibility.
+
+### 5. SSL/TLS
+
+Use **SSL/TLS** (Home > Security > SSL/TLS) or **AutoSSL** to ensure HTTPS is active. The session cookie `Secure` flag only activates over HTTPS.
+
+## Checklist
+
+- [ ] `.htaccess` deployed (copy `.htaccess.base` if needed, preserve cPanel-generated blocks)
+- [ ] `display_errors` = Off in MultiPHP INI Editor
+- [ ] `expose_php` = Off in MultiPHP INI Editor
+- [ ] `log_errors` = On in MultiPHP INI Editor
+- [ ] SSL certificate active
+- [ ] `custom/smtp-config.php` exists but is NOT in git (check `.gitignore`)
+- [ ] `custom/listmonk-config.php` exists but is NOT in git (check `.gitignore`)
+- [ ] `custom/data/` directory writable by web server (`chmod 755` or `775`)
+- [ ] `custom/data/.htaccess` present with `Require all denied`