chore(security): add preflight checks and safe config templates

This commit is contained in:
OpenClaw Assistant 2026-03-04 13:34:05 +08:00
parent 35067c5b8b
commit 0b322aa4e1
6 changed files with 251 additions and 11 deletions

18
.env.example Normal file
View file

@ -0,0 +1,18 @@
# Star Office UI - production environment example
# Copy to .env (or your systemd/pm2 env file), then fill values.
# Mark production mode to enable startup hardening checks
STAR_OFFICE_ENV=production
# Flask/session secret (REQUIRED in production)
# Must be long/random (>=24 chars)
FLASK_SECRET_KEY=replace_with_a_long_random_secret
# Asset drawer password (REQUIRED in production)
# Do NOT use 1234 in production. Recommend >=8 chars.
ASSET_DRAWER_PASS=replace_with_strong_drawer_password
# Optional Gemini runtime defaults
# You can also set these in runtime-config.json via UI
GEMINI_API_KEY=
GEMINI_MODEL=nanobanana-pro

View file

@ -29,7 +29,11 @@ python3 -m pip install -r backend/requirements.txt
# 3) 准备状态文件(首次) # 3) 准备状态文件(首次)
cp state.sample.json state.json cp state.sample.json state.json
# 4) 启动后端 # 4) (推荐)准备本地环境变量
cp .env.example .env
# 然后编辑 .env至少设置 FLASK_SECRET_KEY 与 ASSET_DRAWER_PASS
# 5) 启动后端
cd backend cd backend
python3 app.py python3 app.py
``` ```
@ -123,7 +127,14 @@ python3 -m pip install -r backend/requirements.txt
cp state.sample.json state.json cp state.sample.json state.json
``` ```
### 3) 启动后端 ### 3) (推荐)准备本地环境变量
```bash
cp .env.example .env
# 编辑 .env至少设置 FLASK_SECRET_KEY 与 ASSET_DRAWER_PASS
```
### 4) 启动后端
```bash ```bash
cd backend cd backend
@ -143,6 +154,19 @@ python3 set_state.py idle "待命中"
--- ---
## 3.1、安全自检(推荐上线前执行)
```bash
python3 scripts/security_check.py
```
- 返回 `Result: OK` 才建议进入公网部署。
- 在生产模式(`STAR_OFFICE_ENV=production`)下,请务必配置强密码:
- `FLASK_SECRET_KEY`>=24 位随机字符串)
- `ASSET_DRAWER_PASS`(不要使用 `1234`
---
## 4、常用 API ## 4、常用 API
- `GET /health`:健康检查 - `GET /health`:健康检查
@ -224,10 +248,13 @@ star-office-ui/
...assets ...assets
docs/ docs/
screenshots/ screenshots/
scripts/
security_check.py
office-agent-push.py office-agent-push.py
set_state.py set_state.py
state.sample.json state.sample.json
join-keys.json join-keys.sample.json
.env.example
SKILL.md SKILL.md
README.md README.md
LICENSE LICENSE
@ -270,7 +297,11 @@ python3 -m pip install -r backend/requirements.txt
# 3) Initialize state file (first run) # 3) Initialize state file (first run)
cp state.sample.json state.json cp state.sample.json state.json
# 4) Start backend # 4) (Recommended) prepare local env file
cp .env.example .env
# Then edit .env: set at least FLASK_SECRET_KEY and ASSET_DRAWER_PASS
# 5) Start backend
cd backend cd backend
python3 app.py python3 app.py
``` ```
@ -342,7 +373,14 @@ python3 -m pip install -r backend/requirements.txt
cp state.sample.json state.json cp state.sample.json state.json
``` ```
### 3) Start backend ### 3) (Recommended) prepare local env file
```bash
cp .env.example .env
# Then edit .env: set at least FLASK_SECRET_KEY and ASSET_DRAWER_PASS
```
### 4) Start backend
```bash ```bash
cd backend cd backend
@ -362,6 +400,19 @@ python3 set_state.py idle "Standing by"
--- ---
## III.1 Security preflight (recommended before public deployment)
```bash
python3 scripts/security_check.py
```
- Only deploy publicly when it returns `Result: OK`.
- In production mode (`STAR_OFFICE_ENV=production`), set strong values for:
- `FLASK_SECRET_KEY` (>=24 random chars)
- `ASSET_DRAWER_PASS` (do not use `1234`)
---
## IV. Common APIs ## IV. Common APIs
- `GET /health`: Health check - `GET /health`: Health check
@ -450,10 +501,13 @@ star-office-ui/
...assets ...assets
docs/ docs/
screenshots/ screenshots/
scripts/
security_check.py
office-agent-push.py office-agent-push.py
set_state.py set_state.py
state.sample.json state.sample.json
join-keys.json join-keys.sample.json
.env.example
SKILL.md SKILL.md
README.md README.md
LICENSE LICENSE

View file

@ -834,7 +834,15 @@ def state_to_area(state):
if not os.path.exists(AGENTS_STATE_FILE): if not os.path.exists(AGENTS_STATE_FILE):
save_agents_state(DEFAULT_AGENTS) save_agents_state(DEFAULT_AGENTS)
if not os.path.exists(JOIN_KEYS_FILE): if not os.path.exists(JOIN_KEYS_FILE):
save_join_keys({"keys": []}) if os.path.exists(os.path.join(ROOT_DIR, "join-keys.sample.json")):
try:
with open(os.path.join(ROOT_DIR, "join-keys.sample.json"), "r", encoding="utf-8") as sf:
sample = json.load(sf)
save_join_keys(sample if isinstance(sample, dict) else {"keys": []})
except Exception:
save_join_keys({"keys": []})
else:
save_join_keys({"keys": []})
# Tighten runtime-config file perms if exists # Tighten runtime-config file perms if exists
if os.path.exists(RUNTIME_CONFIG_FILE): if os.path.exists(RUNTIME_CONFIG_FILE):
@ -1983,6 +1991,20 @@ if __name__ == "__main__":
print("=" * 50) print("=" * 50)
print(f"State file: {STATE_FILE}") print(f"State file: {STATE_FILE}")
print("Listening on: http://0.0.0.0:18791") print("Listening on: http://0.0.0.0:18791")
mode = "production" if _is_production_mode() else "development"
print(f"Mode: {mode}")
if _is_production_mode():
print("Security hardening: ENABLED (strict checks)")
else:
weak_flags = []
if not _is_strong_secret(str(app.secret_key)):
weak_flags.append("weak FLASK_SECRET_KEY/STAR_OFFICE_SECRET")
if not _is_strong_drawer_pass(ASSET_DRAWER_PASS_DEFAULT):
weak_flags.append("weak ASSET_DRAWER_PASS")
if weak_flags:
print("Security hardening: WARNING (dev mode) -> " + ", ".join(weak_flags))
else:
print("Security hardening: OK")
print("=" * 50) print("=" * 50)
app.run(host="0.0.0.0", port=18791, debug=False) app.run(host="0.0.0.0", port=18791, debug=False)

View file

@ -1,3 +0,0 @@
{
"keys": []
}

13
join-keys.sample.json Normal file
View file

@ -0,0 +1,13 @@
{
"keys": [
{
"key": "ocj_example_team_01",
"used": false,
"reusable": true,
"maxConcurrent": 3,
"usedBy": null,
"usedByAgentId": null,
"usedAt": null
}
]
}

136
scripts/security_check.py Executable file
View file

@ -0,0 +1,136 @@
#!/usr/bin/env python3
"""Star Office UI security preflight checker (non-destructive).
Checks:
- weak/default secrets in env
- risky tracked files in git index
- known API key patterns in tracked files
"""
from __future__ import annotations
import os
import re
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
def run(cmd: list[str]) -> tuple[int, str, str]:
p = subprocess.run(cmd, cwd=ROOT, capture_output=True, text=True)
return p.returncode, p.stdout.strip(), p.stderr.strip()
def is_strong_secret(v: str) -> bool:
if not v:
return False
s = v.strip()
if len(s) < 24:
return False
low = s.lower()
for token in ("change-me", "default", "example", "test", "dev"):
if token in low:
return False
return True
def is_strong_pass(v: str) -> bool:
if not v:
return False
s = v.strip()
if s == "1234":
return False
return len(s) >= 8
def tracked_files() -> list[str]:
code, out, _ = run(["git", "ls-files"])
if code != 0:
return []
return [x for x in out.splitlines() if x.strip()]
def file_has_secret_pattern(path: Path) -> list[str]:
hits: list[str] = []
try:
text = path.read_text(encoding="utf-8", errors="ignore")
except Exception:
return hits
patterns = [
(r"AIza[0-9A-Za-z\-_]{20,}", "Google/Gemini API key-like token"),
(r"sk-[A-Za-z0-9]{16,}", "Generic sk-* token"),
(r"AKIA[0-9A-Z]{16}", "AWS access key-like token"),
]
for pat, label in patterns:
if re.search(pat, text):
hits.append(label)
return hits
def main() -> int:
print("[security-check] Star Office UI preflight")
failures: list[str] = []
warnings: list[str] = []
env_mode = (os.getenv("STAR_OFFICE_ENV") or os.getenv("FLASK_ENV") or "").strip().lower()
in_prod = env_mode in {"prod", "production"}
secret = os.getenv("FLASK_SECRET_KEY") or os.getenv("STAR_OFFICE_SECRET") or ""
drawer_pass = os.getenv("ASSET_DRAWER_PASS") or ""
if in_prod:
if not is_strong_secret(secret):
failures.append("Weak/missing FLASK_SECRET_KEY (or STAR_OFFICE_SECRET) in production")
if not is_strong_pass(drawer_pass):
failures.append("Weak/missing ASSET_DRAWER_PASS in production")
else:
if not secret:
warnings.append("FLASK_SECRET_KEY not set (ok for local dev, not for production)")
if not drawer_pass:
warnings.append("ASSET_DRAWER_PASS not set (defaults may be unsafe for public exposure)")
tracked = tracked_files()
risky_tracked = [
"runtime-config.json",
"join-keys.json",
"office-agent-state.json",
]
for f in risky_tracked:
if f in tracked:
failures.append(f"Risky runtime file is tracked by git: {f}")
# scan tracked text-ish files for common secret patterns
for rel in tracked:
if rel.startswith(".git/"):
continue
p = ROOT / rel
if not p.exists() or p.is_dir():
continue
if p.stat().st_size > 2_000_000:
continue
hits = file_has_secret_pattern(p)
for h in hits:
failures.append(f"Potential secret pattern in tracked file: {rel} ({h})")
if warnings:
print("\nWarnings:")
for w in warnings:
print(f" - {w}")
if failures:
print("\nFAIL:")
for f in failures:
print(f" - {f}")
print("\nResult: FAILED")
return 1
print("\nResult: OK")
return 0
if __name__ == "__main__":
sys.exit(main())