From 0b322aa4e1c16ee6d4e9aa232bc5c01a36639881 Mon Sep 17 00:00:00 2001 From: OpenClaw Assistant Date: Wed, 4 Mar 2026 13:34:05 +0800 Subject: [PATCH] chore(security): add preflight checks and safe config templates --- .env.example | 18 +++++ README.md | 66 ++++++++++++++++-- backend/app.py | 26 +++++++- join-keys.json | 3 - join-keys.sample.json | 13 ++++ scripts/security_check.py | 136 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 251 insertions(+), 11 deletions(-) create mode 100644 .env.example delete mode 100644 join-keys.json create mode 100644 join-keys.sample.json create mode 100755 scripts/security_check.py diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..bfb76da --- /dev/null +++ b/.env.example @@ -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 diff --git a/README.md b/README.md index 8f3c37b..9eb5f0c 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,11 @@ python3 -m pip install -r backend/requirements.txt # 3) 准备状态文件(首次) cp state.sample.json state.json -# 4) 启动后端 +# 4) (推荐)准备本地环境变量 +cp .env.example .env +# 然后编辑 .env:至少设置 FLASK_SECRET_KEY 与 ASSET_DRAWER_PASS + +# 5) 启动后端 cd backend python3 app.py ``` @@ -123,7 +127,14 @@ python3 -m pip install -r backend/requirements.txt cp state.sample.json state.json ``` -### 3) 启动后端 +### 3) (推荐)准备本地环境变量 + +```bash +cp .env.example .env +# 编辑 .env:至少设置 FLASK_SECRET_KEY 与 ASSET_DRAWER_PASS +``` + +### 4) 启动后端 ```bash 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 - `GET /health`:健康检查 @@ -224,10 +248,13 @@ star-office-ui/ ...assets docs/ screenshots/ + scripts/ + security_check.py office-agent-push.py set_state.py state.sample.json - join-keys.json + join-keys.sample.json + .env.example SKILL.md README.md LICENSE @@ -270,7 +297,11 @@ python3 -m pip install -r backend/requirements.txt # 3) Initialize state file (first run) 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 python3 app.py ``` @@ -342,7 +373,14 @@ python3 -m pip install -r backend/requirements.txt 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 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 - `GET /health`: Health check @@ -450,10 +501,13 @@ star-office-ui/ ...assets docs/ screenshots/ + scripts/ + security_check.py office-agent-push.py set_state.py state.sample.json - join-keys.json + join-keys.sample.json + .env.example SKILL.md README.md LICENSE diff --git a/backend/app.py b/backend/app.py index f873c75..69d391c 100644 --- a/backend/app.py +++ b/backend/app.py @@ -834,7 +834,15 @@ def state_to_area(state): if not os.path.exists(AGENTS_STATE_FILE): save_agents_state(DEFAULT_AGENTS) 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 if os.path.exists(RUNTIME_CONFIG_FILE): @@ -1983,6 +1991,20 @@ if __name__ == "__main__": print("=" * 50) print(f"State file: {STATE_FILE}") 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) - + app.run(host="0.0.0.0", port=18791, debug=False) diff --git a/join-keys.json b/join-keys.json deleted file mode 100644 index 33eac91..0000000 --- a/join-keys.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "keys": [] -} \ No newline at end of file diff --git a/join-keys.sample.json b/join-keys.sample.json new file mode 100644 index 0000000..95c9ef4 --- /dev/null +++ b/join-keys.sample.json @@ -0,0 +1,13 @@ +{ + "keys": [ + { + "key": "ocj_example_team_01", + "used": false, + "reusable": true, + "maxConcurrent": 3, + "usedBy": null, + "usedByAgentId": null, + "usedAt": null + } + ] +} diff --git a/scripts/security_check.py b/scripts/security_check.py new file mode 100755 index 0000000..d5b69e6 --- /dev/null +++ b/scripts/security_check.py @@ -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())