# 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<ExternalApiOptions>                    │
│ ✓ 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<ExternalApiOptions> 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<ExternalApiOptions> 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 ✅
