283 lines
6.7 KiB
Markdown
283 lines
6.7 KiB
Markdown
|
|
|
|
# EonaCat.SecretVault
|
|
|
|
------------
|
|
|
|
Generate a secure vault for storing secrets like API keys, passwords, and other sensitive information.
|
|
|
|
## Overview
|
|
EonaCat.SecretVault is a secure multi-tenant offline vault for storing secrets using AES encryption and SQLite, protected with API key authentication and per-tenant encryption keys.
|
|
It consists of a server that manages the storage and retrieval of secrets, and a client library that allows you to interact with the server.
|
|
The server provides a RESTful API for storing and retrieving secrets, while the client library simplifies the interaction with the server.
|
|
|
|
## Features
|
|
- Secure storage of secrets
|
|
- RESTful API for storing and retrieving secrets
|
|
- Client library for easy interaction with the server
|
|
- Multi-tenancy support
|
|
- Supports HMAC authentication for secure access
|
|
- Supports SQLite for data storage
|
|
- Audit logging
|
|
- Cross-platform compatibility
|
|
- Easy to use
|
|
- Open-source and free to use
|
|
|
|
# Extra security notices:
|
|
- Rotate your API keys regularly.
|
|
- Use strong, unique API keys for each tenant.
|
|
- Ensure your `ROOT_PROTECTOR_KEY` is kept secret and secure.
|
|
- Rotate the `ROOT_PROTECTOR_KEY` if you suspect it has been compromised.
|
|
- Use HTTPS to encrypt data in transit.
|
|
|
|
### API Keys
|
|
|
|
- API keys are generated as secure random 256-bit strings.
|
|
- Only SHA256 hashes of API keys are stored in the database.
|
|
- Incoming API keys are hashed and compared against stored hashes for authentication.
|
|
|
|
### Per-Tenant Encryption Keys
|
|
|
|
- Each tenant has a unique 256-bit AES key, encrypted at rest with a master root key (`ROOT_PROTECTOR_KEY`).
|
|
- Tenant keys are decrypted only in-memory during operations.
|
|
|
|
## 🧩 Multi-Tenant Support
|
|
Each API key is tied to a unique tenant.
|
|
Secrets stored by one tenant cannot be retrieved by another.
|
|
|
|
Tenant mapping is defined in the TenantApiKey table:
|
|
|
|
ApiKey TenantId
|
|
abc123-myapikey tenant-001
|
|
def456-otherkey tenant-002
|
|
|
|
## 🔍 Audit Logs
|
|
|
|
Every secret operation is logged in the AuditLogs table:
|
|
|
|
Id TenantId Action Key PerformedBy Timestamp
|
|
1 tenant-001 Store db-password apikey-user 2025-07-06 17:23:54
|
|
2 tenant-001 Retrieve db-password apikey-user 2025-07-06 17:25:04
|
|
|
|
|
|
HMAC Integrity (Optional)
|
|
The server computes HMAC-SHA256 for internal verification.
|
|
|
|
You can expand this to validate incoming payloads by signing client-side with:
|
|
|
|
```csharp
|
|
var hmac = new HMACSHA256(Convert.FromBase64String(masterKey));
|
|
var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(content));
|
|
```
|
|
|
|
### Environment Variables
|
|
|
|
- `ROOT_PROTECTOR_KEY`: Base64-encoded 256-bit key to encrypt tenant keys.
|
|
|
|
### Example of ROOT_PROTECTOR_KEY:
|
|
```bash
|
|
export ROOT_PROTECTOR_KEY="your-base64-encoded-256-bit-key"
|
|
```
|
|
|
|
e.g.:
|
|
```bash
|
|
export ROOT_PROTECTOR_KEY="c2VjcmV0cGFzc3dvcmQtbm90LXN0b3JlLWluLWRpc3BsYXktb3V0c2lkZS1lbnZpcm9ubWVudC1rZXktdG8tYmUtdXNlZC1mb3ItZW5jcnlwdGlvbi1rZXktZm9yLXNlY3JldC1zdG9yYWdl"
|
|
```
|
|
|
|
EonaCat.SecretVault.Client library
|
|
|
|
## How to use the client:
|
|
|
|
```csharp
|
|
using EonaCat.SecretVault.Client;
|
|
|
|
var vault = new SecretVaultClient("https://your-vault-url.com/", "your-api-key");
|
|
|
|
// Store a secret
|
|
await vault.StoreAsync("db-password", "S3cr3t!");
|
|
|
|
// Retrieve a secret
|
|
string? secret = await vault.RetrieveAsync("db-password");
|
|
Console.WriteLine($"Secret: {secret}");
|
|
|
|
// Delete a secret
|
|
await vault.DeleteAsync("db-password");
|
|
|
|
```
|
|
|
|
## How to use the server:
|
|
|
|
## Generating a new tenant:
|
|
|
|
```csharp
|
|
|
|
string GenerateApiKey()
|
|
{
|
|
return Convert.ToBase64String(RandomNumberGenerator.GetBytes(32));
|
|
}
|
|
|
|
string HashApiKey(string apiKey)
|
|
{
|
|
using var sha256 = SHA256.Create();
|
|
var hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(apiKey));
|
|
return Convert.ToBase64String(hash);
|
|
}
|
|
|
|
var rootKey = Environment.GetEnvironmentVariable("ROOT_PROTECTOR_KEY")!;
|
|
var tenantKey = Convert.ToBase64String(RandomNumberGenerator.GetBytes(32));
|
|
|
|
var encryptedTenantKey = TenantKeyProtector.EncryptTenantKey(tenantKey, rootKey);
|
|
var apiKey = GenerateApiKey();
|
|
var apiKeyHash = HashApiKey(apiKey);
|
|
|
|
_db.ApiKeys.Add(new TenantApiKey
|
|
{
|
|
ApiKey = newApiKey,
|
|
TenantId = "tenant-003",
|
|
EncryptionKey = newTenantKey,
|
|
EncryptedTenantKey = encryptedTenantKey,
|
|
ApiKeyHash = apiKeyHash
|
|
});
|
|
await _db.SaveChangesAsync();
|
|
```
|
|
### Edit appsettings.json or use environment variables if needed (default uses SQLite):
|
|
```json
|
|
{
|
|
"ConnectionStrings": {
|
|
"Default": "Data Source=EonaCatSecretVault.db"
|
|
}
|
|
}
|
|
```
|
|
|
|
### Api usage:
|
|
Base URL: http://localhost:5000/api/secrets
|
|
All requests must include:
|
|
- `X-Api-Key` header with your API key
|
|
|
|
## Generate a new tenant:
|
|
|
|
POST /api/admin/generate-key?role=ReadWrite
|
|
|
|
```http
|
|
Headers:
|
|
X-Api-Key: abc123-myapikey
|
|
### Response:
|
|
HTTP/1.1 200
|
|
Body:
|
|
{
|
|
"apiKey": "qY6LZ5i1dNpsQFF2kLE7uUd8Md5FeKfMY5r6KfzH7i8=",
|
|
"role": "ReadWrite"
|
|
}
|
|
```
|
|
|
|
## Revoke an API key:
|
|
|
|
DELETE /api/admin/revoke-key?key=abc123-myapikey
|
|
```http
|
|
Headers:
|
|
X-Api-Key: abc123-myapikey
|
|
```
|
|
|
|
### Response:
|
|
```http
|
|
HTTP/1.1 200 OK
|
|
Body:
|
|
"Key revoked"
|
|
```
|
|
|
|
## List all API keys:
|
|
GET /api/admin/keys
|
|
```http
|
|
Headers:
|
|
X-Api-Key: abc123-myapikey
|
|
```
|
|
|
|
### Response:
|
|
```http
|
|
HTTP/1.1 200 OK
|
|
Body:
|
|
[
|
|
{
|
|
"apiKey": "qY6LZ5i1dNpsQFF2kLE7uUd8Md5FeKfMY5r6KfzH7i8=",
|
|
"role": "ReadWrite",
|
|
"isActive": true,
|
|
"createdAt": "2025-07-06T17:00:00Z"
|
|
},
|
|
...
|
|
]
|
|
```
|
|
|
|
## Store a secret:
|
|
|
|
POST /api/secrets/store?key=my-service-token
|
|
|
|
```http
|
|
Headers:
|
|
Content-Type: application/json
|
|
X-Api-Key: abc123-myapikey
|
|
```
|
|
|
|
### Response:
|
|
```http
|
|
HTTP/1.1 200 OK
|
|
```
|
|
|
|
## Retrieve a secret:
|
|
GET /api/secrets/retrieve?key=my-service-token
|
|
```http
|
|
Headers:
|
|
X-Api-Key: abc123-myapikey
|
|
```
|
|
|
|
### Response:
|
|
```http
|
|
HTTP/1.1 200 OK
|
|
Body:
|
|
"super-secret-value"
|
|
```
|
|
|
|
```csharp
|
|
using EonaCat.SecretVault.Data;
|
|
using EonaCat.SecretVault.Middleware;
|
|
using EonaCat.SecretVault.Services;
|
|
using Microsoft.AspNetCore.Builder;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using System;
|
|
|
|
var builder = WebApplication.CreateBuilder(args);
|
|
|
|
var config = builder.Configuration;
|
|
|
|
builder.Services.AddDbContext<SecretDbContext>(x => x.UseSqlite("Data Source=EonaCatSecretVault.db"));
|
|
builder.Services.AddScoped<SecretService>();
|
|
builder.Services.AddScoped<TenantKeyService>();
|
|
builder.Services.AddControllers();
|
|
|
|
var app = builder.Build();
|
|
|
|
app.UseHsts();
|
|
app.UseHttpsRedirection();
|
|
app.UseMiddleware<ApiKeyMiddleware>();
|
|
app.MapControllers();
|
|
|
|
using (var scope = app.Services.CreateScope())
|
|
{
|
|
var db = scope.ServiceProvider.GetRequiredService<SecretDbContext>();
|
|
db.Database.EnsureCreated();
|
|
}
|
|
|
|
app.Run();
|
|
```
|
|
|
|
## Curl Example:
|
|
|
|
```curl
|
|
curl -X POST http://localhost:5000/api/secrets/store?key=MYKEY \
|
|
-H "X-Api-Key: abc123-myapikey" \
|
|
-H "Content-Type: application/json" \
|
|
-d "\"sensitive-data\""
|
|
|
|
curl http://localhost:5000/api/secrets/retrieve?key=MYKEY \
|
|
-H "X-Api-Key: abc123-myapikey"
|
|
``` |