Tất cả bài viết

Secrets không được hardcode — Spring profiles và env vars

Password trong application.yml commit lên Git là incident chờ xảy ra. Config theo môi trường, Spring profiles, và cách HMS tách secret khỏi codebase.

clean-codespring-bootsecurityconfiguration

Một intern push branch lên GitHub. Trong application.yml có dòng:

spring:
  datasource:
    password: HMS_prod_2024!

Repo private. Chỉ team xem. Không sao đâu.

Sáu tháng sau repo được mirror sang CI log, một contractor được add vào org, hoặc ai đó fork nhầm sang public template. Password vẫn nằm trong git history — git log -p vẫn đọc được dù mày đã xóa dòng đó ở commit sau. Rotate password toàn bộ stack, audit access log, báo security — vì một dòng config “tạm thời cho dev chạy được”.

Senior không reject vì mày dùng sai syntax YAML. Họ reject vì secret và code không được sống chung.


Tại sao hardcode secret là incident, không phải style issue

Secret gồm: database password, Redis password, Keycloak client secret, JWT signing key, API key của payment gateway, AWS credentials.

Khi secret nằm trong git:

Code review không phải lớp bảo vệ cuối cùng cho secret. Git đã leak thì review không cứu được.


Spring profiles — cùng codebase, khác môi trường

Spring Boot cho phép profile — bộ config kích hoạt theo môi trường:

application.yml          # defaults chung, KHÔNG có secret
application-dev.yml      # local: H2 hoặc local MySQL, port debug
application-staging.yml  # staging URLs
application-prod.yml     # prod URLs, vẫn KHÔNG password thật
# application.yml — chỉ placeholder và cấu trúc
spring:
  application:
    name: hms-api
  datasource:
    url: ${DB_URL}
    username: ${DB_USERNAME}
    password: ${DB_PASSWORD}   # giá trị đến từ env, không hardcode

  data:
    redis:
      host: ${REDIS_HOST:localhost}
      port: ${REDIS_PORT:6379}
      password: ${REDIS_PASSWORD:}

keycloak:
  auth-server-url: ${KEYCLOAK_URL}
  credentials:
    secret: ${KEYCLOAK_CLIENT_SECRET}

${DB_PASSWORD} đọc từ environment variable hoặc từ secret manager khi deploy. File YAML trong repo chỉ mô tả cần biến gì, không chứa giá trị gì.

Chạy local:

export DB_PASSWORD=local_dev_only
./mvnw spring-boot:run -Dspring-boot.run.profiles=dev

Hoặc dùng .env local list trong .gitignore:

# .gitignore
.env
.env.*
!.env.example
# .env.example — COMMIT file này, chỉ là template
DB_URL=jdbc:mysql://localhost:3306/hms_dev
DB_USERNAME=hms_dev
DB_PASSWORD=change_me
KEYCLOAK_CLIENT_SECRET=change_me

Junior copy .env.example.env, điền giá trị thật, không bao giờ commit .env.


Dev vs prod — không chỉ khác password

Profile không chỉ đổi secret. Nó đổi hành vi:

# application-dev.yml
logging:
  level:
    com.hms: DEBUG
spring:
  jpa:
    show-sql: true

# application-prod.yml
logging:
  level:
    com.hms: INFO
spring:
  jpa:
    show-sql: false   # đừng log SQL có PHI ra log aggregation

Trên prod, payment gateway URL là endpoint thật; dev dùng sandbox. Keycloak realm khác. Redis có password; local có thể không. Một application.yml duy nhất với if (prod) trong code là anti-pattern — để Spring profile và @Profile("dev") bean xử lý.

@Profile("dev")
@Bean
public PaymentGateway paymentGateway() {
    return new SandboxPaymentGateway();
}

@Profile("prod")
@Bean
public PaymentGateway paymentGateway(@Value("${payment.api-key}") String apiKey) {
    return new LivePaymentGateway(apiKey); // apiKey từ env
}

@ConfigurationProperties — gom config, không rải @Value

@ConfigurationProperties(prefix = "hms.storage")
public record StorageProperties(
    String bucket,
    String region,
    Duration presignedUrlTtl
) {}
hms:
  storage:
    bucket: ${S3_BUCKET}
    region: ap-southeast-1
    presigned-url-ttl: 15m

Type-safe, test được bằng @EnableConfigurationProperties với giá trị fake trong test. Secret vẫn inject qua ${...}, không nằm trong Java source.


CI/CD và secret manager

Trên GitHub Actions, GitLab CI, hoặc K8s:

Image Docker build từ code không chứa prod password. Cùng image chạy staging và prod — chỉ khác env vars mount vào runtime.


Những thứ vẫn hay bị quên


Takeaway

Rule đơn giản: nếu mất git repo mà attacker có quyền đọc toàn bộ history, họ không được có prod password. Mọi secret là env var hoặc secret manager; YAML trong repo chỉ là skeleton. Copy .env.example, add .env vào .gitignore, và khi senior hỏi “password lấy từ đâu?” — câu trả lời đúng không bao giờ là “trong file application.yml dòng 42”.


Bài tiếp theo: @Transactional sâu hơn — proxy, self-invocation, và rollback rules.