Files
Thanakorn ae98dcdcdd Harden web root, secrets and realtime auth
- .htaccess: refuse .git, .env, docker, nodejs, sdlc*, scripts, archives and
  app internals; security headers, HSTS over TLS, optional HTTPS redirect
- uploads served through app/file.php to signed-in users only
- Apache/PHP hardening config for the container (ServerTokens, expose_php)
- least-privilege DB account and APP_SECRET_KEY via docker/php/provision.php;
  SMTP passwords re-encrypted with a random IV (secret_box.php)
- Socket.IO rooms from a PHP-signed token; /emit and cron refuse empty secrets
- escape notification text; CLI guards on build scripts; no fixed demo password
2026-09-24 14:53:40 +07:00

72 lines
3.0 KiB
PHP

<?php
/**
* provision.php — run by the container entrypoint before setup.php (CLI only).
*
* 1. Creates/updates the least-privilege database account the app connects as
* (DML + CREATE/INDEX on wms and wms2 only — the app creates td_stock_<id>
* tables with CREATE TABLE … LIKE; no DROP, ALTER, GRANT or other databases).
* 2. Brings an existing app/config.php (generated once, never regenerated) onto
* that account and adds APP_SECRET_KEY if it is missing.
*
* All values come from the environment and are passed to MariaDB as bound
* parameters or written with var_export(), so no password is placed on a
* command line or interpolated into SQL or PHP source.
*/
if (PHP_SAPI !== 'cli') {
http_response_code(404);
exit;
}
$root_pass = (string)getenv('DB_ROOT_PASSWORD');
$app_user = (string)getenv('DB_APP_USER');
$app_pass = (string)getenv('DB_APP_PASSWORD');
$secret = (string)getenv('APP_SECRET_KEY');
$config = (string)getenv('CONFIG');
// ── 1. Database account ──────────────────────────────────────────────────────
if ($app_user !== 'root') {
if (!preg_match('/^[A-Za-z0-9_]{1,32}$/', $app_user)) {
fwrite(STDERR, "[provision] DB_APP_USER must be letters, digits or _\n");
exit(1);
}
$pdo = new PDO('mysql:host=db', 'root', $root_pass, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
$pdo->exec('CREATE DATABASE IF NOT EXISTS `wms`');
$pdo->exec('CREATE DATABASE IF NOT EXISTS `wms2`');
$pdo->prepare("CREATE USER IF NOT EXISTS ?@'%' IDENTIFIED BY ?")->execute([$app_user, $app_pass]);
$pdo->prepare("ALTER USER ?@'%' IDENTIFIED BY ?")->execute([$app_user, $app_pass]);
foreach (['wms', 'wms2'] as $db) {
$pdo->exec("GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, INDEX, CREATE TEMPORARY TABLES, LOCK TABLES, EXECUTE"
. " ON `{$db}`.* TO " . $pdo->quote($app_user) . "@'%'");
}
echo "[provision] database account {$app_user} is ready\n";
}
// ── 2. Existing config.php ───────────────────────────────────────────────────
if ($config === '' || !is_file($config)) {
exit(0);
}
$src = file_get_contents($config);
$orig = $src;
$set = function (string $var, string $value) use (&$src) {
$src = preg_replace_callback(
'/^(\s*\$' . $var . '\s*=\s*)[^;]*;/m',
fn ($m) => $m[1] . var_export($value, true) . ';',
$src,
1
);
};
$set('db_user', $app_user);
$set('db_pass', $app_pass);
if ($secret !== '' && strpos($src, 'APP_SECRET_KEY') === false) {
$src = preg_replace('/\?>\s*$/', '', $src);
$src .= "\nif (!defined('APP_SECRET_KEY')) {\n\tdefine('APP_SECRET_KEY', " . var_export($secret, true) . ");\n}\n";
}
if ($src !== $orig) {
file_put_contents($config, $src);
echo "[provision] app/config.php updated (database account / secret key)\n";
}