BÀI VIẾT // [DEVOPS] Bash và Security: Bảo mật script hiệu quả trong DevOps
Bash và Security: Bảo mật script hiệu quả trong DevOps

Bash và Security: Bảo mật script hiệu quả trong DevOps

Hướng dẫn bảo mật Bash script trong DevOps: quản lý secrets, validate input, shellcheck, permission control,防止 injection, và security best practices.

DEVOPS BASH

Bash và Security trong DevOps

Trong bài viết trước, chúng ta đã tìm hiểu cách quản lý cloud với CLI tools. Bây giờ, chúng ta sẽ bàn về security — một chủ đề quan trọng nhưng thường bị bỏ qua khi viết Bash script.

Script Bash chạy tự động khắp nơi — từ CI/CD pipeline đến cron jobs trên server. Nhưng nếu không bảo mật, chúng có thể trở thành lỗ hổng lớn: lộ API keys, bị injection attack, hay bị lợi dụng để chiếm quyền kiểm soát hệ thống.

Bài viết này sẽ hướng dẫn bạn cách quản lý secrets, validate input, set permissions đúng cách, và scan script với shellcheck để xây dựng Bash scripts an toàn trong DevOps.


Tại sao cần bảo mật script?

Rủi ro phổ biến

Rủi roMô tảHậu quả
Hardcoded secretsAPI key, password trong codeLeaked khi share/repo
Command injectionInput không validate chạy thẳngServer bị chiếm quyền
Insecure permissionsScript world-executableAi cũng chạy được
Unquoted variables$var thay vì "$var"Unexpected behavior

Best practices overview

  • Không bao giờ hardcode secrets — dùng env vars hoặc vault
  • Luôn quote variables"$var" thay vì $var
  • Validate input — kiểm tra format trước khi dùng
  • Set restrictive permissionschmod 700 cho script
  • Scan với shellcheck —找出潜在 bugs

Quản lý secrets

Không hardcode secrets

1
2
3
4
5
6
7
8
# BAD — không bao giờ làm thế này
API_KEY="sk-1234567890abcdef"
DB_PASSWORD="mysecretpassword"
./deploy.sh "$API_KEY"

# GOOD — dùng environment variable
export API_KEY="${API_KEY:?API_KEY is required}"
./deploy.sh "$API_KEY"

Load từ file .env

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# Tạo file .env (không commit vào git)
cat > .env << 'EOF'
AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
DATABASE_URL=postgres://user:pass@localhost:5432/mydb
EOF

# Bảo vệ file
chmod 600 .env

# Load trong script
if [[ -f ".env" ]]; then
    set -a  # Tự động export
    source .env
    set +a
else
    echo "ERROR: .env file not found" >&2
    exit 1
fi

# Kiểm tra bắt buộc
: "${AWS_ACCESS_KEY_ID:?AWS_ACCESS_KEY_ID is required}"
: "${DATABASE_URL:?DATABASE_URL is required}"

CI/CD secrets

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# GitLab CI
deploy:
  script:
    - bash deploy.sh
  variables:
    AWS_ACCESS_KEY_ID: $AWS_ACCESS_KEY_ID  # Từ CI/CD settings
    AWS_SECRET_ACCESS_KEY: $AWS_SECRET_ACCESS_KEY

# GitHub Actions
deploy:
  runs-on: ubuntu-latest
  steps:
    - name: Deploy
      env:
        AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
        AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
      run: bash deploy.sh

HashiCorp Vault

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# Lấy secret từ Vault
get_vault_secret() {
    local path="$1"
    local key="$2"

    local secret
    secret=$(vault kv get -field="$key" "$path" 2>/dev/null)
    if [[ -z "$secret" ]]; then
        echo "ERROR: Failed to get secret from $path/$key" >&2
        return 1
    fi
    echo "$secret"
}

# Sử dụng
DB_PASS=$(get_vault_secret "secret/data/database" "password")

Input validation

Validate parameters

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# Kiểm tra số lượng arguments
if [[ $# -ne 2 ]]; then
    echo "Usage: $0 <hostname> <port>" >&2
    exit 1
fi

HOSTNAME="$1"
PORT="$2"

# Validate hostname (chỉ alphanumeric, dot, hyphen)
if [[ ! "$HOSTNAME" =~ ^[a-zA-Z0-9]([a-zA-Z0-9.-]*[a-zA-Z0-9])?$ ]]; then
    echo "ERROR: Invalid hostname: $HOSTNAME" >&2
    exit 1
fi

# Validate port (number 1-65535)
if [[ ! "$PORT" =~ ^[0-9]+$ ]] || (( PORT < 1 || PORT > 65535 )); then
    echo "ERROR: Invalid port: $PORT" >&2
    exit 1
fi

Sanitize filenames

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# Validate filename — chỉ cho phép safe characters
sanitize_filename() {
    local filename="$1"
    # Loại bỏ path traversal và dangerous chars
    filename=$(basename "$filename")
    if [[ "$filename" =~ [^a-zA-Z0-9._-] ]]; then
        echo "ERROR: Invalid filename: $filename" >&2
        return 1
    fi
    echo "$filename"
}

# Sử dụng
SAFE_FILE=$(sanitize_filename "$USER_INPUT") || exit 1
cp "$SAFE_FILE" /backup/

Prevent command injection

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# BAD — injection vulnerability
run_command() {
    local user_input="$1"
    eval "echo $user_input"  # DANGEROUS!
}

# SAFE — validate trước khi dùng
run_command_safe() {
    local user_input="$1"
    if [[ "$user_input" =~ ^[a-zA-Z0-9]+$ ]]; then
        echo "$user_input"
    else
        echo "ERROR: Invalid input" >&2
        return 1
    fi
}

Variable quoting

Luôn quote variables

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# Khi nào cần quote?
# Luôn quote khi expand variables để tránh word splitting và globbing

# BAD — unquoted
file=$1              # Nếu $1 có spaces → break
rm $file             # Nguy hiểm!
echo $var            # Có thể bị glob *

# GOOD — quoted
file="$1"
rm "$file"
echo "$var"

# Ví dụ thực tế
process_file() {
    local filepath="$1"  # Luôn quote parameter expansion

    if [[ ! -f "$filepath" ]]; then
        echo "File not found: $filepath" >&2
        return 1
    fi

    # Command substitution cũng nên quote
    local line_count
    line_count=$(wc -l < "$filepath")
    echo "File has $line_count lines"
}

Khi nào KHÔNG quote?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# Khi intentionally wanting word splitting
# Ví dụ: iterate qua list
for file in *.log; do
    echo "Processing: $file"
done

# Khi dùng [[ ]] (bash-specific, safer than [ ])
if [[ $var == "pattern" ]]; then
    echo "Matched"
fi

File permissions

Set đúng permissions

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# Script permission — chỉ owner được execute
chmod 700 script.sh
# Hoặc
chmod u+x script.sh

# Secret files — chỉ owner read
chmod 600 .env
chmod 600 ~/.ssh/id_rsa
chmod 600 ~/.aws/credentials

# Directory permissions
chmod 700 ~/.ssh
chmod 700 ~/.aws

Check permissions trong script

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
check_permissions() {
    local file="$1"
    local expected_perm="$2"

    local actual_perm
    actual_perm=$(stat -f "%Lp" "$file" 2>/dev/null || stat -c "%a" "$file" 2>/dev/null)

    if [[ "$actual_perm" != "$expected_perm" ]]; then
        echo "ERROR: $file has permissions $actual_perm, expected $expected_perm" >&2
        echo "Fix with: chmod $expected_perm $file" >&2
        return 1
    fi
}

# Sử dụng
check_permissions ".env" "600"
check_permissions "script.sh" "700"

Shellcheck

Cài đặt

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Debian/Ubuntu
apt-get install -y shellcheck

# CentOS/RHEL
yum install -y shellcheck

# macOS
brew install shellcheck

# Hoặc download binary
# https://github.com/koalaman/shellcheck/releases

Sử dụng

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# Check một file
shellcheck script.sh

# Check tất cả scripts trong thư mục
find . -name "*.sh" -exec shellcheck {} \;

# Output with severity levels
shellcheck -S warning script.sh

# Format output
shellcheck -f gcc script.sh
shellcheck -f json script.sh

Các lỗi thường gặp

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
# SC2086: Double quote to prevent globbing and word splitting
# BAD
echo $1
# GOOD
echo "$1"

# SC2046: Quote this to prevent word splitting
# BAD
cd $(dirname $0)
# GOOD
cd "$(dirname "$0")"

# SC2034: Variable appears unused
# WARNING — có thể là typo hoặc forgotten to use

# SC2155: Declare and assign separately
# BAD
local output=$(command)
# GOOD
local output
output=$(command)

Tích hợp vào CI/CD

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# GitHub Actions
lint:
  runs-on: ubuntu-latest
  steps:
    - uses: actions/checkout@v4
    - name: Install shellcheck
      run: sudo apt-get install -y shellcheck
    - name: Run shellcheck
      run: shellcheck -s bash scripts/*.sh

# GitLab CI
shellcheck:
  image: koalaman/shellcheck:stable
  script:
    - shellcheck -s bash scripts/*.sh

Ví dụ thực hành: Secure deploy script

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
#!/usr/bin/env bash
# ==============================================================================
# Script : secure-deploy.sh
# Mục đích: Deploy application với security best practices
# ==============================================================================

set -euo pipefail

# ===== SECURITY CHECKS =====

# Kiểm tra script permissions
SCRIPT_PERMS=$(stat -f "%Lp" "$0" 2>/dev/null || stat -c "%a" "$0" 2>/dev/null)
if [[ "$SCRIPT_PERMS" != "700" && "$SCRIPT_PERMS" != "755" ]]; then
    echo "WARNING: Script has loose permissions ($SCRIPT_PERMS). Recommended: 700" >&2
fi

# ===== ENVIRONMENT SETUP =====

# Load secrets từ .env
if [[ -f ".env" ]]; then
    set -a
    source .env
    set +a
else
    echo "ERROR: .env file not found" >&2
    exit 1
fi

# Validate required variables
for var in APP_NAME DEPLOY_ENV SSH_HOST SSH_KEY; do
    : "${!var:?ERROR: $var is not set in environment or .env}"
done

# Validate SSH key file
if [[ ! -f "$SSH_KEY" ]]; then
    echo "ERROR: SSH key not found: $SSH_KEY" >&2
    exit 1
fi

SSH_KEY_PERMS=$(stat -f "%Lp" "$SSH_KEY" 2>/dev/null || stat -c "%a" "$SSH_KEY" 2>/dev/null)
if [[ "$SSH_KEY_PERMS" != "600" ]]; then
    echo "ERROR: SSH key has insecure permissions ($SSH_KEY_PERMS). Expected: 600" >&2
    echo "Fix with: chmod 600 $SSH_KEY" >&2
    exit 1
fi

# ===== INPUT VALIDATION =====

DEPLOY_VERSION="${1:-}"
if [[ -z "$DEPLOY_VERSION" ]]; then
    echo "Usage: $0 <version>" >&2
    echo "Example: $0 1.2.3" >&2
    exit 1
fi

# Validate version format (semver-ish)
if [[ ! "$DEPLOY_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
    echo "ERROR: Invalid version format: $DEPLOY_VERSION (expected X.Y.Z)" >&2
    exit 1
fi

# ===== DEPLOYMENT =====

log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
}

log "Starting deployment of $APP_NAME v$DEPLOY_VERSION to $DEPLOY_ENV"

# Deploy qua SSH
ssh -i "$SSH_KEY" -o StrictHostKeyChecking=no "deploy@${SSH_HOST}" \
    "cd /opt/$APP_NAME && git pull && git checkout v$DEPLOY_VERSION && systemctl restart $APP_NAME"

log "Deployment completed successfully"

Ghi chú triển khai

  • Secrets management: Trong production, không dùng .env file — dùng dedicated secrets manager (Vault, AWS Secrets Manager, GCP Secret Manager). .env chỉ phù hợp cho development.
  • Audit logging: Log tất cả thay đổi configuration và secrets access. Đáp ứng compliance requirements (SOC 2, PCI DSS).
  • Secret rotation: Định kỳ thay đổi secrets. Script nên hỗ trợ reload secrets mà không cần restart.
  • Minimal privileges: Tạo dedicated service accounts với minimal permissions cho mỗi script. Không dùng root khi không cần thiết.
  • Code review: Luôn review security-focused khi review Bash scripts. Check cho hardcoded secrets, missing validation, và insecure patterns.

Lời kết

Bash và Security là chủ đề quan trọng trong DevOps. Từ việc quản lý secrets an toàn, validate input, quote variables, đến scan với shellcheck — mỗi bước đều giúp giảm thiểu rủi ro bảo mật.

Một script an toàn không chỉ bảo vệ dữ liệu nhạy cảm mà còn ngăn chặn các cuộc tấn công injection và đảm bảo hệ thống ổn định.

bài tiếp theo, chúng ta sẽ tổng kết series với Best Practice trong Bash: shebang, set -euo pipefail, modular functions, logging, và test với bats.

THẢO LUẬN & BÌNH LUẬN