Skip to content

Commit e2a3e4e

Browse files
committed
Harden Railway deployment DB secrets
1 parent 7b023ff commit e2a3e4e

7 files changed

Lines changed: 168 additions & 41 deletions

File tree

‎docs/railway-db-runtime-secrets.md‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Railway DB runtime secrets
2+
3+
Railway database credentials must be supplied through runtime environment variables, not committed PHP config or deployment scripts.
4+
5+
Dev-server Railway app config:
6+
7+
- `DB_DSN`
8+
- `DB_USERNAME`
9+
- `DB_PASSWORD`
10+
- `WALLET_DB_DSN`
11+
- `WALLET_DB_USERNAME`
12+
- `WALLET_DB_PASSWORD`
13+
- `REDIS_HOSTNAME`
14+
- `REDIS_USERNAME`
15+
- `REDIS_PASSWORD`
16+
- `REDIS_PORT`
17+
- `REDIS_DATABASE`
18+
19+
Railway deployment scripts:
20+
21+
- `MYSQL_HOST`, defaults to `mysql.railway.internal`
22+
- `MYSQL_PORT`, defaults to `3306`
23+
- `MYSQL_USER`, defaults to `root`
24+
- `MYSQL_DATABASE`, defaults to `railway`
25+
- `MYSQL_PASSWORD`, required
26+
27+
Set `MYSQL_PASSWORD` in Railway variables before running the deployment scripts. The scripts pass it to `mysql` through `MYSQL_PWD` for each command instead of embedding the password in the script or command line.

‎environments/dev-server-railway/common/config/main-local.php‎

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,16 @@
33
'components' => [
44
'db' => [
55
'class' => 'yii\db\Connection',
6-
'dsn' => 'mysql:host=mysql.railway.internal;dbname=railway',
7-
'username' => 'root',
8-
'password' => 'TpijAlObvfdvZxzPgrnMTHMxyekEqTtt',
6+
'dsn' => getenv('DB_DSN') ?: 'mysql:host=mysql.railway.internal;dbname=railway',
7+
'username' => getenv('DB_USERNAME') ?: 'root',
8+
'password' => getenv('DB_PASSWORD') ?: '',
99
'charset' => 'utf8mb4',
1010
],
1111
'walletDb' => [
1212
'class' => 'yii\db\Connection',
13-
'dsn' => 'mysql:host=mysql-5abl.railway.internal;dbname=railway',
14-
'username' => 'root',
15-
'password' => 'hOCpxbVoSIbPUnuuBmaQGILPshVyRRuj',
13+
'dsn' => getenv('WALLET_DB_DSN') ?: 'mysql:host=mysql-5abl.railway.internal;dbname=railway',
14+
'username' => getenv('WALLET_DB_USERNAME') ?: 'root',
15+
'password' => getenv('WALLET_DB_PASSWORD') ?: '',
1616
'charset' => 'utf8',
1717
],
1818
//todo: replace with wallet from sandbox
@@ -39,11 +39,11 @@
3939
],
4040
'redis' => [
4141
'class' => 'yii\redis\Connection',
42-
'hostname' => 'redis.railway.internal',
43-
'username' => 'default',
44-
'password' => 'nySjmLVspFXlYOzKrOFQcRwuUprjyDli',
45-
'port' => 6379,
46-
'database' => 0,
42+
'hostname' => getenv('REDIS_HOSTNAME') ?: 'redis.railway.internal',
43+
'username' => getenv('REDIS_USERNAME') ?: 'default',
44+
'password' => getenv('REDIS_PASSWORD') ?: null,
45+
'port' => (int)(getenv('REDIS_PORT') ?: 6379),
46+
'database' => (int)(getenv('REDIS_DATABASE') ?: 0),
4747
],
4848
'cache' => [
4949
//'class' => 'yii\redis\Cache',
Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,24 @@
11
#!/bin/sh
22

3-
MYSQL_HOST="mysql.railway.internal"
4-
MYSQL_PORT=3306
5-
MYSQL_USER="root"
6-
MYSQL_PASSWORD="TpijAlObvfdvZxzPgrnMTHMxyekEqTtt"
7-
MYSQL_DATABASE="railway"
3+
MYSQL_HOST="${MYSQL_HOST:-mysql.railway.internal}"
4+
MYSQL_PORT="${MYSQL_PORT:-3306}"
5+
MYSQL_USER="${MYSQL_USER:-root}"
6+
MYSQL_DATABASE="${MYSQL_DATABASE:-railway}"
7+
: "${MYSQL_PASSWORD:?MYSQL_PASSWORD must be set in Railway environment variables.}"
8+
9+
run_mysql() {
10+
MYSQL_PWD="$MYSQL_PASSWORD" mysql -h "$MYSQL_HOST" -P "$MYSQL_PORT" -u "$MYSQL_USER" "$@"
11+
}
812

913
echo "Waiting for MySQL at $MYSQL_HOST:$MYSQL_PORT..."
1014
for attempt in $(seq 1 60); do
11-
mysql -h "$MYSQL_HOST" -P "$MYSQL_PORT" -u "$MYSQL_USER" -p"$MYSQL_PASSWORD" -e "SELECT 1" >/dev/null 2>&1 && break
15+
run_mysql -e "SELECT 1" >/dev/null 2>&1 && break
1216
echo "Attempt $attempt: retrying in 1s..."
1317
sleep 1
1418
[ "$attempt" -eq 60 ] && echo "MySQL not responding." && exit 1
1519
done
1620

17-
mysql -h "$MYSQL_HOST" -P "$MYSQL_PORT" -u "$MYSQL_USER" -p"$MYSQL_PASSWORD" "$MYSQL_DATABASE" -e "
21+
run_mysql "$MYSQL_DATABASE" -e "
1822
ALTER TABLE permission_user DROP COLUMN IF EXISTS companies;
1923
"
2024

‎environments/dev-server-railway/deployments/july_2025/3_july_2025_deployment.sh‎

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,31 @@
11
#!/bin/sh
22

3-
MYSQL_HOST="mysql.railway.internal"
4-
MYSQL_PORT=3306
5-
MYSQL_USER="root"
6-
MYSQL_PASSWORD="TpijAlObvfdvZxzPgrnMTHMxyekEqTtt"
7-
MYSQL_DATABASE="railway"
3+
MYSQL_HOST="${MYSQL_HOST:-mysql.railway.internal}"
4+
MYSQL_PORT="${MYSQL_PORT:-3306}"
5+
MYSQL_USER="${MYSQL_USER:-root}"
6+
MYSQL_DATABASE="${MYSQL_DATABASE:-railway}"
7+
: "${MYSQL_PASSWORD:?MYSQL_PASSWORD must be set in Railway environment variables.}"
8+
9+
run_mysql() {
10+
MYSQL_PWD="$MYSQL_PASSWORD" mysql -h "$MYSQL_HOST" -P "$MYSQL_PORT" -u "$MYSQL_USER" "$@"
11+
}
812

913
echo "Waiting for MySQL at $MYSQL_HOST:$MYSQL_PORT..."
1014
for attempt in $(seq 1 60); do
11-
mysql -h "$MYSQL_HOST" -P "$MYSQL_PORT" -u "$MYSQL_USER" -p"$MYSQL_PASSWORD" -e "SELECT 1" >/dev/null 2>&1 && break
15+
run_mysql -e "SELECT 1" >/dev/null 2>&1 && break
1216
echo "Attempt $attempt: retrying in 1s..."
1317
sleep 1
1418
[ "$attempt" -eq 60 ] && echo "MySQL not responding." && exit 1
1519
done
1620

1721
echo "Disabling ONLY_FULL_GROUP_BY..."
18-
mysql -h "$MYSQL_HOST" -P "$MYSQL_PORT" -u "$MYSQL_USER" -p"$MYSQL_PASSWORD" -e "
22+
run_mysql -e "
1923
SET GLOBAL sql_mode = REPLACE(@@GLOBAL.sql_mode, 'ONLY_FULL_GROUP_BY', '');
2024
SET SESSION sql_mode = REPLACE(@@SESSION.sql_mode, 'ONLY_FULL_GROUP_BY', '');
2125
"
2226

2327
echo "Converting 'candidate' table to utf8mb4 for emoji support..."
24-
mysql -h "$MYSQL_HOST" -P "$MYSQL_PORT" -u "$MYSQL_USER" -p"$MYSQL_PASSWORD" "$MYSQL_DATABASE" -e "
28+
run_mysql "$MYSQL_DATABASE" -e "
2529
ALTER TABLE candidate
2630
MODIFY candidate_intro TEXT
2731
CHARACTER SET utf8mb4
Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,24 @@
11
#!/bin/sh
22

3-
MYSQL_HOST="mysql.railway.internal"
4-
MYSQL_PORT=3306
5-
MYSQL_USER="root"
6-
MYSQL_PASSWORD="JImnisvcRDpKLdWpoMECoHHoCbutPhQC"
7-
MYSQL_DATABASE="railway"
3+
MYSQL_HOST="${MYSQL_HOST:-mysql.railway.internal}"
4+
MYSQL_PORT="${MYSQL_PORT:-3306}"
5+
MYSQL_USER="${MYSQL_USER:-root}"
6+
MYSQL_DATABASE="${MYSQL_DATABASE:-railway}"
7+
: "${MYSQL_PASSWORD:?MYSQL_PASSWORD must be set in Railway environment variables.}"
8+
9+
run_mysql() {
10+
MYSQL_PWD="$MYSQL_PASSWORD" mysql -h "$MYSQL_HOST" -P "$MYSQL_PORT" -u "$MYSQL_USER" "$@"
11+
}
812

913
echo "Waiting for MySQL at $MYSQL_HOST:$MYSQL_PORT..."
1014
for attempt in $(seq 1 60); do
11-
mysql -h "$MYSQL_HOST" -P "$MYSQL_PORT" -u "$MYSQL_USER" -p"$MYSQL_PASSWORD" -e "SELECT 1" >/dev/null 2>&1 && break
15+
run_mysql -e "SELECT 1" >/dev/null 2>&1 && break
1216
echo "Attempt $attempt: retrying in 1s..."
1317
sleep 1
1418
[ "$attempt" -eq 60 ] && echo "MySQL not responding." && exit 1
1519
done
1620

17-
mysql -h "$MYSQL_HOST" -P "$MYSQL_PORT" -u "$MYSQL_USER" -p"$MYSQL_PASSWORD" "$MYSQL_DATABASE" -e "
21+
run_mysql "$MYSQL_DATABASE" -e "
1822
ALTER TABLE permission_user DROP COLUMN IF EXISTS companies;
1923
"
2024

‎environments/prod-railway/deployments/july_2025/3_july_2025_deployment.sh‎

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,32 @@
11
#!/bin/sh
22

3-
MYSQL_HOST="mysql.railway.internal"
4-
MYSQL_PORT=3306
5-
MYSQL_USER="root"
6-
MYSQL_PASSWORD="JImnisvcRDpKLdWpoMECoHHoCbutPhQC"
7-
MYSQL_DATABASE="railway"
3+
MYSQL_HOST="${MYSQL_HOST:-mysql.railway.internal}"
4+
MYSQL_PORT="${MYSQL_PORT:-3306}"
5+
MYSQL_USER="${MYSQL_USER:-root}"
6+
MYSQL_DATABASE="${MYSQL_DATABASE:-railway}"
7+
: "${MYSQL_PASSWORD:?MYSQL_PASSWORD must be set in Railway environment variables.}"
8+
9+
run_mysql() {
10+
MYSQL_PWD="$MYSQL_PASSWORD" mysql -h "$MYSQL_HOST" -P "$MYSQL_PORT" -u "$MYSQL_USER" "$@"
11+
}
812

913
echo "Waiting for MySQL at $MYSQL_HOST:$MYSQL_PORT..."
1014
for attempt in $(seq 1 60); do
11-
mysql -h "$MYSQL_HOST" -P "$MYSQL_PORT" -u "$MYSQL_USER" -p"$MYSQL_PASSWORD" -e "SELECT 1" >/dev/null 2>&1 && break
15+
run_mysql -e "SELECT 1" >/dev/null 2>&1 && break
1216
echo "Attempt $attempt: retrying in 1s..."
1317
sleep 1
1418
[ "$attempt" -eq 60 ] && echo "MySQL not responding." && exit 1
1519
done
1620

1721
echo "Disabling ONLY_FULL_GROUP_BY..."
18-
mysql -h "$MYSQL_HOST" -P "$MYSQL_PORT" -u "$MYSQL_USER" -p"$MYSQL_PASSWORD" -e "
22+
run_mysql -e "
1923
SET GLOBAL sql_mode = REPLACE(@@GLOBAL.sql_mode, 'ONLY_FULL_GROUP_BY', '');
2024
SET SESSION sql_mode = REPLACE(@@SESSION.sql_mode, 'ONLY_FULL_GROUP_BY', '');
2125
"
2226

2327

2428
echo "Converting 'candidate' table to utf8mb4 for emoji support..."
25-
mysql -h "$MYSQL_HOST" -P "$MYSQL_PORT" -u "$MYSQL_USER" -p"$MYSQL_PASSWORD" "$MYSQL_DATABASE" -e "
29+
run_mysql "$MYSQL_DATABASE" -e "
2630
ALTER TABLE candidate
2731
MODIFY candidate_intro TEXT
2832
CHARACTER SET utf8mb4
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
"""Validate that Railway database credentials stay runtime-configured."""
2+
3+
4+
from pathlib import Path
5+
6+
7+
ROOT = Path(__file__).resolve().parents[1]
8+
9+
DEV_CONFIG = ROOT / "environments/dev-server-railway/common/config/main-local.php"
10+
DEPLOYMENT_SCRIPTS = [
11+
ROOT / "environments/dev-server-railway/deployments/july_2025/3_july_2025_deployment.sh",
12+
ROOT / "environments/dev-server-railway/deployments/july_2025/29_july_2025_deployment.sh",
13+
ROOT / "environments/prod-railway/deployments/july_2025/3_july_2025_deployment.sh",
14+
ROOT / "environments/prod-railway/deployments/july_2025/29_july_2025_deployment.sh",
15+
]
16+
DOC = ROOT / "docs/railway-db-runtime-secrets.md"
17+
18+
FORBIDDEN_SECRET_FRAGMENTS = [
19+
"TpijAlObvfdvZxzPgrnMTHMxyekEqTtt",
20+
"hOCpxbVoSIbPUnuuBmaQGILPshVyRRuj",
21+
"nySjmLVspFXlYOzKrOFQcRwuUprjyDli",
22+
"JImnisvcRDpKLdWpoMECoHHoCbutPhQC",
23+
]
24+
25+
DEV_CONFIG_ENV_VARS = [
26+
"DB_DSN",
27+
"DB_USERNAME",
28+
"DB_PASSWORD",
29+
"WALLET_DB_DSN",
30+
"WALLET_DB_USERNAME",
31+
"WALLET_DB_PASSWORD",
32+
"REDIS_HOSTNAME",
33+
"REDIS_USERNAME",
34+
"REDIS_PASSWORD",
35+
"REDIS_PORT",
36+
"REDIS_DATABASE",
37+
]
38+
39+
SCRIPT_TOKENS = [
40+
'MYSQL_HOST="${MYSQL_HOST:-mysql.railway.internal}"',
41+
'MYSQL_PORT="${MYSQL_PORT:-3306}"',
42+
'MYSQL_USER="${MYSQL_USER:-root}"',
43+
'MYSQL_DATABASE="${MYSQL_DATABASE:-railway}"',
44+
': "${MYSQL_PASSWORD:?MYSQL_PASSWORD must be set in Railway environment variables.}"',
45+
'MYSQL_PWD="$MYSQL_PASSWORD" mysql',
46+
]
47+
48+
49+
def read(path):
50+
"""Read a repository file as UTF-8 text."""
51+
return path.read_text(encoding="utf-8")
52+
53+
54+
def require(condition, message):
55+
"""Fail the check with a clear message when a condition is false."""
56+
if not condition:
57+
raise SystemExit(message)
58+
59+
60+
def main():
61+
"""Run the Railway runtime secret regression checks."""
62+
config = read(DEV_CONFIG)
63+
doc = read(DOC)
64+
script_contents = [read(path) for path in DEPLOYMENT_SCRIPTS]
65+
checked_text = "\n".join([config, doc, *script_contents])
66+
67+
for fragment in FORBIDDEN_SECRET_FRAGMENTS:
68+
require(fragment not in checked_text, f"Committed Railway secret fragment remains: {fragment}")
69+
70+
for name in DEV_CONFIG_ENV_VARS:
71+
require(f"getenv('{name}')" in config, f"Dev Railway config must use getenv('{name}').")
72+
require(f"`{name}`" in doc, f"Docs must mention `{name}`.")
73+
74+
for path, content in zip(DEPLOYMENT_SCRIPTS, script_contents):
75+
for token in SCRIPT_TOKENS:
76+
require(token in content, f"{path} must contain {token!r}.")
77+
require('-p"$MYSQL_PASSWORD"' not in content, f"{path} must not pass MYSQL_PASSWORD on the command line.")
78+
require('MYSQL_PASSWORD="' not in content, f"{path} must not assign a committed MYSQL_PASSWORD.")
79+
80+
print("Railway DB runtime secret check passed.")
81+
82+
83+
if __name__ == "__main__":
84+
main()

0 commit comments

Comments
 (0)