Gitlab-ci กล้วยๆ: ใช้ Gitleaks ตรวจจับและป้องกัน secret key ไม่ให้เข้ามาในโค็ด
![Gitlab-ci กล้วยๆ: ใช้ Gitleaks ตรวจจับและป้องกัน secret key ไม่ให้เข้ามาในโค็ด](/content/images/size/w960/2024/01/gitleaks-on-giltab-ci.webp)
ในการพัฒนาซอฟแวร์ต่างๆ เรามีความจำเป็นต้องใช้งานพวก secret key หรือรหัสผ่านต่างๆ เพื่อเข้าถึงฐานข้อมูล หรือเข้าถึงระบบต่างๆ จากผู้ให้บริการที่อื่น ซึ่งโดยปกติเราจะทำกันในรูปแบบ .env
เพื่อเรียกค่ามาใช้งาน และไม่ควรเขียนรหัสผ่าน หรือ secret key ลงในโค็ดเลย
แต่เราจะมั่นใจได้ยังไงว่าโค็ดเราก่อน Push ขึ้นไปยัง Repo ไม่ได้มีข้อมูลพวกนี้หลุดเข้ามา ผมเลยอยากแนะนำเครื่องที่ผมใช้ประจำนั้นคือ Gitleaks
Gitleaks คืออะไร?
Gitleaks เป็นเครื่องมือโอเพ่นซอร์สสำหรับตรวจจับและป้องกันรหัสผ่านลับ, คีย์ API, โทเค็น, คีย์ส่วนตัว ฯลฯ ในโค็ด โดยมันจะสแกนโค้ดของเราเพื่อหาข้อมูลที่ละเอียดอ่อนที่อาจกระทำโดยไม่ได้ตั้งใจ ข้อมูลลับเหล่านี้อาจเป็นรหัสผ่าน คีย์ API โทเค็น คีย์ส่วนตัว หรือชื่อไฟล์ที่น่าสงสัย หรือนามสกุลไฟล์ เช่น id_rsa, .pem, htpasswd
GitLeaks ทำงานอย่างไร?
GitLeaks ทำงานโดยการสแกนโค้ดของเราเพื่อหาไฟล์ที่ตรงกับชุดรูปแบบที่กำหนดไว้ล่วงหน้า รูปแบบเหล่านี้ได้รับการออกแบบเพื่อให้ตรงกับข้อมูลที่ละเอียดอ่อนประเภทต่างๆ เช่น รหัสผ่าน ข้อมูลลับ ไฟล์คีย์ ฯลฯ เมื่อ GitLeaks พบข้อมูลที่ตรงกัน ระบบจะแจ้งเตือน และเราสามารถดำเนินการเพื่อลบข้อมูลเหล่านี้ออกจากโค็ดได้
Gitleaks + Gitlab-ci
เราสามารถใช้งาน Gitleaks มาทำงานใน Gitlab-ci ได้เพื่อให้มันตรวจโค็ดก่อนเข้ามายัง Repo ของเรา โดยสถานกาณ์จะเป็นในรูปแบบคือ ทุกครั้งที่มีการ Merge request เข้ามา ให้มันทำการแสกนโค็ดก่อน ถ้าผ่านถึงจะยอมให้ Merge request ได้
รูปแบบไฟล์ gitlab-ci.yml
จะได้แบบนี้
detect_secrets:
stage: scan
image:
name: "zricethezav/gitleaks"
entrypoint: [""]
script:
- gitleaks detect --source . --report-path reports/detect_secrets.json -v --no-git
artifacts:
reports:
secret_detection: reports/detect_secrets.json
rules:
- if: $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "dev"
ให้เพิ่ม reports/
ในโฟลเดอร์หลักของโปรเจ็กต์ด้วยเพื่อไว้ให้ gitleaks เขียน report ออกมา
สามารถเพิ่มไฟล์ .gitleaks.toml
ในโฟลเดอร์หลักของโปรเจ็กต์ได้
title = "Global gitleaks config"
[extend]
# useDefault will extend the base configuration with the default gitleaks config:
# https://github.com/zricethezav/gitleaks/blob/master/config/gitleaks.toml
useDefault = true
[allowlist]
description = "Global allowlisted paths, files, and stopwords"
regexTarget = "line" # gitleaks defaults to matching the Secret, let's change it to match the entire line (added in gitleaks v8.16.0)
paths = [
'''\.gitleaks\.toml$''', # Ignoring the generated configuration file
'''(.*?)(jpg|gif|doc|pdf|bin)$''', # Ignoring common binaries
'''(.*?)_test\.go$''', # Ignoring Go test files
'''(.*?)\.(spec|test)\.(j|t)s$''', # Ignoring JavaScript and TypeScript test files
'''go\.(mod|sum)$''', # Ignoring Go manifests
'''vendor\.json''',
'''Gopkg\.(lock|toml)''',
'''package-lock\.json''', # Ignoring Node/JS manifests
'''package\.json''',
'''composer\.json''',
'''composer\.lock''', #Ignoring PHP manifests
'''yarn\.lock''',
'''Pipfile''', # Ignoring Python manifests
'''Pipfile\.lock''',
'''poetry\.lock''',
'''node_modules\/''', # Ignoring Node dependencies
'''vendor\/''', # Ignoring Go dependencies
'''test(|s)\/''', # Ignoring test directories
]
# A more precise rule to detect Typeform API tokens
[[rules]]
description = "Typeform API token"
id = "typeform-api-token-custom"
regex = '''(?i)tfp_[a-zA-Z1-9]{40,50}_[a-zA-Z1-9]{8,16}'''
tags = [
"typeform",
]
# A rule to detect hardcoded tokens in HTTP Bearer authentication, like "Bearer: <token>"
[[rules]]
description = "Authorization Bearer tokens"
id = "authorization-bearer-token"
regex = '''(?i)Bearer(?:\s)+(\S{8,})'''
secretGroup = 1
entropy = 3.8
tags = [
"key",
"HTTP",
"bearer"
]
# The following rules look for credentials assigned to variables that its value has an entropy of more than 3 bits.
# To achieve this there\x27s a regexp for each language. The regexp checks for a variable with a suspicious name followed
# by a value assignation (for example, := in Go, = in JS, etc.). Then, looks for a group of non-space characters enclosed
# between quotes. If that group has an entropy higher than 3 bits the rule will trigger.
[[rules]]
description = "Hardcoded credentials in Go files"
id = "credentials-go"
path = '''(.*?)\.go$'''
regex = '''(?i)(?:secret|key|password|pwd|pass|token)(?:\w|\s*?)(?:=|:=)(?:\s*?)[\"\x27\x60](.{4,120}?)[\"\x27\x60]'''
secretGroup = 1
entropy = 3
tags = [
"credentials",
"hardcoded",
"go",
]
[[rules]]
description = "Hardcoded credentials in JavaScript or TypeScript files"
id = "credentials-javascript"
path = '''(.*?)\.(?:j|t)s$'''
regex = '''(?i)(?:secret|key|password|pwd|pass|token)(?:\w|\s*?)(?:=){1}(?:\s{0,10})[\"\x27`](.*?)[\"\x27`]'''
secretGroup = 1
entropy = 3
tags = [
"credentials",
"hardcoded",
"js",
]
[[rules]]
description = "Hardcoded credentials in PHP files"
id = "credentials-php"
path = '''(.*?)\.php$'''
regex = '''(?i)(?:secret|key|password|pwd|pass|token)(?:.{0,20})(?:=){1}(?:.{0,10})[\"\x27`](.{4,120})[\"\x27`]'''
secretGroup = 1
entropy = 3
tags = [
"credentials",
"hardcoded",
"php",
]
[[rules]]
description = "Hardcoded credentials in YAML files as quoted strings"
id = "credentials-yaml-quoted"
path = '''(.*?)\.y(a|)ml$'''
regex = '''(?i)(?:secret|key|password|pwd|pass|token)(?:.{0,20})(?::){1}(?:\s{0,10})(?:[\"\x27](.{4,120})[\"\x27])'''
secretGroup = 1
entropy = 3
tags = [
"credentials",
"hardcoded",
"yaml",
]
[rules.allowlist]
description = "Skip YAML Serverless variables, grabbed and concatenated values, encrypted secrets, and values with jinja2 placeholders"
regexes = [
'''\${(?:.)+}''', # Serverless variables
'''(?i)\(\((?:\s)*?(?:grab|concat)(?:.)*?(?:\s)*?\)\)''', # Grabbed and concatenated values
'''(?i)!!enveloped:''', # Encrypted secrets
'''(?:.)*?{{(?:.)*?}}''', # jinja2 placeholders
'''ENC\[AES256_GCM,data:''', # sops secrets
]
[[rules]]
description = "Hardcoded credentials in YAML files as unquoted strings"
id = "credentials-yaml-unquoted"
path = '''(.*?)\.y(a|)ml$'''
#regex = '''(?i)(?:secret|key|password|pwd|pass|token)(?:\w|\s*?)(?::){1}(?:\s*?)((?:\w|\S)+)(?:|(?:\s*?#.*))$'''
regex = '''(?i)(?:secret|key|password|pwd|pass|token)(?:\w|\s*?)(?::){1}(?:\s*?)((?:\w|\S)+)'''
secretGroup = 1
entropy = 3.5 # A higher entropy is required for this type of match, as unquoted can trigger many false positives
tags = [
"credentials",
"hardcoded",
"yaml",
]
[rules.allowlist]
description = "Skip YAML Serverless variables, grabbed and concated values, encrypted secrets, and values with jinja2 placeholders"
regexes = [
''':$''', # It's a YAML key (as in key: value)
'''\${(?:.)+}''', # Serverless variables
'''(?i)\(\((?:\s)*?(?:grab|concat)(?:.)*?(?:\s)*?\)\)''', # Grabbed and concatenated values
'''(?i)!!enveloped:''', # Encrypted secrets
'''(?:.)*?{{(?:.)*?}}''', # jinja2 placeholders
'''={{$''', # jinja2 variable assignation
'''ENC\[AES256_GCM,data:''', # sops secrets
]
[[rules]]
description = "Hardcoded credentials in YAML files as multiline strings"
id = "credentials-yaml-multiline"
path = '''(.*?)\.y(a|)ml$'''
regex = '''(?i)(?:secret|key|password|pwd|pass|token)(?:.{0,20})(?::){1}(?:\s{0,10})(?:\|(?:-|))\n(?:\s{0,10})(\S{4,120})'''
secretGroup = 1
entropy = 4
tags = [
"credentials",
"hardcoded",
"yaml",
]
[[rules]]
description = "Hardcoded credentials in HCL files (*.tf)"
id = "credentials-terraform"
path = '''(.*?)\.tf$'''
regex = '''(?i)(?:secret|key|password|pwd|pass|token)(?:.{0,20})(?:=){1}(?:\s)*?"(.{4,120})"'''
secretGroup = 1
entropy = 3
tags = [
"credentials",
"hardcoded",
"hcl",
]
[rules.allowlist]
description = "Skip variable substitution"
regexes = [
'''\${(?:.)*?}''',
]
[[rules]]
description = "Hardcoded credentials in Python files (*.py)"
id = "credentials-python"
path = '''(.*?)\.py$'''
regex = '''(?i)(?:secret|key|password|pwd|pass|token)(?:.{0,20})(?:=){1}(?:\s)*?["\x27](.{4,120})["\x27]'''
secretGroup = 1
entropy = 3
tags = [
"credentials",
"hardcoded",
"hcl",
]
ถ้าอยากให้มัน ignore ไฟล์หรือโฟลเดอร์ไหนก็ให้ไปเพิ่มใน [allowlist]
ในส่วนของ paths = []
ได้เลย
ตัวอย่างผลใน Merge request
เมื่อการ Merge request เข้ามายัง branch ที่เรากำหนดไป มันจะทำการแสกนและถ้าเจอก็จะ Error ออกมา
![](https://snappytux.com/content/images/2024/01/error-gitleaks.webp)
และเมื่อมาดูหน้า Merge request ก็จะเจอ Error และก็จะไม่ให้ Merge และเราสามารถโหลด report ออกมาอ่านได้ด้วย
![](https://snappytux.com/content/images/2024/01/gitlab-mr-report.webp)
Gitleaks + pre-commit
ใช้งานร่วมกับ pre-commit ก็ได้นะ .pre-commit-config.yaml
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.18.1
hooks:
- id: gitleaks
เพียงเท่านี้ก็เป็นการป้องกันความผิดพลาดของนักพัฒนาไม่ให้นำไฟล์ Secret key หรือรหัสผ่านต่างๆ เข้ามาใน Repo ได้แล้ว...
Comments ()