#!/bin/bash # Jenkins生产环境安全加固脚本 # 作者:张翔 # 日期:2026-04-07 # 版本:1.0 # 用途:系统性解决Jenkins暴露在公网8080端口的安全风险 set -euo pipefail # 颜色定义 RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # 日志函数 log_info() { echo -e "${GREEN}[INFO]${NC} $1" } log_warn() { echo -e "${YELLOW}[WARN]${NC} $1" } log_error() { echo -e "${RED}[ERROR]${NC} $1" } log_step() { echo -e "${BLUE}[STEP]${NC} $1" } # 配置参数 JENKINS_HOME="${JENKINS_HOME:-/var/lib/jenkins}" NGINX_CONF_DIR="${NGINX_CONF_DIR:-/etc/nginx/conf.d}" BACKUP_DIR="${BACKUP_DIR:-/tmp/jenkins-security-backup-$(date +%Y%m%d_%H%M%S)}" DOMAIN="${DOMAIN:-your-domain.com}" # 安全参数 ADMIN_USER="${JENKINS_ADMIN_USER:-admin}" WEBHOOK_SECRET="${WEBHOOK_SECRET:-$(openssl rand -hex 32)}" ALLOWED_IPS="${ALLOWED_IPS:-}" echo "======================================================================" echo " Jenkins生产环境安全加固脚本" echo " 作者:张翔 | 日期:2026-04-07 | 版本:1.0" echo "======================================================================" echo "" # 前置检查 log_step "执行前置检查..." if [ "$EUID" -ne 0 ]; then log_error "请使用root权限运行此脚本" exit 1 fi if ! command -v nginx &> /dev/null; then log_error "Nginx未安装,请先安装Nginx" exit 1 fi if ! command -v openssl &> /dev/null; then log_error "OpenSSL未安装" exit 1 fi log_info "前置检查通过" # 创建备份目录 log_step "创建备份目录..." mkdir -p "$BACKUP_DIR" log_info "备份目录:$BACKUP_DIR" # 备份现有配置 log_step "备份现有配置..." if [ -d "$JENKINS_HOME" ]; then cp -r "$JENKINS_HOME" "$BACKUP_DIR/jenkins-home" 2>/dev/null || true fi if [ -d "$NGINX_CONF_DIR" ]; then cp -r "$NGINX_CONF_DIR" "$BACKUP_DIR/nginx-conf" 2>/dev/null || true fi log_info "配置已备份" # 步骤1:修改Jenkins监听地址 log_step "步骤1/7:修改Jenkins监听地址为127.0.0.1..." if [ -f "/etc/default/jenkins" ]; then JENKINS_DEFAULT="/etc/default/jenkins" elif [ -f "/etc/sysconfig/jenkins" ]; then JENKINS_DEFAULT="/etc/sysconfig/jenkins" else log_warn "未找到Jenkins配置文件,跳过此步骤" JENKINS_DEFAULT="" fi if [ -n "$JENKINS_DEFAULT" ]; then cp "$JENKINS_DEFAULT" "$BACKUP_DIR/jenkins-default.bak" if grep -q "JENKINS_ARGS" "$JENKINS_DEFAULT"; then if grep -q "httpListenAddress" "$JENKINS_DEFAULT"; then sed -i 's/httpListenAddress=[^ ]*/httpListenAddress=127.0.0.1/' "$JENKINS_DEFAULT" else sed -i '/JENKINS_ARGS=/ s/"$/ --httpListenAddress=127.0.0.1"/' "$JENKINS_DEFAULT" fi else echo 'JENKINS_ARGS="--httpListenAddress=127.0.0.1"' >> "$JENKINS_DEFAULT" fi log_info "Jenkins配置已更新,仅监听127.0.0.1" fi # 步骤2:生成HTTP Basic Auth密码 log_step "步骤2/7:生成HTTP Basic Auth密码..." read -sp "请输入Jenkins访问密码: " JENKINS_PASSWORD echo "" read -sp "请再次确认密码: " JENKINS_PASSWORD_CONFIRM echo "" if [ "$JENKINS_PASSWORD" != "$JENKINS_PASSWORD_CONFIRM" ]; then log_error "两次密码输入不一致" exit 1 fi if [ -z "$JENKINS_PASSWORD" ]; then log_error "密码不能为空" exit 1 fi HTPASSWD_FILE="$NGINX_CONF_DIR/.jenkins-htpasswd" htpasswd -bc "$HTPASSWD_FILE" "$ADMIN_USER" "$JENKINS_PASSWORD" 2>/dev/null || \ openssl passwd -apr1 "$JENKINS_PASSWORD" | sed "s|^|$ADMIN_USER:|" > "$HTPASSWD_FILE" chmod 600 "$HTPASSWD_FILE" log_info "HTTP Basic Auth密码文件已生成:$HTPASSWD_FILE" # 步骤3:创建Nginx安全配置 log_step "步骤3/7:创建Nginx反向代理安全配置..." NGINX_JENKINS_CONF="$NGINX_CONF_DIR/jenkins-security.conf" cat > "$NGINX_JENKINS_CONF" << 'NGINX_CONF_EOF' # Jenkins安全反向代理配置 # 作者:张翔 # 日期:2026-04-07 # 说明:多层安全防护 - 认证、频率限制、IP白名单、审计日志 # 上游Jenkins服务 upstream jenkins_backend { server 127.0.0.1:8080; keepalive 32; } # 频率限制区域 limit_req_zone $binary_remote_addr zone=jenkins_limit:10m rate=10r/m; limit_conn_zone $binary_remote_addr zone=jenkins_conn:10m; # 日志格式(包含安全审计信息) log_format jenkins_security '$remote_addr - $remote_user [$time_local] ' '"$request" $status $body_bytes_sent ' '"$http_referer" "$http_user_agent" ' 'request_time=$request_time ' 'upstream_response_time=$upstream_response_time ' 'ssl_protocol=$ssl_protocol ' 'ssl_cipher=$ssl_cipher'; # HTTP重定向到HTTPS server { listen 80; server_name DOMAIN_PLACEHOLDER; # Let's Encrypt验证路径 location ^~ /.well-known/acme-challenge/ { default_type "text/plain"; root /var/www/letsencrypt; } location / { return 301 https://$server_name$request_uri; } } # HTTPS主配置 server { listen 443 ssl http2; server_name DOMAIN_PLACEHOLDER; # SSL配置 ssl_certificate /etc/letsencrypt/live/DOMAIN_PLACEHOLDER/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/DOMAIN_PLACEHOLDER/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384'; ssl_prefer_server_ciphers on; ssl_session_cache shared:SSL:10m; ssl_session_timeout 10m; # 安全响应头 add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Content-Type-Options "nosniff" always; add_header X-XSS-Protection "1; mode=block" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; # 访问日志 access_log /var/log/nginx/jenkins-access.log jenkins_security; error_log /var/log/nginx/jenkins-error.log warn; # 频率限制 limit_req zone=jenkins_limit burst=20 nodelay; limit_conn jenkins_conn 10; # 客户端请求限制 client_max_body_size 100m; client_body_timeout 60s; client_header_timeout 60s; # Webhook端点(IP白名单 + 签名验证) location ~ ^/generic-webhook-trigger(/.*)?$ { # IP白名单(仅允许Gitea服务器) # ALLOWED_IPS_PLACEHOLDER # 验证Webhook签名 # if ($http_x_gitea_signature = "") { # return 403; # } # 代理到Jenkins proxy_pass http://jenkins_backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Port $server_port; proxy_connect_timeout 60s; proxy_send_timeout 60s; proxy_read_timeout 60s; } # Jenkins主界面(需要认证) location /jenkins/ { # HTTP Basic Auth auth_basic "Jenkins Production Access"; auth_basic_user_file HTPASSWD_FILE_PLACEHOLDER; # 代理到Jenkins proxy_pass http://jenkins_backend/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Port $server_port; proxy_connect_timeout 60s; proxy_send_timeout 60s; proxy_read_timeout 60s; # WebSocket支持 proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; } # 默认拒绝其他路径 location / { return 404; } } NGINX_CONF_EOF # 替换占位符 sed -i "s|DOMAIN_PLACEHOLDER|$DOMAIN|g" "$NGINX_JENKINS_CONF" sed -i "s|HTPASSWD_FILE_PLACEHOLDER|$HTPASSWD_FILE|g" "$NGINX_JENKINS_CONF" # 添加IP白名单 if [ -n "$ALLOWED_IPS" ]; then IP_ALLOW_RULE="allow $ALLOWED_IPS; deny all;" sed -i "s|# ALLOWED_IPS_PLACEHOLDER|$IP_ALLOW_RULE|g" "$NGINX_JENKINS_CONF" fi log_info "Nginx安全配置已创建:$NGINX_JENKINS_CONF" # 步骤4:配置防火墙规则 log_step "步骤4/7:配置防火墙规则..." if command -v ufw &> /dev/null; then ufw --force enable ufw default deny incoming ufw default allow outgoing ufw allow 22/tcp comment 'SSH' ufw allow 80/tcp comment 'HTTP' ufw allow 443/tcp comment 'HTTPS' ufw deny 8080/tcp comment 'Jenkins Direct Access Blocked' ufw --force reload log_info "UFW防火墙规则已配置" elif command -v firewall-cmd &> /dev/null; then systemctl start firewalld systemctl enable firewalld firewall-cmd --permanent --add-service=ssh firewall-cmd --permanent --add-service=http firewall-cmd --permanent --add-service=https firewall-cmd --permanent --remove-port=8080/tcp firewall-cmd --reload log_info "Firewalld防火墙规则已配置" else log_warn "未检测到防火墙,请手动配置iptables规则" fi # 步骤5:创建Webhook签名验证脚本 log_step "步骤5/7:创建Webhook签名验证脚本..." WEBHOOK_VERIFY_SCRIPT="/usr/local/bin/verify-jenkins-webhook.sh" cat > "$WEBHOOK_VERIFY_SCRIPT" << 'WEBHOOK_EOF' #!/bin/bash # Webhook签名验证脚本 # 用途:验证来自Gitea的Webhook请求签名 set -euo pipefail WEBHOOK_SECRET="${WEBHOOK_SECRET:-}" PAYLOAD_FILE="${1:-/dev/stdin}" if [ -z "$WEBHOOK_SECRET" ]; then echo "ERROR: WEBHOOK_SECRET not set" >&2 exit 1 fi # 读取请求体 PAYLOAD=$(cat "$PAYLOAD_FILE") # 计算HMAC签名 SIGNATURE=$(echo -n "$PAYLOAD" | openssl dgst -sha256 -hmac "$WEBHOOK_SECRET" | awk '{print $2}') echo "sha256=$SIGNATURE" WEBHOOK_EOF chmod +x "$WEBHOOK_VERIFY_SCRIPT" log_info "Webhook验证脚本已创建:$WEBHOOK_VERIFY_SCRIPT" # 步骤6:配置Jenkins安全设置 log_step "步骤6/7:配置Jenkins安全设置..." JENKINS_CONFIG_XML="$JENKINS_HOME/config.xml" if [ -f "$JENKINS_CONFIG_XML" ]; then cp "$JENKINS_CONFIG_XML" "$BACKUP_DIR/config.xml.bak" # 禁用匿名访问 if grep -q "true" "$JENKINS_CONFIG_XML"; then log_info "Jenkins安全已启用" else sed -i 's|.*|true|' "$JENKINS_CONFIG_XML" 2>/dev/null || true fi log_info "Jenkins安全配置已更新" fi # 步骤7:创建安全验证脚本 log_step "步骤7/7:创建安全验证脚本..." VERIFY_SCRIPT="/usr/local/bin/verify-jenkins-security.sh" cat > "$VERIFY_SCRIPT" << 'VERIFY_EOF' #!/bin/bash # Jenkins安全验证脚本 # 作者:张翔 # 用途:验证Jenkins安全加固是否成功 set -euo pipefail GREEN='\033[0;32m' RED='\033[0;31m' YELLOW='\033[1;33m' NC='\033[0m' echo "==========================================" echo " Jenkins安全验证" echo "==========================================" echo "" PASS=0 FAIL=0 check_pass() { echo -e "${GREEN}[✓]${NC} $1" ((PASS++)) } check_fail() { echo -e "${RED}[✗]${NC} $1" ((FAIL++)) } check_warn() { echo -e "${YELLOW}[!]${NC} $1" } # 检查1:Jenkins是否仅监听127.0.0.1 echo "检查1:Jenkins监听地址" if netstat -tlnp 2>/dev/null | grep -q ":8080.*127.0.0.1"; then check_pass "Jenkins仅监听127.0.0.1:8080" elif netstat -tlnp 2>/dev/null | grep -q ":8080.*0.0.0.0"; then check_fail "Jenkins监听0.0.0.0:8080(风险!)" else check_warn "Jenkins未运行或监听地址未知" fi # 检查2:直接访问8080端口是否被拒绝 echo "" echo "检查2:直接访问8080端口" if curl -s -o /dev/null -w "%{http_code}" --connect-timeout 2 http://localhost:8080 2>/dev/null | grep -q "000"; then check_pass "直接访问8080端口被拒绝" else check_fail "可以直接访问8080端口(风险!)" fi # 检查3:Nginx配置是否正确 echo "" echo "检查3:Nginx配置" if nginx -t 2>/dev/null; then check_pass "Nginx配置语法正确" else check_fail "Nginx配置存在错误" fi # 检查4:HTTPS是否启用 echo "" echo "检查4:HTTPS配置" if [ -f "/etc/letsencrypt/live/$DOMAIN/fullchain.pem" ]; then check_pass "SSL证书已配置" else check_warn "SSL证书未找到,请手动配置" fi # 检查5:防火墙规则 echo "" echo "检查5:防火墙规则" if command -v ufw &> /dev/null; then if ufw status | grep -q "8080.*DENY"; then check_pass "防火墙已阻止8080端口" else check_fail "防火墙未阻止8080端口" fi elif command -v firewall-cmd &> /dev/null; then if ! firewall-cmd --list-ports | grep -q "8080"; then check_pass "防火墙已阻止8080端口" else check_fail "防火墙未阻止8080端口" fi else check_warn "未检测到防火墙" fi # 检查6:HTTP Basic Auth echo "" echo "检查6:HTTP Basic Auth" if [ -f "/etc/nginx/conf.d/.jenkins-htpasswd" ]; then check_pass "HTTP Basic Auth密码文件存在" else check_fail "HTTP Basic Auth密码文件不存在" fi # 检查7:Jenkinsfile中是否还有硬编码token echo "" echo "检查7:敏感信息检查" if [ -f "Jenkinsfile" ]; then if grep -q "token.*=.*['\"].*['\"]" Jenkinsfile 2>/dev/null; then check_fail "Jenkinsfile中存在硬编码token" else check_pass "Jenkinsfile中未发现硬编码token" fi else check_warn "未找到Jenkinsfile" fi # 汇总 echo "" echo "==========================================" echo " 验证结果:通过 $PASS 项,失败 $FAIL 项" echo "==========================================" if [ $FAIL -eq 0 ]; then echo -e "${GREEN}安全加固验证通过!${NC}" exit 0 else echo -e "${RED}安全加固存在风险,请检查失败项!${NC}" exit 1 fi VERIFY_EOF chmod +x "$VERIFY_SCRIPT" log_info "安全验证脚本已创建:$VERIFY_SCRIPT" # 重启服务 log_step "重启服务..." echo "" read -p "是否立即重启Jenkins和Nginx服务?(y/N): " RESTART_CHOICE if [[ "$RESTART_CHOICE" =~ ^[Yy]$ ]]; then if command -v systemctl &> /dev/null; then systemctl restart jenkins systemctl restart nginx log_info "服务已重启" else service jenkins restart service nginx restart log_info "服务已重启" fi else log_warn "请手动重启服务:systemctl restart jenkins nginx" fi # 输出安全信息 echo "" echo "======================================================================" echo " 安全加固完成" echo "======================================================================" echo "" echo "📋 重要信息:" echo " - Jenkins访问地址: https://$DOMAIN/jenkins/" echo " - 管理员用户: $ADMIN_USER" echo " - Webhook密钥: $WEBHOOK_SECRET" echo "" echo "📁 备份位置: $BACKUP_DIR" echo "" echo "✅ 后续步骤:" echo " 1. 运行安全验证: $VERIFY_SCRIPT" echo " 2. 更新Jenkinsfile中的webhook token为环境变量" echo " 3. 配置SSL证书(如未配置)" echo " 4. 设置定期安全审计" echo "" echo "⚠️ 安全提醒:" echo " - 请妥善保管管理员密码和Webhook密钥" echo " - 定期更新密码(建议每90天)" echo " - 监控访问日志:/var/log/nginx/jenkins-access.log" echo "" echo "📞 如遇问题,请检查:" echo " - Jenkins日志: journalctl -u jenkins -f" echo " - Nginx日志: tail -f /var/log/nginx/jenkins-error.log" echo "======================================================================"