본문으로 건너뛰기

안동민 개발노트

본문 시작

환경 변수 관리와 비밀 정보 보호

ConfigModule로 환경별 설정을 분리·검증하고 API 키와 자격 증명을 저장소와 배포 산출물에서 보호합니다.

이 절에서는 애플리케이션 설정과 민감한 정보를 관리하는 방법을 다루며, 환경 변수 관리비밀 정보 보호에 초점을 둡니다.

애플리케이션은 데이터베이스 연결 문자열, API 키, 클라우드 자격 증명 등 다양한 민감한 정보를 필요로 합니다.

이러한 비밀 정보가 코드에 하드코딩되거나, 버전 관리 시스템(Git)에 노출되거나, 안전하지 않은 방식으로 저장되면 심각한 보안 취약점이 됩니다.

따라서 개발 및 배포 과정에서 이러한 정보를 안전하게 다루는 것이 매우 중요합니다.

Git과 이미지에는 비밀 값을 남기지 않고, CI의 OIDC 단기 자격과 배포 게이트를 통과한 산출물을 배포하며, NestJS Pod가 워크로드 아이덴티티로 전용 저장소의 비밀을 런타임에 가져오는 세 신뢰 영역 구조

NestJS · secure paved road · 3 trust zones

비밀 값은 승인된 경로로만 런타임에 도착한다

Git과 이미지에는 키 이름·검증 규칙만 두고, 실제 값은 버전이 있는 전용 저장소에서 런타임에 가져옵니다. CI는 OIDC 단기 자격으로 배포 게이트를 통과하고, Pod는 EKS Pod Identity 또는 IRSA 같은 워크로드 아이덴티티로 필요한 비밀만 읽습니다.

NestJS 비밀 값의 승인 배포 및 런타임 주입 경로 저장소, 제어면, 런타임의 세 신뢰 영역을 보여 준다. 코드와 이미지는 OIDC 및 배포 게이트를 거쳐 Pod로 배포되고, 비밀은 워크로드 아이덴티티가 전용 저장소에서 런타임에 가져온다. Git, 이미지, 로그, 오류 응답으로 향하는 비밀 값 경로는 각각 차단된다. STORAGE · TRUST ZONE CONTROL PLANE · TRUST ZONE RUNTIME · TRUST ZONE SOURCE BUILD · SIGN DIGEST DEPLOY IDENTITY-CHECKED FETCH METADATA SECRET COMMIT SECRET BAKE BYPASS LOG · ERROR Git 저장소 code · key name · schema Image Registry immutable image · no secret Secret Store Vault · Secrets Manager owner · version · audit CI/CD · OIDC short-lived deploy identity 권한 있는 배포 게이트 digest · policy · approval no direct credential bypass NestJS Pod workload identity Pod Identity · IRSA schema · fail-fast 감사 증거 key name · version · status only 실선은 승인 경로 · × 표시는 비밀 값의 Git·이미지·로그·오류 응답 유입 또는 유출 차단
저장소

Git·이미지에는 이름과 규칙만 둔다

코드에는 설정 키와 검증 스키마를, 이미지에는 실행 산출물만 둡니다. 실제 값은 버전·소유자·감사 기록이 있는 Vault나 Secrets Manager에 저장합니다.

제어면

OIDC 단기 자격과 배포 게이트를 통과한다

CI는 장기 클라우드 키 대신 OIDC로 짧은 배포 자격을 받고, 승인된 이미지 digest와 정책을 확인한 뒤 런타임에 배포합니다.

런타임

Pod가 자기 워크로드 아이덴티티로 읽는다

EKS Pod Identity나 IRSA로 필요한 버전만 가져오고 시작 시 스키마를 검증합니다. 비밀 누락·형식 오류는 fail-fast로 시작을 중단합니다.

차단

Git · image · log · error response

실제 비밀 값이 네 경로에 남지 않게 합니다. GitHub 로그 마스킹은 불완전할 수 있으므로 출력 자체를 만들지 않습니다.

load · type

런타임 환경 변수가 .env보다 우선한다

get<T>는 런타임 변환이 아닙니다. Joi의 number().port()registerAs·ConfigType로 검증 결과와 소비 타입을 맞춥니다.

transport ≠ store

환경 변수는 암호화 저장소가 아니다

환경 변수는 전달 수단일 뿐 실행 프로세스 침해를 막지 못합니다. 최소 권한 워크로드 아이덴티티와 전용 저장소, 값 없는 감사를 함께 사용합니다.

no bypass

마스킹보다 노출 경로를 만들지 않는다

인코딩·분할·다중 행 값은 GitHub 마스킹을 벗어날 수 있습니다. OIDC 단기 자격을 우선하고 로그와 오류 응답에는 값 대신 키·버전·상태만 남깁니다.

승인된 길은 코드·이미지 배포와 비밀 값 전달을 분리합니다. 배포 산출물은 게이트를 지나고, 실제 값은 런타임 주체가 자기 아이덴티티로 가져옵니다.


환경 변수 관리의 중요성

환경 변수(Environment Variables)는 애플리케이션의 동작을 제어하거나, 실행 환경에 따라 변경되어야 하는 설정 값을 저장하는 데 사용되는 메커니즘입니다.

환경 변수를 사용하는 주된 이유는 다음과 같습니다.

  • 환경별 설정 분리: 개발, 스테이징, 운영 등 각 환경마다 데이터베이스 주소, 로깅 수준, API 엔드포인트 등이 다를 수 있습니다. 환경 변수를 사용하면 코드 변경 없이 동일한 애플리케이션 바이너리(또는 Docker 이미지)를 다른 환경에 배포할 수 있습니다.
  • 비밀 정보 보호: 민감한 정보(데이터베이스 비밀번호, API 키, OAuth 비밀 등)를 코드베이스에서 분리하여 Git 레포지토리 등에 노출되는 것을 방지합니다. 이는 Do not commit secrets to Git 원칙의 핵심입니다. 다만 환경 변수는 전달 수단이지 암호화 저장소가 아니며, 실행 프로세스가 침해된 뒤의 값 노출까지 막아 주지는 않습니다.
  • 유연성: 코드나 이미지를 다시 만들지 않고도 실행 환경별 설정을 바꿀 수 있습니다. 단, NestJS/Node 애플리케이션은 보통 시작 시 환경 변수를 읽으므로 변경한 값은 재시작이나 재배포 과정에서 반영됩니다.

ConfigModule.forRoot()가 파일과 실행 환경을 함께 읽을 때는 이미 설정된 런타임 환경 변수(process.env)가 .env 파일의 같은 키보다 우선합니다. 개발 편의를 위한 .env와 운영 비밀의 저장·주입 경로를 구분해야 합니다.


NestJS에서 환경 변수 관리

NestJS는 @nestjs/config 패키지를 통해 환경 변수 및 설정 관리를 위한 ConfigModule을 제공합니다.

이는 .env 파일을 로드하고, 환경 변수에 접근하며, 스키마 유효성 검사 등을 지원합니다.

ConfigModule 기본 사용법

단계 1: 필요한 패키지 설치
npm install @nestjs/config
단계 2: .env 파일 생성

로컬 개발에서는 프로젝트 루트에 .env 파일을 생성하고 환경 변수를 정의할 수 있습니다. 운영 환경에서는 전용 Secret Store와 배포 플랫폼의 런타임 주입을 우선합니다.

이 파일은 .gitignore에 추가하여 Git에 커밋되지 않도록 해야 합니다.
# .env
DATABASE_HOST=localhost
DATABASE_PORT=5432
DATABASE_USER=myuser
DATABASE_PASSWORD=mypassword123
DATABASE_NAME=mydb

API_KEY_GOOGLE=some_google_api_key_123
JWT_SECRET=replace_with_at_least_32_chars_secret

NODE_ENV=development
PORT=3000
단계 3: AppModuleConfigModule 등록

ConfigModule을 임포트하고, forRoot() 메서드를 사용하여 전역으로 사용할 수 있도록 설정합니다.

src/app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config'; // ConfigModule 임포트
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true, // ConfigModule을 전역 모듈로 만들어 모든 모듈에서 ConfigService를 주입 가능하게 함
      // 기본값은 프로젝트 루트의 .env 파일을 로드합니다.
      // envFilePath: '.env.development', // 실제 파일명을 쓸 때만 명시
      // ignoreEnvFile: true, // 운영에서는 플랫폼의 런타임 주입만 사용할 때 권장
    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}
  • isGlobal: true: ConfigModule을 전역으로 설정하여 다른 모듈에서 별도로 임포트하지 않고도 ConfigService를 주입받아 사용할 수 있게 합니다.
  • envFilePath: 기본값은 프로젝트 루트의 .env입니다. 다른 파일을 쓰려면 .env.development처럼 실제 파일명을 명시해야 합니다. 운영에서는 .env.production을 생성해 두는 방식보다 ignoreEnvFile: true와 플랫폼의 런타임 주입을 우선합니다.
  • 런타임 환경 변수와 .env에 같은 키가 있으면 런타임 값이 우선합니다. 이 우선순위를 배포 환경별 override에 사용하되, 비밀의 원본은 전용 저장소에서 관리합니다.
단계 4: ConfigService 주입 및 사용

애플리케이션의 서비스, 컨트롤러 등 필요한 곳에서 ConfigService를 주입받아 환경 변수 값에 접근할 수 있습니다.

src/app.service.ts
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config'; // ConfigService 임포트

@Injectable()
export class AppService {
  private readonly databaseHost: string;
  private readonly jwtSecret: string;
  private readonly nodeEnv: string;
  private readonly appPort: number;

  constructor(private configService: ConfigService) {
    this.databaseHost = this.configService.getOrThrow<string>('DATABASE_HOST');
    this.jwtSecret = this.configService.getOrThrow<string>('JWT_SECRET');
    this.nodeEnv = this.configService.get<string>('NODE_ENV', 'development'); // 기본값 설정 가능
    this.appPort = Number.parseInt(
      this.configService.get<string>('PORT', '3000'),
      10,
    );
    console.log(`Database Host: ${this.databaseHost}`);
    console.log(`JWT Secret: ${this.jwtSecret ? 'Loaded' : 'Not Loaded'}`); // 보안상 실제 값 출력 지양
    console.log(`Node Environment: ${this.nodeEnv}`);
    console.log(`App Port: ${this.appPort}`);
  }

  getHello(): string {
    return `Hello from ${this.nodeEnv} environment!`;
  }
}
  • configService.get<T>(key: string, defaultValue?: T): 환경 변수 값을 가져옵니다. 제네릭 타입은 TypeScript 단계의 힌트일 뿐 런타임 변환이나 검증을 수행하지 않습니다. 숫자는 명시적으로 변환하고, 입력은 검증 스키마를 통과시킵니다.
  • configService.getOrThrow<T>(key: string): 필수 값이 없으면 시작 단계에서 실패하게 만들어 누락을 빨리 발견할 수 있습니다.

환경 변수 유효성 검사

민감한 환경 변수나 필수적인 환경 변수가 누락되거나 형식이 잘못되면 애플리케이션이 제대로 작동하지 않거나 보안 취약점이 발생할 수 있습니다.

ConfigModule은 Joi 또는 class-validatorclass-transformer를 사용하여 환경 변수의 유효성을 검사할 수 있습니다.

단계 1: 필요한 패키지 설치 (Joi 사용 예시)
npm install joi
단계 2: 유효성 검사 스키마 정의
src/config/validation.schema.ts
import * as Joi from 'joi';

export const validationSchema = Joi.object({
  NODE_ENV: Joi.string()
    .valid('development', 'production', 'test', 'provision')
    .default('development'),
  PORT: Joi.number().port().default(3000),
  DATABASE_HOST: Joi.string().required(),
  DATABASE_PORT: Joi.number().port().default(5432),
  DATABASE_USER: Joi.string().required(),
  DATABASE_PASSWORD: Joi.string().required(), // 비밀번호는 항상 'required'
  DATABASE_NAME: Joi.string().required(),
  JWT_SECRET: Joi.string().min(32).required(),
  API_KEY_GOOGLE: Joi.string().optional(), // 선택적
});
단계 3: AppModule에 스키마 적용
src/app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { validationSchema } from './config/validation.schema'; // 스키마 임포트

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      validationSchema,
      validationOptions: {
        allowUnknown: true,
        abortEarly: false,
      },
    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}
  • validationSchema: 정의한 Joi 스키마를 전달합니다. 애플리케이션 시작 시 환경 변수가 이 스키마에 따라 유효성 검사를 통과하지 못하면 애플리케이션이 시작되지 않고 에러를 발생시킵니다. 이는 배포 전 환경 설정 문제를 조기에 발견하는 데 매우 유용합니다.

registerAsConfigType으로 소비 타입 맞추기

검증을 통과한 원시 값을 도메인별 설정 객체로 변환하면 문자열 키를 여러 서비스에 흩뿌리지 않아도 됩니다. ConfigType은 설정 팩터리의 반환 타입을 소비 코드에 연결하며, 런타임 검증은 여전히 Joi 스키마가 담당합니다.

src/config/app.config.ts
import { registerAs } from '@nestjs/config';

export default registerAs('app', () => ({
  port: Number.parseInt(process.env.PORT ?? '3000', 10),
  database: {
    host: process.env.DATABASE_HOST!,
    port: Number.parseInt(process.env.DATABASE_PORT ?? '5432', 10),
  },
}));
src/app.module.ts (설정 부분)
import { ConfigModule } from '@nestjs/config';
import appConfig from './config/app.config';
import { validationSchema } from './config/validation.schema';

ConfigModule.forRoot({
  isGlobal: true,
  load: [appConfig],
  validationSchema,
  validationOptions: { allowUnknown: true, abortEarly: false },
});
src/app.service.ts (설정 주입 부분)
import { Inject, Injectable } from '@nestjs/common';
import { ConfigType } from '@nestjs/config';
import appConfig from './config/app.config';

@Injectable()
export class AppService {
  constructor(
    @Inject(appConfig.KEY)
    private readonly config: ConfigType<typeof appConfig>,
  ) {}

  getHello(): string {
    return `NestJS listens on ${this.config.port}`;
  }
}

ConfigType은 팩터리의 반환 구조를 추론할 뿐 입력을 안전하게 만들지는 않습니다. 따라서 PORTDATABASE_PORT는 Joi의 number().port()로 범위까지 검사하고, 필수 비밀 누락은 시작 단계에서 실패시킵니다. validationOptions를 직접 넘길 때는 운영체제가 추가한 다른 환경 변수를 거부하지 않도록 allowUnknown: true도 명시합니다.


비밀 정보 보호 모범 사례

환경 변수는 개발 환경에서 .env 파일을 통해 쉽게 관리할 수 있지만, 운영 환경에서는 더욱 강력하고 안전한 비밀 정보 관리 전략이 필요합니다.

.env 파일은 서버 내부에 직접 저장되므로, 서버가 침해당하면 즉시 노출될 위험이 있습니다.

버전 관리 시스템에서 비밀 정보 제외

가장 기본적이면서도 중요한 원칙입니다.

.env 파일이나 민감한 정보가 포함된 설정 파일은 절대 Git 레포지토리에 커밋하면 안 됩니다.

# .gitignore
.env
.env.*
!.env.example
dist/
node_modules/
# ... 기타

CI/CD 파이프라인에서 환경 변수 주입

지속적 통합/배포(CI/CD) 파이프라인에서는 비밀 값을 이미지 빌드에 넣지 않고, 배포 대상의 런타임 주입 기능과 워크로드 아이덴티티를 사용합니다.

CI/CD가 클라우드 API를 호출해야 한다면 저장된 장기 액세스 키보다 OIDC로 발급받는 단기 자격 증명을 우선합니다.

  • GitHub Actions OIDC: 워크플로우가 클라우드 역할을 짧게 위임받아 승인된 이미지와 배포 명세만 전달하게 합니다. 애플리케이션 비밀은 대상 플랫폼이 런타임에 주입하거나 워크로드가 Secret Store에서 가져옵니다.

    .github/workflows/deploy.yml (부분 예시)
    permissions:
      id-token: write
      contents: read
    
    steps:
      - uses: actions/checkout@v7
      - uses: aws-actions/configure-aws-credentials@v6.2.3
        with:
          role-to-assume: ${{ vars.DEPLOY_ROLE_ARN }}
          aws-region: ap-northeast-2
      - name: Deploy immutable image
        run: ./scripts/deploy.sh

    이 예시는 .env.production 파일을 만들지 않습니다. ConfigModule은 운영에서 ignoreEnvFile: true로 두고, 대상 플랫폼이 런타임 환경 변수를 주입하거나 Pod가 전용 저장소에서 값을 가져오게 합니다.

    GitHub의 로그 마스킹은 알려진 원문에 대한 보조 장치입니다. 인코딩·분할·변환된 값이나 다중 행 출력은 완전히 가려지지 않을 수 있으므로 비밀을 출력하지 말고, 불가피한 배포 비밀보다 OIDC 단기 자격을 먼저 선택합니다.

  • Jenkins Credentials: Jenkins에서는 비밀 정보(Secret Text, Secret File, Username/Password)를 Jenkins Credential Provider에 저장하고 파이프라인 스크립트에서 안전하게 사용할 수 있습니다.

  • GitLab CI/CD Variables: GitLab 레포지토리 설정에서 CI/CD Variables를 정의하고 보호할 수 있습니다.

비밀 정보 관리 도구

대규모 애플리케이션이나 마이크로서비스 아키텍처에서는 전용 비밀 정보 관리 도구를 사용하는 것이 가장 안전하고 효율적입니다.

  • HashiCorp Vault: 중앙 집중식으로 비밀 정보를 저장하고 접근을 제어하며, 동적 비밀 정보 생성(예: 데이터베이스 임시 자격 증명) 기능을 제공합니다.
  • AWS Secrets Manager / AWS Parameter Store (SSM): AWS에서 제공하는 관리형 비밀 정보 저장 및 검색 서비스입니다. EC2는 인스턴스 역할을, EKS Pod는 EKS Pod Identity 또는 IRSA 같은 워크로드 아이덴티티를 사용해 장기 액세스 키 없이 필요한 값만 읽게 합니다.
  • Google Cloud Secret Manager: GCP의 관리형 비밀 정보 서비스.
  • Azure Key Vault: Azure의 비밀 정보 관리 서비스.
클라우드 기반 비밀 정보 관리 도구 활용 예시 (개념)

비밀 정보 저장: 데이터베이스 비밀번호, API 키 등을 Secrets Manager에 저장합니다.

워크로드 아이덴티티 할당: EC2에는 인스턴스 역할을, EKS Pod에는 EKS Pod Identity 또는 IRSA를 연결하고 해당 비밀 버전을 읽는 최소 권한만 부여합니다. 정적 액세스 키를 Pod 환경 변수에 저장하지 않습니다.

애플리케이션에서 접근: 애플리케이션 코드는 AWS SDK의 기본 자격 증명 공급자 체인을 통해 워크로드 아이덴티티를 사용하고, 런타임에 Secrets Manager에서 비밀 정보를 가져옵니다.

이 경우 .env 파일에 비밀 정보를 저장할 필요가 없습니다.

npm install @aws-sdk/client-secrets-manager
src/config/secrets.service.ts (AWS Secrets Manager 예시 - 개념 코드)
import { Injectable, OnModuleInit } from '@nestjs/common';
import { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager';
import * as Joi from 'joi';

interface DatabaseCredentials {
  username: string;
  password: string;
}

const databaseCredentialsSchema = Joi.object<DatabaseCredentials>({
  username: Joi.string().required(),
  password: Joi.string().min(16).required(),
}).required();

@Injectable()
export class SecretsService implements OnModuleInit {
  private readonly client = new SecretsManagerClient({
    region: 'ap-northeast-2',
  });
  private databaseCredentials!: DatabaseCredentials;

  async onModuleInit(): Promise<void> {
    this.databaseCredentials = await this.loadDatabaseCredentials();
  }

  private async loadDatabaseCredentials(): Promise<DatabaseCredentials> {
    const response = await this.client.send(
      new GetSecretValueCommand({ SecretId: 'DATABASE_CREDENTIALS' }),
    );

    if (!response.SecretString) {
      throw new Error('DATABASE_CREDENTIALS is required');
    }

    let parsed: unknown;
    try {
      parsed = JSON.parse(response.SecretString) as unknown;
    } catch {
      throw new Error('DATABASE_CREDENTIALS must be valid JSON');
    }

    const { error, value } = databaseCredentialsSchema.validate(parsed, {
      abortEarly: false,
      convert: false,
    });
    if (error) {
      throw new Error(`DATABASE_CREDENTIALS schema error: ${error.message}`);
    }

    return value;
  }

  getDatabaseCredentials(): Readonly<DatabaseCredentials> {
    return this.databaseCredentials;
  }
}

이후 다른 서비스에서 SecretsService를 주입받아 this.secretsService.getDatabaseCredentials()처럼 사용합니다. 이 코드는 필수 비밀의 조회 실패, 빈 값, JSON 파싱 실패, 스키마 오류를 삼키지 않으므로 애플리케이션 시작이 중단됩니다. 오류 로그에는 비밀 원문을 넣지 않습니다.

예시는 시작 시 값을 메모리에 보관합니다. Secret Store에서 버전을 교체한 뒤에는 캐시를 안전하게 새로고침하거나 애플리케이션을 재배포해 새 버전 사용을 확인하고, 그 다음 이전 버전과 불필요한 권한을 회수합니다.


환경 변수 관리와 비밀 정보 보호는 애플리케이션 보안의 기본입니다. 환경 변수는 설정을 전달하지만 암호화 저장소나 런타임 침해 방어선은 아닙니다.

NestJS의 ConfigModule.env 기반 환경 변수 관리를 편리하게 해주고, 유효성 검사로 설정 오류를 줄여줍니다.

운영 환경에서는 HashiCorp Vault나 클라우드 Secrets Manager 같은 전용 도구를 원본 저장소로 사용하고, CI에는 OIDC 단기 자격을, 애플리케이션에는 워크로드 아이덴티티를 부여합니다.

이 모범 사례를 지키면 애플리케이션 보안 수준을 크게 높일 수 있습니다.

마지막으로 비밀 정보는 목록화와 생성, 저장, 권한·주입, 검증·사용, 관측, 교체·회수, 검토·재배포까지 하나의 수명 주기로 관리해야 합니다.

아래 다이어그램은 값이 코드 저장소에 남지 않도록 각 단계의 소유 위치와 차단 기준을 정리합니다.

비밀 정보를 목록화하고 생성한 뒤 전용 저장소에 보관하고, 권한을 부여해 런타임에 주입하고, 검증과 사용 및 관측을 거쳐 교체와 회수, 검토와 재배포로 돌아오는 일곱 단계 운영 루프

NestJS · secret lifecycle · true loop

소유자·버전·감사 증거가 다음 교체를 준비한다

비밀 관리는 저장 한 번으로 끝나지 않습니다. 목록화·생성부터 저장, 권한·주입, 검증·사용, 관측, 교체·회수, 검토·재배포를 반복하고 모든 단계가 중앙 증거에 소유자와 버전, 결과를 남깁니다.

비밀 정보의 일곱 단계 운영 루프 목록화와 생성, 전용 저장, 권한과 주입, 검증과 사용, 관측, 교체와 회수, 검토와 재배포가 시계 방향으로 이어지고 마지막 검토가 다음 목록화로 돌아간다. 각 단계는 중앙 수명 주기 증거에 소유자, 버전과 감사 결과를 기록한다. 1. 목록화·생성 owner · purpose 2. 전용 저장 versioned secret 3. 권한·주입 Pod ID · IRSA 4. 검증·사용 schema · fail-fast 5. 관측 key · version · status 6. 교체·회수 refresh · revoke 7. 검토·재배포 change · redeploy 수명 주기 증거 owner · version audit evidence 검토·재배포 결과가 다음 목록화와 생성 기준으로 돌아간다
수명 주기 증거 owner · version · audit evidence
  1. 목록화·생성

    비밀의 소유자, 용도, 소비 서비스, 교체 기한을 정하고 관리 도구에서 값을 생성합니다.

  2. 전용 저장

    Vault나 클라우드 Secret Manager에 버전으로 저장하고 Git·이미지·공유 문서에는 값이 남지 않게 합니다.

  3. 권한·주입

    EKS Pod Identity나 IRSA 같은 워크로드 아이덴티티에 최소 읽기 권한을 주고 런타임에만 전달합니다.

  4. 검증·사용

    필수 값과 파싱한 구조를 스키마로 검증합니다. 누락되거나 잘못된 비밀이면 애플리케이션 시작을 중단합니다.

  5. 관측

    값 대신 키 이름, 버전, 접근 주체, 성공·실패 상태를 감사 증거로 기록합니다.

  6. 교체·회수

    새 버전을 만든 뒤 캐시를 새로고침하거나 재배포하고, 사용 확인 후 이전 버전과 권한을 회수합니다.

  7. 검토·재배포

    소유권·권한·기한·장애 복구 결과를 검토하고 필요한 변경을 재배포한 뒤 다음 목록화로 돌아갑니다.

inventory · store

누가 왜 쓰는지부터 버전으로 남긴다

소유자·소비자·용도·교체 기한 없는 비밀은 관리할 수 없습니다. 값은 전용 저장소에 두고 이름과 스키마만 코드에 둡니다.

inject · validate

최소 권한으로 가져와 시작 전에 검증한다

워크로드 아이덴티티로 필요한 버전만 읽고, JSON을 unknown으로 파싱한 뒤 스키마를 통과시킵니다. 필수 비밀 실패를 삼키지 않습니다.

rotate · revoke

새 버전을 확인한 뒤 이전 값을 회수한다

교체 후 캐시 refresh 또는 재배포로 새 값을 읽고 정상 동작을 확인합니다. 그 다음 이전 버전·권한을 revoke하고 증거를 검토합니다.

실선 고리는 실제 운영 순서, 안쪽 점선은 각 단계가 중앙 기록에 쓰는 증거입니다. 마지막 검토와 재배포가 다음 목록화를 바꾸므로 실제 폐루프입니다.

이것으로 10장 보안과 모범 사례의 두 번째 절을 마칩니다.