민감 데이터 마스킹
env는 로그, 오류 메시지, 디버그 출력에서 민감 데이터 유출을 방지하기 위한 Loader와 독립적인 실용 도구 함수 세트를 제공합니다. 이 함수들은 Loader를 생성하지 않고도 직접 호출할 수 있으며, 구성을 안전하게 기록해야 하는 모든 시나리오에 적합합니다.
마스킹이 필요한 이유
SecureValue로 메모리 내의 민감 값을 적절히 보호하더라도, 여전히 세 가지 경로로 유출될 수 있습니다:
- 애플리케이션 로그 - 구성, 요청 매개변수, 연결 문자열을 직접 출력
- 오류 메시지 - panic / error가 키를 로그 수집 시스템으로 유입
- 디버그 출력 -
fmt.Println디버깅 시 무심코 환경 변수 출력
log.Printf("구성 로드 DB_PASSWORD=%s", pwd) ← 로그 유출
panic("connect failed: password=hunter2") ← 오류 유출
fmt.Println(env.GetString("API_KEY")) ← 디버그 유출이러한 출력이 로그 집계 시스템(ELK, Datadog...)으로 유입되거나 팀 멤버, 운영자, 심지어 공격자에게 보이면 키가 유출된 것이나 다름없습니다. env의 마스킹 도구를 사용하면 기록할 때 민감 콘텐츠를 자동으로 차폐하여 근원에서 유출을 막을 수 있습니다.
함수 상세
IsSensitiveKey
func IsSensitiveKey(key string) bool대소문자 구분 없이 key가 민감 패턴을 포함하는지 확인합니다. 검사는 부분 문자열 매칭을 사용합니다 - 키 이름(대문자로 변환 후)이 내장 패턴 중 하나라도 포함하면 민감으로 판정합니다.
내장 감지 패턴:
| 카테고리 | 패턴 |
|---|---|
| 인증류 | PASSWORD, SECRET, TOKEN, AUTH, CREDENTIAL, PASSPHRASE, SESSION, COOKIE |
| 키류 | API_KEY, APIKEY, ACCESS_KEY, SECRET_KEY, PRIVATE_KEY, PUBLIC_KEY |
| 암호화류 | PRIVATE, ENCRYPTION_KEY, ENCRYPT_KEY, DECRYPT_KEY, SIGNING_KEY, SIGN_KEY, VERIFY_KEY |
| 금융 / PII | SSN, SOCIAL_SECURITY, CREDIT_CARD, CARD_NUMBER, CVV, CVC, CCV, PAN |
| 암호화폐 | MNEMONIC, SEED, RECOVERY, WALLET, PRIVATE_ADDRESS |
| 인프라 | CONNECTION_STRING, CONN_STRING, DATABASE_URL, DB_PASSWORD |
| 클라우드 서비스 | AWS_SECRET, AZURE_KEY, GCP_KEY, SERVICE_ACCOUNT |
부분 문자열 매칭의 의미
IsSensitiveKey("MY_API_KEY_TOKEN")은 API_KEY와 TOKEN에 모두 매칭되어 true를 반환합니다. 즉, AUTHORIZATION도 AUTH를 포함하고 있어 민감으로 판정됩니다 - 이것이 의도된 보수적인 동작입니다.
package main
import (
"fmt"
"github.com/cybergodev/env"
)
func main() {
// 인증 및 키류
fmt.Println(env.IsSensitiveKey("DB_PASSWORD")) // true
fmt.Println(env.IsSensitiveKey("API_KEY")) // true
fmt.Println(env.IsSensitiveKey("ACCESS_TOKEN")) // true
// 대소문자 구분 안 함
fmt.Println(env.IsSensitiveKey("api_key")) // true
fmt.Println(env.IsSensitiveKey("ApiKey")) // true
// 비민감 키
fmt.Println(env.IsSensitiveKey("PORT")) // false
fmt.Println(env.IsSensitiveKey("DB_HOST")) // false
}MaskValue
func MaskValue(key, value string) stringkey의 민감도에 따라 value를 마스킹하며, 구성 키-값 쌍을 기록하는 데 적합합니다:
| 조건 | 반환값 |
|---|---|
IsSensitiveKey(key)가 true | [MASKED:N chars](N = len(value)) |
비민감이고 len(value) ≤ 20 | 원래 값 |
비민감이고 len(value) > 20 | value[:17] + "..." |
package main
import (
"fmt"
"github.com/cybergodev/env"
)
func main() {
// 민감 값 → 마스킹(문제 해결을 위해 길이 정보 보존)
fmt.Println(env.MaskValue("DB_PASSWORD", "p@ssw0rd123"))
// 출력: [MASKED:11 chars]
// 비민감 짧은 값 → 원래대로 반환
fmt.Println(env.MaskValue("PORT", "8080"))
// 출력: 8080
// 비민감 긴 값(>20자) → 잘림
fmt.Println(env.MaskValue("DESCRIPTION", "this-is-a-very-long-description-value"))
// 출력: this-is-a-very-lo...
}길이 보존의 의도
[MASKED:N chars]은 값의 콘텐츠가 아닌 길이만 노출합니다. 이는 "비밀번호가 잘렸는지", "키가 완전한지"를 해결할 때 유용하며, 평문을 유출하지 않습니다.
MaskKey
func MaskKey(key string) string키 이름 자체를 마스킹하며, 키의 존재는 보여야 하지만 키의 의미를 노출하지 않아야 하는 시나리오에 사용됩니다(내부적으로 DefaultMaskKey 호출):
| 조건 | 반환값 |
|---|---|
len(key) ≤ 3 | *** |
len(key) > 3 | key[:2] + "***" |
package main
import (
"fmt"
"github.com/cybergodev/env"
)
func main() {
fmt.Println(env.MaskKey("DB_PASSWORD")) // DB***
fmt.Println(env.MaskKey("API_KEY")) // AP***
fmt.Println(env.MaskKey("TOKEN")) // TO***
fmt.Println(env.MaskKey("AB")) // ***
fmt.Println(env.MaskKey("XYZ")) // ***(길이 ≤ 3)
}MaskValue와의 조합
MaskKey는 키 이름의 앞 2자만 가져오므로, DB_HOST와 DB_PASSWORD 모두 DB***가 됩니다. 로그에서 둘을 구분해야 하는 경우 MaskValue와 함께 출력하거나, 키 이름이 중요하지 않을 때만 단독으로 사용하세요.
MaskSensitiveInString
func MaskSensitiveInString(s string) string긴 문자열을 잘라내어, 로그에 너무 많은 콘텐츠가 출력되는 것을 방지합니다(간접적으로 정보 유출되거나 로그가 폭발하는 것을 방지):
| 조건 | 반환값 |
|---|---|
len(s) > 50 | s[:47] + "..." |
| 그 외 | 원래 값 |
package main
import (
"fmt"
"github.com/cybergodev/env"
)
func main() {
long := "012345678901234567890123456789012345678901234567890123456789"
fmt.Println(env.MaskSensitiveInString(long))
// 출력: 012345678901234567890123456789012345678901234567...
short := "hello world"
fmt.Println(env.MaskSensitiveInString(short))
// 출력: hello world
}SanitizeForLog
func SanitizeForLog(s string) string문자열에서 key=value 패턴을 스캔하여, 민감 키에 해당하는 key=value를 전체 [MASKED]로 교체하며, 제어 문자도 제거합니다(\n과 \t는 보존). 연결 문자열, 오류 메시지 등 인라인 키-값을 처리하는 데 적합합니다.
감지하는 할당 패턴: password=, secret=, token=, auth=, credential=, passphrase=, session=, cookie=, api_key=, apikey=, access_key=, secret_key=, private_key=, public_key=, encrypt_key=, decrypt_key=, signing_key=, ssn=, credit_card=, card_number=, cvv=, cvc=, mnemonic=, seed=, recovery=, wallet=, connection_string=, database_url=, db_password=
package main
import (
"fmt"
"github.com/cybergodev/env"
)
func main() {
fmt.Println(env.SanitizeForLog("user=admin password=s3cret"))
// 출력: user=admin [MASKED]
fmt.Println(env.SanitizeForLog("token=abc123 host=localhost"))
// 출력: [MASKED] host=localhost
// 여러 민감 값을 모두 마스킹
fmt.Println(env.SanitizeForLog("user=pguser password=hunter2 api_key=sk_123"))
// 출력: user=pguser [MASKED] [MASKED]
}교체 단위
SanitizeForLog는 password=s3cret 전체를 단일 [MASKED]로 교체합니다(키 이름과 함께). password=[MASKED]로 보존하지 않습니다. 이렇게 하면 로그에 "여기에 비밀번호가 있다"는 정보조차 노출되지 않습니다.
ClearBytes
func ClearBytes(b []byte)바이트 슬라이스를 모두 0으로 설정합니다. Reveal()로 획득하여 []byte 형태로 처리한 민감 데이터를 수동으로 제로화하여 평문이 메모리에 남지 않도록 합니다.
package main
import (
"fmt"
"github.com/cybergodev/env"
)
func main() {
// []byte 형태로 처리되는 민감 데이터 시뮬레이션
secret := []byte("secret123")
fmt.Printf("제로화 전: %s\n", secret)
// 출력: 제로화 전: secret123
env.ClearBytes(secret)
fmt.Printf("제로화 후: %q\n", secret)
// 출력: 제로화 후: "\x00\x00\x00\x00\x00\x00\x00\x00\x00"
}ClearBytes의 한계
ClearBytes는 전달된 슬라이스만 제로화합니다. 동일한 민감 데이터가 여러 번 복사된 경우(예: string과 []byte 간 변환은 새 복사본을 생성), 이 복사본들은 일괄 제로화할 수 없습니다. 민감 데이터는 복사를 최소화하고, SecureValue의 Release() / Close()와 함께 사용해야 합니다.
실전 예제
아래는 애플리케이션 시작 시 구성을 안전하게 출력하고, 인라인 자격 증명이 포함된 오류 메시지를 처리하는 예제입니다 - MaskValue, SanitizeForLog, IsSensitiveKey, MaskKey의 협력 사용을 다룹니다:
package main
import (
"errors"
"fmt"
"github.com/cybergodev/env"
)
func main() {
// 환경 변수에서 로드된 구성 시뮬레이션
config := []struct{ key, value string }{
{"PORT", "8080"},
{"DB_HOST", "localhost"},
{"DB_PASSWORD", "super-secret-pwd"},
{"API_KEY", "sk_live_1234567890abcdef"},
}
fmt.Println("=== 시작 구성(마스킹됨)===")
for _, c := range config {
fmt.Printf("%-15s = %s\n", c.key, env.MaskValue(c.key, c.value))
}
fmt.Println("\n=== 오류 로그(자동 마스킹)===")
err := errors.New("failed to connect: user=admin password=hunter2 host=db.local")
fmt.Println(env.SanitizeForLog(err.Error()))
fmt.Println("\n=== 민감 키 목록(키 이름 마스킹)===")
for _, c := range config {
if env.IsSensitiveKey(c.key) {
fmt.Printf("민감 구성: %s\n", env.MaskKey(c.key))
}
}
}출력:
=== 시작 구성(마스킹됨)===
PORT = 8080
DB_HOST = localhost
DB_PASSWORD = [MASKED:16 chars]
API_KEY = [MASKED:24 chars]
=== 오류 로그(자동 마스킹)===
failed to connect: user=admin [MASKED] host=db.local
=== 민감 키 목록(키 이름 마스킹)===
민감 구성: DB***
민감 구성: AP***SecureValue와의 관계
env의 보안 체계는 두 가지 상호 보완적인 방어선으로 구성됩니다:
| 방어선 | 보호 대상 | 도구 |
|---|---|---|
| 메모리 보호 | 런타임에 메모리에 상주하는 값 | GetSecure / Reveal / Masked / Release |
| 출력 마스킹 | 로그, 오류, 디버그 출력에 기록되는 값 | IsSensitiveKey / MaskValue / SanitizeForLog 등 |
// 1. 메모리 보호: SecureValue로 읽기
secret := env.GetSecure("API_KEY")
defer secret.Release()
key := secret.Reveal()
// 2. 출력 마스킹: 기록 시 차폐
log.Printf("사용 %s 연결", secret.Masked())
// 또는 임의 소스의 값을 수동으로 마스킹(SecureValue에 한정하지 않음)
log.Printf("구성 %s", env.MaskValue("API_KEY", key))명확한 역할 분담
- SecureValue의
Masked()출력은[SECURE:32 bytes locked]형태로, 자신이 관리하는 값에만 사용됩니다. - 마스킹 도구 함수(
MaskValue등)는 모든 소스의 값에 사용할 수 있습니다 - SecureValue에 한정하지 않으며, Loader에도 의존하지 않습니다.
관련 문서
- 보안 개요 - 보안 아키텍처 총관
- SecureValue API - 메모리 내 값 보호(
Masked/Reveal포함) - 메모리 잠금 - 민감 데이터가 디스크로 스왑되는 것 방지
- 프로덕션 체크리스트 - 프로덕션 적용 전 보안 점검