feat(install): per-host auto-generated secrets for Docker installs

Each fresh 1Panel/Docker install generates unique MYSQL and NEXUS keys
via scripts/generate_nexus_secrets.py; install wizard reuses compose env.
This commit is contained in:
Nexus Deploy
2026-06-06 00:25:57 +08:00
parent e613aecc8e
commit 83ce11f55c
14 changed files with 431 additions and 89 deletions
+2
View File
@@ -40,6 +40,8 @@ bash /tmp/quick-install.sh --profile 2c8g
安装完成后在浏览器打开安装向导,完成 `.env` / MySQL / 管理员初始化。
**每台新服务器的密钥自动不同**:安装脚本生成 `MYSQL_*``NEXUS_SECRET_KEY` / `NEXUS_API_KEY` / `NEXUS_ENCRYPTION_KEY` 写入 `docker/.env.prod`,备份在 `/root/.nexus-install-secrets-*.env`。勿把生产密钥放进 `nexus-1panel.secrets.sh`(仅保留 `NEXUS_GITEA_TOKEN`)。
---
## 已 clone 到本机(跳过再次拉仓)
+17
View File
@@ -42,3 +42,20 @@ nx god # 上帝菜单(重启 Python / 升级 / 日志)
## 资源档位
`--profile 1c4g` · `2c8g`(默认)· `4c16g`
## 密钥(每台新服务器唯一)
全新安装(`install-nexus-fresh.sh` / `--fresh`)会调用 `scripts/generate_nexus_secrets.py` 自动生成:
- `MYSQL_ROOT_PASSWORD` / `MYSQL_PASSWORD`
- `NEXUS_SECRET_KEY` / `NEXUS_API_KEY` / `NEXUS_ENCRYPTION_KEY`(与安装向导算法一致)
写入 `docker/.env.prod`,备份到 `/root/.nexus-install-secrets-*.env`chmod 600)。
| 场景 | 命令 |
|------|------|
| 新机(推荐) | `curl \| bash``install-nexus-fresh.sh` |
| 迁机保留旧密钥 | `nexus-1panel.sh install --from-env /path/to/old/.env` |
| 显式复用 secrets 文件 | `nexus-1panel.sh install --reuse-secrets`**勿用于新机** |
`deploy/nexus-1panel.secrets.sh` 只应放 `NEXUS_GITEA_TOKEN`,不要放生产 `SECRET_KEY`
+6 -5
View File
@@ -14,10 +14,9 @@
#
# bash deploy/install-nexus-fresh.sh --profile 4c16g
#
# 凭据(任选其一,无需 export:
# - /root/.nexus-deploy.env
# - /root/.nexus-secrets/nexus.env
# - 仓库内 deploy/nexus-1panel.secrets.sh(私人仓库可 git add -f 后 push
# 凭据(仅 Gitea git 用,NEXUS 密钥由安装脚本按机自动生成:
# - /root/.nexus-deploy.env → NEXUS_GITEA_TOKEN
# - deploy/nexus-1panel.secrets.sh 勿放生产 SECRET_KEY(新机勿复用)
# =============================================================================
set -euo pipefail
@@ -115,8 +114,10 @@ load_credentials() {
set -a
source "${NEXUS_ROOT}/deploy/nexus-1panel.secrets.sh"
set +a
info "已加载 ${NEXUS_ROOT}/deploy/nexus-1panel.secrets.sh"
info "已加载 ${NEXUS_ROOT}/deploy/nexus-1panel.secrets.sh(仅 git 令牌)"
fi
unset NEXUS_SECRET_KEY NEXUS_API_KEY NEXUS_ENCRYPTION_KEY
info "全新安装:NEXUS 密钥将在 docker/.env.prod 中按本机自动生成"
}
install_root_commands() {
+5 -3
View File
@@ -3,9 +3,11 @@
NEXUS_GITEA_TOKEN='你的Gitea_Access_Token'
NEXUS_SECRET_KEY='生产NEXUS_SECRET_KEY不可改'
NEXUS_API_KEY='生产NEXUS_API_KEY不可改'
NEXUS_ENCRYPTION_KEY=''
# 新机安装勿填下面三项(install-nexus-fresh / --fresh 会自动按机生成)
# 仅迁机时: bash deploy/nexus-1panel.sh install --reuse-secrets
# NEXUS_SECRET_KEY=''
# NEXUS_API_KEY=''
# NEXUS_ENCRYPTION_KEY=''
NEXUS_DOMAIN='api.synaglobal.vip'
NEXUS_ROOT='/opt/nexus'
+74 -42
View File
@@ -50,6 +50,7 @@ step() { echo -e "${CYAN}[STEP]${NC} $*"; }
SKIP_CLONE=false
FRESH_INSTALL=false
REUSE_SECRETS=false
FROM_ENV="${NEXUS_FROM_ENV}"
NO_BACKUP=false
PRUNE_IMAGES=false
@@ -74,8 +75,9 @@ Nexus 1Panel 一键脚本
安装选项:
--skip-clone 已在 /opt/nexus,仅 pull
--fresh 全新安装(/app/install.html
--from-env PATH 从旧 .env 导入密钥
--fresh 全新安装(本机自动生成唯一密钥 + /app/install.html
--from-env PATH 从旧 .env 迁机导入密钥
--reuse-secrets 使用 secrets 文件中的 NEXUS 密钥(勿用于新机)
--domain NAME 覆盖域名
升级选项:
@@ -128,6 +130,10 @@ load_secrets() {
info "已加载 $NEXUS_DEPLOY_ENV"
fi
load_token_from_git_remote "$NEXUS_ROOT"
if [[ "$FRESH_INSTALL" == true ]]; then
unset NEXUS_SECRET_KEY NEXUS_API_KEY NEXUS_ENCRYPTION_KEY
info "全新安装:忽略 secrets 中的 NEXUS 密钥,将为本机自动生成"
fi
}
resolve_profile() {
@@ -443,15 +449,35 @@ clone_or_pull() {
git -C "$NEXUS_ROOT" checkout "$GIT_BRANCH" 2>/dev/null || true
}
write_temp_secrets_env() {
local f
f="$(mktemp /tmp/nexus-secrets-import.XXXXXX.env)"
{
printf 'MYSQL_ROOT_PASSWORD=%s\n' "${MYSQL_ROOT_PASSWORD:-$(rand_pass)}"
printf 'MYSQL_PASSWORD=%s\n' "${MYSQL_PASSWORD:-$(rand_pass)}"
printf 'NEXUS_SECRET_KEY=%s\n' "$NEXUS_SECRET_KEY"
printf 'NEXUS_API_KEY=%s\n' "$NEXUS_API_KEY"
printf 'NEXUS_ENCRYPTION_KEY=%s\n' "$NEXUS_ENCRYPTION_KEY"
} >"$f"
chmod 600 "$f"
echo "$f"
}
write_env_prod() {
local root="$1"
local env_file="$root/$ENV_PROD"
local example="$root/docker/.env.prod.example"
local gen="$root/scripts/generate_nexus_secrets.py"
local prof backup tmp
if [[ ! -f "$example" ]]; then
error "缺少 $example"
exit 1
fi
if [[ ! -f "$gen" ]]; then
error "缺少 $gen"
exit 1
fi
if [[ -f "$env_file" ]]; then
warn "$env_file 已存在,仅更新域名与连接池"
@@ -462,53 +488,58 @@ write_env_prod() {
return 0
fi
local mysql_root mysql_pass secret api enc
mysql_root="$(rand_pass)"
mysql_pass="$(rand_pass)"
prof="$(profile_env_file "$root")"
backup="/root/.nexus-install-secrets-$(date +%Y%m%d%H%M%S).env"
if [[ -n "$FROM_ENV" && -f "$FROM_ENV" ]]; then
info "$FROM_ENV 导入密钥..."
secret="$(read_env_val "$FROM_ENV" NEXUS_SECRET_KEY)"
api="$(read_env_val "$FROM_ENV" NEXUS_API_KEY)"
enc="$(read_env_val "$FROM_ENV" NEXUS_ENCRYPTION_KEY)"
local old_root old_pass dburl
old_root="$(read_env_val "$FROM_ENV" MYSQL_ROOT_PASSWORD)"
old_pass="$(read_env_val "$FROM_ENV" MYSQL_PASSWORD)"
[[ -n "$old_root" ]] && mysql_root="$old_root"
[[ -n "$old_pass" ]] && mysql_pass="$old_pass"
if [[ -z "$old_pass" ]]; then
dburl="$(read_env_val "$FROM_ENV" NEXUS_DATABASE_URL)"
if [[ "$dburl" =~ [Nn]exus:([^@]+)@ ]]; then
mysql_pass="${BASH_REMATCH[1]}"
fi
fi
info "迁机:$FROM_ENV 导入密钥..."
python3 "$gen" write-prod \
--template "$example" \
--output "$env_file" \
--domain "$NEXUS_DOMAIN" \
--profile-env "$prof" \
--from-env "$FROM_ENV" \
--backup "$backup"
elif [[ "$FRESH_INSTALL" == true ]]; then
secret=""
api=""
enc=""
info "全新安装:完成后访问 https://${NEXUS_DOMAIN}/app/install.html"
elif [[ -n "${NEXUS_SECRET_KEY:-}" ]]; then
secret="$NEXUS_SECRET_KEY"
api="${NEXUS_API_KEY:-}"
enc="${NEXUS_ENCRYPTION_KEY:-}"
info "使用 secrets 文件中的生产 NEXUS 密钥(Docker MySQL 密码为新生成)"
info "全新安装:为本机生成唯一 MYSQL / NEXUS 密钥..."
python3 "$gen" write-prod \
--template "$example" \
--output "$env_file" \
--domain "$NEXUS_DOMAIN" \
--profile-env "$prof" \
--fresh \
--wizard-pending \
--backup "$backup"
info "密钥备份: $backup(仅本机,勿复制到其他服务器)"
info "完成后访问 https://${NEXUS_DOMAIN}/app/install.html"
elif [[ "$REUSE_SECRETS" == true && -n "${NEXUS_SECRET_KEY:-}" && -n "${NEXUS_API_KEY:-}" && -n "${NEXUS_ENCRYPTION_KEY:-}" ]]; then
warn "使用 secrets 中的 NEXUS 密钥(仅迁机/复用,新机勿用 --reuse-secrets"
tmp="$(write_temp_secrets_env)"
python3 "$gen" write-prod \
--template "$example" \
--output "$env_file" \
--domain "$NEXUS_DOMAIN" \
--profile-env "$prof" \
--from-env "$tmp" \
--backup "$backup"
rm -f "$tmp"
else
secret="$(rand_secret)"
api="$(rand_secret)"
enc=""
warn "已生成随机 NEXUS 密钥(仅适合测试)"
info "未迁机导入:为本机生成唯一密钥..."
python3 "$gen" write-prod \
--template "$example" \
--output "$env_file" \
--domain "$NEXUS_DOMAIN" \
--profile-env "$prof" \
--fresh \
--wizard-pending \
--backup "$backup"
info "密钥备份: $backup"
if [[ "$FRESH_INSTALL" != true ]]; then
info "完成后访问 https://${NEXUS_DOMAIN}/app/install.html"
fi
fi
cp "$example" "$env_file"
chmod 600 "$env_file"
sed -i "s|^MYSQL_ROOT_PASSWORD=.*|MYSQL_ROOT_PASSWORD=${mysql_root}|" "$env_file"
sed -i "s|^MYSQL_PASSWORD=.*|MYSQL_PASSWORD=${mysql_pass}|" "$env_file"
sed -i "s|^NEXUS_SECRET_KEY=.*|NEXUS_SECRET_KEY=${secret}|" "$env_file"
sed -i "s|^NEXUS_API_KEY=.*|NEXUS_API_KEY=${api}|" "$env_file"
sed -i "s|^NEXUS_ENCRYPTION_KEY=.*|NEXUS_ENCRYPTION_KEY=${enc}|" "$env_file"
sed -i "s|^NEXUS_CORS_ORIGINS=.*|NEXUS_CORS_ORIGINS=https://${NEXUS_DOMAIN}|" "$env_file"
sed -i "s|^NEXUS_API_BASE_URL=.*|NEXUS_API_BASE_URL=https://${NEXUS_DOMAIN}|" "$env_file"
apply_pool_to_env_prod "$root" "$(profile_env_file "$root")"
info "已写入 $env_file"
}
@@ -715,6 +746,7 @@ parse_common_install() {
-h|--help) usage ;;
--skip-clone) SKIP_CLONE=true; shift ;;
--fresh) FRESH_INSTALL=true; shift ;;
--reuse-secrets) REUSE_SECRETS=true; shift ;;
--profile) NEXUS_PROFILE="$2"; shift 2 ;;
--cpus) CUSTOM_CPUS="$2"; shift 2 ;;
--mem-gb|--memory-gb) CUSTOM_MEM_GB="$2"; shift 2 ;;
+1
View File
@@ -97,6 +97,7 @@ main() {
for cf in /root/.nexus-deploy.env /root/.nexus-secrets/nexus.env; do
[[ -f "$cf" ]] && set -a && source "$cf" && set +a
done
unset NEXUS_SECRET_KEY NEXUS_API_KEY NEXUS_ENCRYPTION_KEY
echo ""
echo -e "${BLUE}╔══════════════════════════════════════════════════════════════╗${NC}"
+6 -2
View File
@@ -1,14 +1,18 @@
# Production Compose env — copy to docker/.env.prod (never commit)
# Migrate NEXUS_* secrets from Baota /www/wwwroot/.../.env UNCHANGED.
# Fresh install: bash deploy/install-nexus-fresh.sh (auto-generates unique secrets per host)
# Migration: bash deploy/nexus-1panel.sh install --from-env /path/to/old/.env
MYSQL_ROOT_PASSWORD=change_me_root_strong
MYSQL_PASSWORD=change_me_nexus_strong
# Required — copy from production .env (do NOT regenerate)
# Auto-generated on fresh install (unique per server). Do NOT copy between hosts.
NEXUS_SECRET_KEY=
NEXUS_API_KEY=
NEXUS_ENCRYPTION_KEY=
# 1 = entrypoint must not write /app/.env until install wizard step 3
NEXUS_INSTALL_WIZARD_PENDING=0
# Public URL
NEXUS_CORS_ORIGINS=https://api.synaglobal.vip
NEXUS_API_BASE_URL=https://api.synaglobal.vip
+2
View File
@@ -64,5 +64,7 @@ docker compose -f docker/docker-compose.prod.yml --env-file docker/.env.prod up
- 仅绑定 `127.0.0.1:8600`OpenResty 反代见 `deploy/1panel/openresty-nexus.conf.example`
- **2C8G 调优**`docker/mysql-prod-2c8g.cnf`buffer pool 1G、`max_connections=120`+ 容器 `mem_limit` + `NEXUS_DB_POOL_SIZE=30`
- 完整迁移步骤:`docs/design/plans/2026-06-04-1panel-docker-production.md`
- 一键安装:`bash deploy/install-1panel-docker.sh`
- 一键升级:`bash deploy/upgrade-1panel-docker.sh``--check` 仅查更新)
详见 `docs/design/specs/2026-06-03-docker-design.md`
+2 -1
View File
@@ -84,7 +84,8 @@ services:
NEXUS_CORS_ORIGINS: ${NEXUS_CORS_ORIGINS:?set NEXUS_CORS_ORIGINS}
NEXUS_SECRET_KEY: ${NEXUS_SECRET_KEY:?set NEXUS_SECRET_KEY}
NEXUS_API_KEY: ${NEXUS_API_KEY:?set NEXUS_API_KEY}
NEXUS_ENCRYPTION_KEY: ${NEXUS_ENCRYPTION_KEY:-}
NEXUS_ENCRYPTION_KEY: ${NEXUS_ENCRYPTION_KEY:?set NEXUS_ENCRYPTION_KEY}
NEXUS_INSTALL_WIZARD_PENDING: ${NEXUS_INSTALL_WIZARD_PENDING:-0}
NEXUS_API_BASE_URL: ${NEXUS_API_BASE_URL:?set NEXUS_API_BASE_URL}
NEXUS_DB_POOL_SIZE: ${NEXUS_DB_POOL_SIZE:-30}
NEXUS_DB_MAX_OVERFLOW: ${NEXUS_DB_MAX_OVERFLOW:-20}
+4
View File
@@ -38,6 +38,10 @@ write_env_from_environment() {
if [ -z "${NEXUS_SECRET_KEY:-}" ]; then
return 0
fi
if [ "${NEXUS_INSTALL_WIZARD_PENDING:-}" = "1" ]; then
echo "entrypoint: install wizard pending — skip writing /app/.env"
return 0
fi
mkdir -p "${PERSIST_DIR}"
{
echo "# Generated by docker/entrypoint.sh — do not commit"
+18 -30
View File
@@ -3,47 +3,35 @@
from __future__ import annotations
import base64
import secrets
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from scripts.generate_nexus_secrets import ( # noqa: E402
generate_nexus_secrets,
write_env_prod,
)
OUT = ROOT / "docker" / ".env"
EXAMPLE = ROOT / "docker" / ".env.example"
def _token(n: int = 32) -> str:
return secrets.token_urlsafe(n)
def main() -> None:
if OUT.exists():
raise SystemExit(f"Refusing to overwrite existing {OUT} — delete it first.")
mysql_root = _token(24)
mysql_pass = _token(24)
secret = _token(32)
api_key = _token(24)
# Fernet-compatible (same as install wizard)
enc_key = base64.urlsafe_b64encode(secrets.token_bytes(32)).decode()
text = EXAMPLE.read_text(encoding="utf-8")
lines: list[str] = []
for line in text.splitlines():
if line.startswith("MYSQL_ROOT_PASSWORD="):
lines.append(f"MYSQL_ROOT_PASSWORD={mysql_root}")
elif line.startswith("MYSQL_PASSWORD="):
lines.append(f"MYSQL_PASSWORD={mysql_pass}")
elif line.startswith("NEXUS_SECRET_KEY="):
lines.append(f"NEXUS_SECRET_KEY={secret}")
elif line.startswith("NEXUS_API_KEY="):
lines.append(f"NEXUS_API_KEY={api_key}")
elif line.startswith("NEXUS_ENCRYPTION_KEY="):
lines.append(f"NEXUS_ENCRYPTION_KEY={enc_key}")
else:
lines.append(line)
OUT.write_text("\n".join(lines) + "\n", encoding="utf-8", newline="\n")
secrets_map = generate_nexus_secrets()
write_env_prod(
EXAMPLE,
OUT,
domain="localhost",
secrets_map=secrets_map,
profile_env=None,
wizard_pending=False,
)
print(f"Wrote {OUT}")
print("Next: docker compose up -d --build")
@@ -0,0 +1,34 @@
# 2026-06-06 — 安装脚本按机自动生成唯一密钥
## 摘要
Docker / 1Panel 全新安装路径统一通过 `scripts/generate_nexus_secrets.py` 生成每台服务器独立的 `MYSQL_*``NEXUS_SECRET_KEY` / `NEXUS_API_KEY` / `NEXUS_ENCRYPTION_KEY`,写入 `docker/.env.prod` 并备份到 `/root/.nexus-install-secrets-*.env`。安装向导 `init-db` 复用容器环境内预生成密钥,避免与 Compose 不一致。
## 动机
新机安装不应从 `nexus-1panel.secrets.sh` 复制生产密钥;此前 `--fresh` 留空密钥导致 Compose 校验失败或各机密钥相同。
## 涉及文件
- `scripts/generate_nexus_secrets.py`(新建)
- `deploy/nexus-1panel.sh``write_env_prod` / `--reuse-secrets` / `--fresh` 忽略 secrets 中 NEXUS 项
- `deploy/install-nexus-fresh.sh` / `deploy/quick-install.sh`
- `docker/generate_env.py` — 复用同一生成器
- `docker/entrypoint.sh``NEXUS_INSTALL_WIZARD_PENDING=1` 时不写 `/app/.env`
- `docker/docker-compose.prod.yml` — 传递 `NEXUS_INSTALL_WIZARD_PENDING`
- `server/api/install.py``_install_keys_from_environment()`
- `deploy/README-1panel.md` · `deploy/nexus-1panel.secrets.sh.example`
## 迁移 / 重启
已部署实例无需变更。新机安装后需完成 `/app/install.html` 向导。
## 验证
```bash
python3 scripts/generate_nexus_secrets.py write-prod \
--template docker/.env.prod.example --output /tmp/t.env.prod \
--domain test.example.com --profile-env docker/profiles/2c8g.env --fresh --wizard-pending
grep NEXUS_SECRET_KEY /tmp/t.env.prod
ruff check server/api/install.py scripts/generate_nexus_secrets.py
```
+237
View File
@@ -0,0 +1,237 @@
#!/usr/bin/env python3
"""Generate per-host Nexus secrets (install / Docker).
Each fresh install must get unique MYSQL_* and NEXUS_* keys. Matches install wizard:
SECRET_KEY = token_hex(32)
API_KEY = token_hex(16)
ENCRYPTION_KEY = urlsafe_b64encode(token_bytes(32))
"""
from __future__ import annotations
import argparse
import base64
import re
import secrets
import shlex
import sys
from datetime import datetime, timezone
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from server.utils.text_io import read_utf8_lf, write_utf8_lf # noqa: E402
def generate_nexus_secrets() -> dict[str, str]:
return {
"MYSQL_ROOT_PASSWORD": secrets.token_urlsafe(24),
"MYSQL_PASSWORD": secrets.token_urlsafe(24),
"NEXUS_SECRET_KEY": secrets.token_hex(32),
"NEXUS_API_KEY": secrets.token_hex(16),
"NEXUS_ENCRYPTION_KEY": base64.urlsafe_b64encode(secrets.token_bytes(32)).decode(),
}
def parse_env_file(path: Path) -> dict[str, str]:
data: dict[str, str] = {}
if not path.is_file():
return data
for line in read_utf8_lf(path).splitlines():
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, _, value = line.partition("=")
data[key.strip()] = value.strip().strip('"').strip("'")
return data
def import_secrets_from_env(path: Path) -> dict[str, str]:
"""Import NEXUS + MySQL passwords from an existing .env (migration)."""
raw = parse_env_file(path)
out = generate_nexus_secrets()
for key in (
"MYSQL_ROOT_PASSWORD",
"MYSQL_PASSWORD",
"NEXUS_SECRET_KEY",
"NEXUS_API_KEY",
"NEXUS_ENCRYPTION_KEY",
):
val = raw.get(key, "").strip()
if val:
out[key] = val
if not out.get("NEXUS_ENCRYPTION_KEY"):
dburl = raw.get("NEXUS_DATABASE_URL", "")
m = re.search(r"mysql\+aiomysql://[^:]+:([^@]+)@", dburl)
if m and not out.get("MYSQL_PASSWORD"):
from urllib.parse import unquote_plus
out["MYSQL_PASSWORD"] = unquote_plus(m.group(1))
return out
def apply_profile_pools(lines: list[str], profile_env: Path) -> list[str]:
if not profile_env.is_file():
return lines
prof = parse_env_file(profile_env)
pool = prof.get("NEXUS_DB_POOL_SIZE", "")
overflow = prof.get("NEXUS_DB_MAX_OVERFLOW", "")
out: list[str] = []
for line in lines:
if pool and line.startswith("NEXUS_DB_POOL_SIZE="):
out.append(f"NEXUS_DB_POOL_SIZE={pool}")
elif overflow and line.startswith("NEXUS_DB_MAX_OVERFLOW="):
out.append(f"NEXUS_DB_MAX_OVERFLOW={overflow}")
else:
out.append(line)
return out
def write_env_prod(
template: Path,
output: Path,
*,
domain: str,
secrets_map: dict[str, str],
profile_env: Path | None,
wizard_pending: bool,
) -> None:
if not template.is_file():
raise SystemExit(f"Missing template: {template}")
lines: list[str] = []
for line in read_utf8_lf(template).splitlines():
key = line.split("=", 1)[0].strip() if "=" in line and not line.strip().startswith("#") else ""
if key in secrets_map:
lines.append(f"{key}={secrets_map[key]}")
elif key == "NEXUS_CORS_ORIGINS":
if domain == "localhost":
lines.append(line)
else:
lines.append(f"NEXUS_CORS_ORIGINS=https://{domain}")
elif key == "NEXUS_API_BASE_URL":
if domain == "localhost":
lines.append(line)
else:
lines.append(f"NEXUS_API_BASE_URL=https://{domain}")
else:
lines.append(line)
if wizard_pending:
found = False
new_lines: list[str] = []
for line in lines:
if line.startswith("NEXUS_INSTALL_WIZARD_PENDING="):
new_lines.append("NEXUS_INSTALL_WIZARD_PENDING=1")
found = True
else:
new_lines.append(line)
if not found:
new_lines.append("")
new_lines.append("# Set 0 after /app/install.html completes (init-db writes /app/.env)")
new_lines.append("NEXUS_INSTALL_WIZARD_PENDING=1")
lines = new_lines
if profile_env:
lines = apply_profile_pools(lines, profile_env)
header = [
f"# Generated {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S')} UTC",
"# Unique secrets for this host — do not copy to other servers",
"",
]
text = "\n".join(header + lines) + "\n"
write_utf8_lf(output, text)
output.chmod(0o600)
def write_secrets_backup(path: Path, secrets_map: dict[str, str], *, domain: str) -> None:
lines = [
f"# Nexus install secrets backup — {datetime.now(timezone.utc).isoformat()}",
f"# Domain: {domain}",
"# Store only on this server (chmod 600). Do NOT commit to Git.",
"",
]
for key in (
"MYSQL_ROOT_PASSWORD",
"MYSQL_PASSWORD",
"NEXUS_SECRET_KEY",
"NEXUS_API_KEY",
"NEXUS_ENCRYPTION_KEY",
):
if key in secrets_map:
lines.append(f"{key}={secrets_map[key]}")
write_utf8_lf(path, "\n".join(lines) + "\n")
path.chmod(0o600)
def cmd_shell(secrets_map: dict[str, str]) -> None:
for key, val in secrets_map.items():
print(f"export {key}={shlex.quote(val)}")
def main() -> None:
parser = argparse.ArgumentParser(description="Generate unique Nexus install secrets")
sub = parser.add_subparsers(dest="cmd", required=True)
p_shell = sub.add_parser("shell", help="Print export statements for bash eval")
p_shell.add_argument("--from-env", type=Path, help="Import keys from existing .env")
p_write = sub.add_parser("write-prod", help="Write docker/.env.prod from template")
p_write.add_argument("--template", type=Path, required=True)
p_write.add_argument("--output", type=Path, required=True)
p_write.add_argument("--domain", required=True)
p_write.add_argument("--profile-env", type=Path, default=None)
p_write.add_argument("--from-env", type=Path, default=None, help="Migration: import NEXUS keys")
p_write.add_argument(
"--fresh",
action="store_true",
help="New host: always generate new secrets (ignore process env)",
)
p_write.add_argument("--backup", type=Path, default=None, help="Write chmod 600 backup file")
p_write.add_argument(
"--wizard-pending",
action="store_true",
help="Set NEXUS_INSTALL_WIZARD_PENDING=1 in .env.prod",
)
args = parser.parse_args()
if args.cmd == "shell":
if args.from_env:
secrets_map = import_secrets_from_env(args.from_env)
else:
secrets_map = generate_nexus_secrets()
cmd_shell(secrets_map)
return
if args.cmd == "write-prod":
if args.from_env and args.from_env.is_file():
secrets_map = import_secrets_from_env(args.from_env)
wizard = False
elif args.fresh:
secrets_map = generate_nexus_secrets()
wizard = bool(args.wizard_pending)
else:
secrets_map = generate_nexus_secrets()
wizard = bool(args.wizard_pending)
write_env_prod(
args.template,
args.output,
domain=args.domain,
secrets_map=secrets_map,
profile_env=args.profile_env,
wizard_pending=wizard,
)
if args.backup:
write_secrets_backup(args.backup, secrets_map, domain=args.domain)
print(f"Wrote {args.output}")
if args.backup:
print(f"Backup {args.backup}")
if __name__ == "__main__":
main()
+23 -6
View File
@@ -98,6 +98,18 @@ def _build_redis_url(req: InitDbRequest) -> str:
return url
def _install_keys_from_environment() -> tuple[str, str, str] | None:
"""Reuse keys pre-generated by Docker install (docker/.env.prod → compose env)."""
import os
sk = os.environ.get("NEXUS_SECRET_KEY", "").strip()
ak = os.environ.get("NEXUS_API_KEY", "").strip()
ek = os.environ.get("NEXUS_ENCRYPTION_KEY", "").strip()
if sk and ak and ek:
return sk, ak, ek
return None
def _escape_env_value(value: str) -> str:
"""Escape a value for safe inclusion in a .env file.
@@ -512,12 +524,17 @@ async def init_db(req: InitDbRequest):
except Exception:
pool_size, max_overflow = 160, 120
# Generate keys
secret_key = secrets.token_hex(32)
api_key = secrets.token_hex(16)
# Generate Fernet-compatible encryption key (32 url-safe base64 bytes)
import base64
encryption_key = base64.urlsafe_b64encode(secrets.token_bytes(32)).decode()
# Keys: reuse Docker install pre-seed, else generate (must match install wizard)
preseed = _install_keys_from_environment()
if preseed:
secret_key, api_key, encryption_key = preseed
logger.info("Using pre-generated NEXUS keys from install environment")
else:
secret_key = secrets.token_hex(32)
api_key = secrets.token_hex(16)
import base64
encryption_key = base64.urlsafe_b64encode(secrets.token_bytes(32)).decode()
# Insert settings
settings_kv = {