# Secrets Management: Complete Configuration Summary **Status:** Production-ready secrets handling via Gitea Secrets + User Secrets --- ## ๐Ÿ” Architecture ``` โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ Secret Sources (Priority) โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ 1. Environment Variables (highest) โ† CI/CD or shell export โ”‚ โ”‚ 2. User Secrets (local dev) โ† dotnet user-secrets โ”‚ โ”‚ 3. appsettings.json (lowest) โ† placeholders ${VAR_NAME} โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ†“ ResolveSecret() helper โ†“ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ Program.cs Configuration Setup โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ - KARTSELL_POSTGRES (database connection string) โ”‚ โ”‚ - KRX_API_KEY (Korea Exchange market data API) โ”‚ โ”‚ - OPENDART_API_KEY (financial disclosure API) โ”‚ โ”‚ - KIS_API_KEY + KIS_SECRET_KEY (trading API credentials) โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ†“ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ ExternalApiOptions Service โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ Injected via IOptions โ”‚ โ”‚ โœ“ Type-safe access to all API credentials โ”‚ โ”‚ โœ“ Validated at startup (no missing secrets) โ”‚ โ”‚ โœ“ No secrets in dependency injection logs โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ ``` --- ## ๐Ÿ“ Files Changed/Created ### 1. **Program.cs** (UPDATED) - Added `using KArtSell.Host.Configuration;` - Added `ResolveSecret()` helper method - Registered `ExternalApiOptions` with secret validation - Resolves KARTSELL_POSTGRES and KRX_API_KEY with priority: env โ†’ user-secrets โ†’ appsettings ### 2. **appsettings.json** (UPDATED) ```json "ConnectionStrings": { "Postgres": "${KARTSELL_POSTGRES}" }, "ExternalApis": { "KrxOpenApi": { "ApiKey": "${KRX_API_KEY}", "BaseUrl": "https://openapi.krx.co.kr" } } ``` ### 3. **Configuration/ExternalApiOptions.cs** (NEW) Type-safe options class for all external APIs: - `KrxOpenApi` (Korea Exchange) - `OpenDart` (Financial Disclosures) - `Kis` (Trading & Orders) ### 4. **.gitea/workflows/secrets-injection.yml** (NEW) CI/CD workflow that: - Receives secrets from Gitea Actions Secrets via `${{ secrets.* }}` - Injects as environment variables at build time - Prevents secrets from being logged or stored in artifacts - Runs on push/PR to main and develop ### 5. **docs/SECRETS_LOCAL_DEVELOPMENT.md** (NEW) Complete local development guide: - One-time user-secrets setup - How to set/update secrets locally - Troubleshooting guide - Best practices --- ## โœ… Setup Checklist ### Local Development (ONE-TIME) ```bash # 1. Initialize user-secrets for KArtSell.Host cd src/KArtSell.Host dotnet user-secrets init # 2. Store PostgreSQL connection dotnet user-secrets set "ConnectionStrings:Postgres" \ "Host=localhost;Port=5432;Database=kartsell;Username=kartsell;Password=kartsell" # 3. Store KRX API Key dotnet user-secrets set "ExternalApis:KrxOpenApi:ApiKey" "your-krx-key" # 4. Verify dotnet user-secrets list # Expected: 2+ entries showing your secrets # 5. Run application dotnet run -c Release ``` **Verification:** Application starts without "secret is required" errors. ### CI/CD Setup (Gitea) 1. **Add secrets to Gitea:** - Go to: https://gitea.taxbaik.com/kjh2064/KArtSell.Aegis/settings/actions/secrets - Add these secrets: - `KARTSELL_POSTGRES` = database connection string - `KRX_API_KEY` = Korea Exchange API key - `OPENDART_API_KEY` = OpenDart API key - `KIS_API_KEY` = Trading API key - `KIS_SECRET_KEY` = Trading API secret 2. **Workflow already configured:** - `.gitea/workflows/secrets-injection.yml` injects them at build time - Tests can use secrets via `${{ secrets.* }}` - No secrets stored in docker images or artifacts 3. **Verify CI/CD:** - Next push/PR build will use Gitea Secrets - Check workflow logs (secrets are masked) - Database migrations and tests pass --- ## ๐Ÿ” How ResolveSecret() Works ```csharp static string? ResolveSecret(string? configValue, string environmentVariable) { // 1. Check if environment variable is set (highest priority) var envValue = Environment.GetEnvironmentVariable(environmentVariable); if (!string.IsNullOrEmpty(envValue)) return envValue; // CI/CD sets this via ${{ secrets.* }} // 2. Check if config has a placeholder (e.g., "${VAR_NAME}") if (!string.IsNullOrEmpty(configValue)) { if (configValue.StartsWith("${") && configValue.EndsWith("}")) { // This is a placeholder, try environment return Environment.GetEnvironmentVariable(environmentVariable); } // Config has actual value (local dev via user-secrets) return configValue; } // 3. No value found return null; } ``` **Example execution:** | Scenario | configValue | envValue | Result | |----------|------------|----------|--------| | CI/CD (Gitea Secrets) | `${KARTSELL_POSTGRES}` | set by `${{ secrets.* }}` | โœ… Uses envValue | | Local dev (user-secrets) | actual value from user-secrets | not set | โœ… Uses configValue | | Missing secret | null | not set | โŒ Throws error | --- ## ๐Ÿš€ Usage in Application Code ### Inject via IOptions ```csharp public class MyDataService { private readonly ExternalApiOptions _apiOptions; public MyDataService(IOptions options) { _apiOptions = options.Value; } public async Task FetchMarketData() { var krxKey = _apiOptions.KrxOpenApi.ApiKey; // โœ“ Type-safe var krxUrl = _apiOptions.KrxOpenApi.BaseUrl; // Use krxKey and krxUrl with HTTP client } } ``` ### Benefits - โœ… Secrets never hardcoded - โœ… Type-safe access to API options - โœ… Validated at startup (fails fast if missing) - โœ… Works in both local dev and CI/CD --- ## ๐Ÿ”’ Security Properties | Property | Status | Mechanism | |----------|--------|-----------| | Secrets in code? | โŒ NO | Always from external sources | | Secrets in git? | โŒ NO | appsettings has only `${PLACEHOLDERS}` | | Secrets in logs? | โŒ NO | ResolveSecret does not log; LogsFilter redacts | | Secrets in CI artifacts? | โŒ NO | Secrets masked in workflow logs | | Local isolation? | โœ… YES | User-secrets in `~/.microsoft/usersecrets/` | | CI/CD isolation? | โœ… YES | Secrets in Gitea Actions Secrets (encrypted) | | Rotation support? | โœ… YES | Update Gitea secret โ†’ next build uses new value | --- ## ๐Ÿงช Testing with Secrets ### Unit Tests (No Secrets Needed) ```csharp [Fact] public void MyMethod_WithValidInput_ReturnsSuccess() { // No secrets needed for unit tests var policy = new MyPolicy(); var result = policy.Execute(input); Assert.True(result); } ``` ### Integration Tests (Use Test Fixtures) ```csharp [Fact] public async Task MyIntegration_ConnectsToPostgres() { // Database is set up via KARTSELL_POSTGRES env var // In CI/CD, secrets are available; locally, user-secrets provide them var factory = new NpgsqlConnectionFactory(connectionString); var connection = await factory.GetConnectionAsync(); Assert.NotNull(connection); } ``` Secrets automatically available: - **Local:** From user-secrets - **CI/CD:** From Gitea Actions Secrets (via environment) --- ## โš ๏ธ Common Mistakes & How to Avoid ### โŒ Mistake 1: Storing secrets in appsettings files ```json // DON'T { "ConnectionStrings": { "Postgres": "Host=localhost;Password=MyActualPassword" } } ``` ### โœ… Fix: Use placeholder ```json // DO { "ConnectionStrings": { "Postgres": "${KARTSELL_POSTGRES}" } } ``` ### โŒ Mistake 2: Logging configuration ```csharp // DON'T logger.Information("Database: {ConnectionString}", connectionString); ``` ### โœ… Fix: Never log secrets ```csharp // DO logger.Information("Database connection initialized"); ``` ### โŒ Mistake 3: Passing secrets as method arguments ```csharp // DON'T public async Task ConnectAsync(string apiKey) { // DON'T: apiKey might be logged in stack traces } ``` ### โœ… Fix: Use IOptions injection ```csharp // DO public MyService(IOptions options) { _apiKey = options.Value.KrxOpenApi.ApiKey; // Injected, not passed } ``` --- ## ๐Ÿ“ž Support & Troubleshooting | Issue | Solution | Reference | |-------|----------|-----------| | "ConnectionStrings:Postgres is required" | Set via `dotnet user-secrets` | SECRETS_LOCAL_DEVELOPMENT.md | | "KRX_API_KEY is required" | Add to Gitea Actions Secrets | SECRETS_LOCAL_DEVELOPMENT.md | | Secrets showing in logs | Report security issue immediately | SECRETS_LOCAL_DEVELOPMENT.md | | CI/CD build fails with auth error | Verify Gitea Secrets are set | .gitea/workflows/secrets-injection.yml | | Local test fails but CI passes | Use same KARTSELL_POSTGRES | SECRETS_LOCAL_DEVELOPMENT.md | --- ## ๐Ÿ“š Related Documentation - **Local Dev Setup:** `docs/SECRETS_LOCAL_DEVELOPMENT.md` - **CI/CD Workflow:** `.gitea/workflows/secrets-injection.yml` - **ExternalApiOptions:** `src/KArtSell.Host/Configuration/ExternalApiOptions.cs` - **Program Configuration:** `src/KArtSell.Host/Program.cs` (ResolveSecret method) - **CLAUDE.md Secrets Section:** `CLAUDE.md` (Gitea API Automation section) --- ## โœจ Next Steps 1. **Immediate:** - [ ] Run local user-secrets setup (SECRETS_LOCAL_DEVELOPMENT.md) - [ ] Test application startup (no "secret is required" errors) - [ ] Verify Hangfire dashboard loads at http://localhost:5000/hangfire 2. **CI/CD (Gitea Secrets):** - [ ] Add secrets to https://gitea.taxbaik.com/kjh2064/KArtSell.Aegis/settings/actions/secrets - [ ] Next push/PR will use `.gitea/workflows/secrets-injection.yml` - [ ] Verify build passes with secrets 3. **Ongoing:** - [ ] Rotate API keys quarterly - [ ] Review logs for any secret leaks (should be none) - [ ] Add new APIs following ExternalApiOptions pattern --- **Last Updated:** 2026-08-02 **Status:** Production-Ready โœ