fix: P0问题修复 - JWT配置、安全扫描、备份、Runbook
P0 问题修复(按照 gap analysis): 1. JWT密钥配置修复 - config.yaml 移除占位符,改为空字符串 - 添加测试验证 JWT_SECRET 环境变量覆盖功能 2. Docker 部署完善 - 添加 deploy.resources 限制(内存 512M,CPU 0.5) - 添加 healthcheck 健康检查 - 添加 restart: unless-stopped 重启策略 3. 安全扫描集成 - 创建 scripts/security/run-gosec.sh 安全扫描脚本 - 创建 scripts/security/workflow-template.yml CI工作流模板 - 运行 gosec 扫描发现 6 个 HIGH 级别整数溢出问题 4. 备份自动化 - 创建 scripts/backup/backup.sh 自动备份脚本 - 支持 SQLite 数据库和配置文件备份 - 支持备份验证、自动清理、恢复功能 5. Runbook 文档 - 创建 docs/runbooks/ 目录 - 添加 4 个核心 Runbook:服务启动、服务停止、备份恢复、日志分析 - 添加 README.md 索引文档
This commit is contained in:
332
scripts/backup/backup.sh
Normal file
332
scripts/backup/backup.sh
Normal file
@@ -0,0 +1,332 @@
|
||||
#!/bin/bash
|
||||
# 数据备份脚本
|
||||
# 支持 SQLite 数据库和配置文件的备份
|
||||
#
|
||||
# 使用方法:
|
||||
# ./scripts/backup/backup.sh # 执行一次备份
|
||||
# ./scripts/backup/backup.sh --restore # 从最新备份恢复
|
||||
# ./scripts/backup/backup.sh --list # 列出所有备份
|
||||
# ./scripts/backup/backup.sh --verify # 验证备份完整性
|
||||
#
|
||||
# 自动备份 (crontab):
|
||||
# 0 2 * * * /path/to/scripts/backup/backup.sh # 每天凌晨2点
|
||||
|
||||
set -e
|
||||
|
||||
# 配置
|
||||
BACKUP_DIR="${BACKUP_DIR:-./backups}"
|
||||
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
||||
BACKUP_NAME="user-management_${TIMESTAMP}"
|
||||
DB_PATH="${DB_PATH:-./data/user_management.db}"
|
||||
CONFIG_PATH="${CONFIG_PATH:-./configs/config.yaml}"
|
||||
RETENTION_DAYS="${RETENTION_DAYS:-30}"
|
||||
|
||||
# 颜色输出
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
log_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
|
||||
log_success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; }
|
||||
log_warning() { echo -e "${YELLOW}[WARNING]${NC} $1"; }
|
||||
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
||||
|
||||
# 创建备份目录
|
||||
mkdir -p "${BACKUP_DIR}"
|
||||
|
||||
# 备份数据库
|
||||
backup_database() {
|
||||
local db_file="$1"
|
||||
local backup_file="$2"
|
||||
|
||||
if [ -f "${db_file}" ]; then
|
||||
log_info "Backing up database: ${db_file}"
|
||||
# 使用 SQLite 的 .backup 命令进行一致性备份
|
||||
sqlite3 "${db_file}" ".backup '${backup_file}'"
|
||||
log_success "Database backed up to: ${backup_file}"
|
||||
return 0
|
||||
else
|
||||
log_warning "Database file not found: ${db_file}"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# 备份配置文件
|
||||
backup_config() {
|
||||
local config_file="$1"
|
||||
local backup_file="$2"
|
||||
|
||||
if [ -f "${config_file}" ]; then
|
||||
log_info "Backing up config: ${config_file}"
|
||||
cp "${config_file}" "${backup_file}"
|
||||
log_success "Config backed up to: ${backup_file}"
|
||||
return 0
|
||||
else
|
||||
log_warning "Config file not found: ${config_file}"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# 验证备份完整性
|
||||
verify_backup() {
|
||||
local backup_file="$1"
|
||||
local file_type="$2"
|
||||
|
||||
log_info "Verifying backup: ${backup_file}"
|
||||
|
||||
if [ ! -f "${backup_file}" ]; then
|
||||
log_error "Backup file not found: ${backup_file}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
case "${file_type}" in
|
||||
"sqlite")
|
||||
# 验证 SQLite 数据库完整性
|
||||
if sqlite3 "${backup_file}" "PRAGMA integrity_check;" 2>/dev/null | grep -q "ok"; then
|
||||
log_success "SQLite backup is valid"
|
||||
return 0
|
||||
else
|
||||
log_error "SQLite backup is corrupted"
|
||||
return 1
|
||||
fi
|
||||
;;
|
||||
"config")
|
||||
# 验证 YAML 格式
|
||||
if grep -q "^server:" "${backup_file}"; then
|
||||
log_success "Config backup is valid"
|
||||
return 0
|
||||
else
|
||||
log_error "Config backup is invalid"
|
||||
return 1
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# 执行完整备份
|
||||
do_backup() {
|
||||
log_info "Starting backup..."
|
||||
|
||||
local backup_subdir="${BACKUP_DIR}/${BACKUP_NAME}"
|
||||
mkdir -p "${backup_subdir}"
|
||||
|
||||
local db_backup="${backup_subdir}/database.db"
|
||||
local config_backup="${backup_subdir}/config.yaml"
|
||||
local metadata_file="${backup_subdir}/metadata.json"
|
||||
|
||||
# 备份数据库
|
||||
backup_database "${DB_PATH}" "${db_backup}" || true
|
||||
|
||||
# 备份配置
|
||||
backup_config "${CONFIG_PATH}" "${config_backup}" || true
|
||||
|
||||
# 创建元数据
|
||||
cat > "${metadata_file}" << EOF
|
||||
{
|
||||
"timestamp": "${TIMESTAMP}",
|
||||
"backup_name": "${BACKUP_NAME}",
|
||||
"db_path": "${DB_PATH}",
|
||||
"config_path": "${CONFIG_PATH}",
|
||||
"created_at": "$(date -Iseconds)"
|
||||
}
|
||||
EOF
|
||||
|
||||
# 创建压缩包
|
||||
local archive_file="${BACKUP_DIR}/${BACKUP_NAME}.tar.gz"
|
||||
tar -czf "${archive_file}" -C "${BACKUP_DIR}" "${BACKUP_NAME}"
|
||||
|
||||
# 计算校验和
|
||||
local checksum_file="${BACKUP_DIR}/${BACKUP_NAME}.sha256"
|
||||
sha256sum "${archive_file}" > "${checksum_file}"
|
||||
|
||||
# 清理未压缩的备份目录
|
||||
rm -rf "${backup_subdir}"
|
||||
|
||||
log_success "Backup completed: ${archive_file}"
|
||||
log_success "Checksum: $(cat ${checksum_file})"
|
||||
|
||||
# 清理过期备份
|
||||
cleanup_old_backups
|
||||
}
|
||||
|
||||
# 清理过期备份
|
||||
cleanup_old_backups() {
|
||||
log_info "Cleaning up backups older than ${RETENTION_DAYS} days..."
|
||||
|
||||
find "${BACKUP_DIR}" -name "*.tar.gz" -mtime +${RETENTION_DAYS} -delete 2>/dev/null || true
|
||||
find "${BACKUP_DIR}" -name "*.sha256" -mtime +${RETENTION_DAYS} -delete 2>/dev/null || true
|
||||
|
||||
log_success "Cleanup completed"
|
||||
}
|
||||
|
||||
# 列出所有备份
|
||||
list_backups() {
|
||||
log_info "Available backups in ${BACKUP_DIR}:"
|
||||
|
||||
if [ ! -d "${BACKUP_DIR}" ] || [ -z "$(ls -A ${BACKUP_DIR}/*.tar.gz 2>/dev/null)" ]; then
|
||||
log_warning "No backups found"
|
||||
return
|
||||
fi
|
||||
|
||||
printf "\n%-40s %15s %20s\n" "BACKUP FILE" "SIZE" "CREATED"
|
||||
printf "%s\n" "------------------------------------------------------------------------"
|
||||
|
||||
for archive in "${BACKUP_DIR}"/*.tar.gz; do
|
||||
if [ -f "${archive}" ]; then
|
||||
local size=$(du -h "${archive}" | cut -f1)
|
||||
local date=$(date -r "${archive}" "+%Y-%m-%d %H:%M:%S")
|
||||
printf "%-40s %15s %20s\n" "$(basename ${archive})" "${size}" "${date}"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# 恢复备份
|
||||
do_restore() {
|
||||
local latest_archive=$(ls -t "${BACKUP_DIR}"/*.tar.gz 2>/dev/null | head -1)
|
||||
|
||||
if [ -z "${latest_archive}" ]; then
|
||||
log_error "No backup found to restore"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_warning "This will overwrite current data with backup: ${latest_archive}"
|
||||
read -p "Are you sure? (yes/no): " confirm
|
||||
|
||||
if [ "${confirm}" != "yes" ]; then
|
||||
log_info "Restore cancelled"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
log_info "Restoring from: ${latest_archive}"
|
||||
|
||||
# 验证备份
|
||||
if [ -f "${latest_archive}.sha256" ]; then
|
||||
if ! sha256sum --check "${latest_archive}.sha256"; then
|
||||
log_error "Backup checksum verification failed!"
|
||||
exit 1
|
||||
fi
|
||||
log_success "Checksum verified"
|
||||
fi
|
||||
|
||||
# 解压到临时目录
|
||||
local temp_dir="${BACKUP_DIR}/.restore_temp"
|
||||
rm -rf "${temp_dir}"
|
||||
mkdir -p "${temp_dir}"
|
||||
tar -xzf "${latest_archive}" -C "${temp_dir}"
|
||||
|
||||
# 查找解压的目录
|
||||
local restored_subdir=$(find "${temp_dir}" -mindepth 1 -maxdepth 1 -type d | head -1)
|
||||
|
||||
# 恢复数据库
|
||||
local db_backup="${restored_subdir}/database.db"
|
||||
if [ -f "${db_backup}" ]; then
|
||||
verify_backup "${db_backup}" "sqlite"
|
||||
log_info "Restoring database..."
|
||||
cp "${db_backup}" "${DB_PATH}"
|
||||
log_success "Database restored"
|
||||
fi
|
||||
|
||||
# 恢复配置
|
||||
local config_backup="${restored_subdir}/config.yaml"
|
||||
if [ -f "${config_backup}" ]; then
|
||||
verify_backup "${config_backup}" "config"
|
||||
log_info "Restoring config..."
|
||||
cp "${config_backup}" "${CONFIG_PATH}"
|
||||
log_success "Config restored"
|
||||
fi
|
||||
|
||||
# 清理临时目录
|
||||
rm -rf "${temp_dir}"
|
||||
|
||||
log_success "Restore completed successfully!"
|
||||
}
|
||||
|
||||
# 验证备份
|
||||
do_verify() {
|
||||
local latest_archive=$(ls -t "${BACKUP_DIR}"/*.tar.gz 2>/dev/null | head -1)
|
||||
|
||||
if [ -z "${latest_archive}" ]; then
|
||||
log_error "No backup found to verify"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_info "Verifying latest backup: ${latest_archive}"
|
||||
|
||||
# 验证校验和
|
||||
if [ -f "${latest_archive}.sha256" ]; then
|
||||
if sha256sum --check "${latest_archive}.sha256"; then
|
||||
log_success "Checksum verified"
|
||||
else
|
||||
log_error "Checksum verification failed"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# 验证压缩包完整性
|
||||
if tar -tzf "${latest_archive}" > /dev/null 2>&1; then
|
||||
log_success "Archive integrity verified"
|
||||
else
|
||||
log_error "Archive is corrupted"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 解压并验证内容
|
||||
local temp_dir="${BACKUP_DIR}/.verify_temp"
|
||||
rm -rf "${temp_dir}"
|
||||
mkdir -p "${temp_dir}"
|
||||
tar -xzf "${latest_archive}" -C "${temp_dir}"
|
||||
|
||||
local restored_subdir=$(find "${temp_dir}" -mindepth 1 -maxdepth 1 -type d | head -1)
|
||||
|
||||
local db_backup="${restored_subdir}/database.db"
|
||||
if [ -f "${db_backup}" ]; then
|
||||
verify_backup "${db_backup}" "sqlite"
|
||||
fi
|
||||
|
||||
rm -rf "${temp_dir}"
|
||||
log_success "Backup verification completed successfully!"
|
||||
}
|
||||
|
||||
# 显示帮助
|
||||
show_help() {
|
||||
echo "Usage: $0 [COMMAND]"
|
||||
echo ""
|
||||
echo "Commands:"
|
||||
echo " (no args) Execute a backup"
|
||||
echo " --restore Restore from the latest backup"
|
||||
echo " --list List all backups"
|
||||
echo " --verify Verify the latest backup"
|
||||
echo " --help Show this help message"
|
||||
echo ""
|
||||
echo "Environment variables:"
|
||||
echo " BACKUP_DIR Backup directory (default: ./backups)"
|
||||
echo " DB_PATH Database path (default: ./data/user_management.db)"
|
||||
echo " CONFIG_PATH Config path (default: ./configs/config.yaml)"
|
||||
echo " RETENTION_DAYS Backup retention days (default: 30)"
|
||||
}
|
||||
|
||||
# 主逻辑
|
||||
case "${1:-}" in
|
||||
--restore)
|
||||
do_restore
|
||||
;;
|
||||
--list)
|
||||
list_backups
|
||||
;;
|
||||
--verify)
|
||||
do_verify
|
||||
;;
|
||||
--help)
|
||||
show_help
|
||||
;;
|
||||
"")
|
||||
do_backup
|
||||
;;
|
||||
*)
|
||||
log_error "Unknown command: $1"
|
||||
show_help
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
44
scripts/security/run-gosec.sh
Normal file
44
scripts/security/run-gosec.sh
Normal file
@@ -0,0 +1,44 @@
|
||||
#!/bin/bash
|
||||
# Go 安全扫描脚本
|
||||
# 使用 gosec 对代码进行安全扫描
|
||||
#
|
||||
# 使用方法:
|
||||
# ./scripts/security/run-gosec.sh # 扫描所有代码
|
||||
# ./scripts/security/run-gosec.sh ./internal # 扫描指定目录
|
||||
#
|
||||
# 依赖:
|
||||
# go install github.com/securego/gosec/v2/cmd/gosec@latest
|
||||
|
||||
set -e
|
||||
|
||||
# 颜色输出
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
SCAN_DIR="${1:-./...}"
|
||||
OUTPUT_FILE="gosec-report.json"
|
||||
|
||||
echo -e "${YELLOW}Running gosec security scan...${NC}"
|
||||
|
||||
# 检查 gosec 是否安装
|
||||
if ! command -v gosec &> /dev/null; then
|
||||
echo -e "${RED}gosec not found. Installing...${NC}"
|
||||
go install github.com/securego/gosec/v2/cmd/gosec@latest
|
||||
fi
|
||||
|
||||
# 运行 gosec
|
||||
gosec -fmt json -out="${OUTPUT_FILE}" "${SCAN_DIR}"
|
||||
|
||||
# 检查返回码
|
||||
RESULT=$?
|
||||
|
||||
if [ $RESULT -eq 0 ]; then
|
||||
echo -e "${GREEN}No issues found!${NC}"
|
||||
else
|
||||
echo -e "${RED}Security issues detected!${NC}"
|
||||
echo -e "${YELLOW}Report saved to: ${OUTPUT_FILE}${NC}"
|
||||
fi
|
||||
|
||||
exit $RESULT
|
||||
93
scripts/security/workflow-template.yml
Normal file
93
scripts/security/workflow-template.yml
Normal file
@@ -0,0 +1,93 @@
|
||||
# Go 安全扫描工作流
|
||||
# 集成 gosec 安全扫描
|
||||
#
|
||||
# 使用方法:
|
||||
# 1. 复制此文件到 .github/workflows/security.yml
|
||||
# 2. 或适配到 Gitea Actions
|
||||
# 3. 或手动运行: ./scripts/security/run-gosec.sh
|
||||
|
||||
name: Security Scan
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, develop]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
schedule:
|
||||
- cron: '0 2 * * *' # 每周凌晨2点运行
|
||||
|
||||
jobs:
|
||||
gosec:
|
||||
name: Go Security Scan
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.23'
|
||||
|
||||
- name: Install gosec
|
||||
run: go install github.com/securego/gosec/v2/cmd/gosec@latest
|
||||
|
||||
- name: Run gosec
|
||||
run: |
|
||||
gosec -fmt json -out=gosec-report.json ./...
|
||||
|
||||
- name: Upload security report
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: gosec-report
|
||||
path: gosec-report.json
|
||||
|
||||
- name: Display results
|
||||
run: |
|
||||
if [ -f gosec-report.json ]; then
|
||||
echo "Security issues found:"
|
||||
cat gosec-report.json | jq -r '.Results[] | "\(.Severity): \(.Details)"' 2>/dev/null || cat gosec-report.json
|
||||
fi
|
||||
|
||||
govulncheck:
|
||||
name: Vulnerability Check
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.23'
|
||||
|
||||
- name: Run govulncheck
|
||||
run: |
|
||||
go install golang.org/x/vuln/cmd/govulncheck@latest
|
||||
govulncheck ./...
|
||||
|
||||
npm-audit:
|
||||
name: NPM Audit
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: frontend/admin
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: frontend/admin/package-lock.json
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Run npm audit
|
||||
run: npm audit --audit-level=moderate
|
||||
Reference in New Issue
Block a user