diff --git a/API.md b/API.md new file mode 100644 index 0000000..b75e284 --- /dev/null +++ b/API.md @@ -0,0 +1,1803 @@ +# GitCaddy Vault API Reference + +**Version:** v1.0.0 (dev) +**License:** Business Source License 1.1 +**Copyright:** 2026 MarketAlly. All rights reserved. + +GitCaddy Vault is a secure secrets management plugin for GitCaddy (Gitea fork) that provides encrypted storage for sensitive data like API keys, passwords, certificates, and environment variables. It uses AES-256-GCM encryption with a two-tier key architecture (master KEK + per-repository DEK) and includes features like version history, audit logging, and scoped API tokens. + +--- + +## Table of Contents + +- [Overview](#overview) +- [Architecture](#architecture) +- [Installation](#installation) +- [Configuration](#configuration) +- [Authentication](#authentication) +- [API Endpoints](#api-endpoints) + - [Secrets](#secrets) + - [Versions](#versions) + - [Tokens](#tokens) + - [Audit Log](#audit-log) + - [Key Management](#key-management) +- [Go Package API](#go-package-api) + - [Crypto Package](#crypto-package) + - [License Package](#license-package) + - [Models Package](#models-package) + - [Services Package](#services-package) +- [Error Codes](#error-codes) +- [Code Examples](#code-examples) +- [License Tiers](#license-tiers) + +--- + +## Overview + +GitCaddy Vault provides: + +- **Encrypted Secret Storage**: AES-256-GCM encryption for all secrets +- **Version History**: Track changes to secrets over time with rollback capability +- **Audit Logging**: Complete audit trail of all vault operations +- **Scoped API Tokens**: Fine-grained access control for CI/CD pipelines +- **Key Rotation**: Enterprise-tier DEK rotation for enhanced security +- **Multi-language Support**: 25+ locale translations included +- **Lockbox Mode**: Client-side end-to-end encryption support + +The plugin integrates directly into GitCaddy's repository interface, adding a "Vault" tab to each repository for managing secrets. + +--- + +## Architecture + +### Encryption Model + +GitCaddy Vault uses a two-tier key architecture: + +1. **Master Key (KEK - Key Encryption Key)**: A 256-bit key configured at the instance level +2. **Data Encryption Keys (DEK)**: Per-repository 256-bit keys encrypted with the master KEK + +All secret values are encrypted with the repository's DEK, which itself is encrypted with the master KEK. This allows for key rotation without re-encrypting all secrets. + +### Key Hierarchy + +``` +Master Key (KEK) - 256-bit AES key + └── Repository DEK - 256-bit AES key (encrypted with KEK) + └── Secret Value - Encrypted with DEK using AES-256-GCM +``` + +### Database Schema + +The plugin adds five database tables: + +- `vault_secret`: Secret metadata (name, type, description) +- `vault_secret_version`: Encrypted secret values with version history +- `vault_repo_key`: Per-repository encrypted DEKs +- `vault_token`: API access tokens for CI/CD +- `vault_audit_entry`: Complete audit log + +--- + +## Installation + +### Prerequisites + +- GitCaddy v3.0+ (Gitea fork) +- Go 1.21+ (for building from source) +- PostgreSQL, MySQL, or SQLite database + +### Building + +```bash +# Clone the repository +git clone https://git.marketally.com/gitcaddy/gitcaddy-vault.git +cd gitcaddy-vault + +# Build the plugin +go build -ldflags "-X git.marketally.com/gitcaddy/gitcaddy-vault.Version=v1.0.0" + +# The plugin is automatically registered via init() in plugin.go +``` + +### Installation + +1. Copy the built plugin to your GitCaddy plugins directory +2. Configure the master encryption key (see [Configuration](#configuration)) +3. Restart GitCaddy +4. The plugin will automatically create database tables on first startup + +--- + +## Configuration + +### Master Key Configuration + +The master encryption key can be configured in three ways (priority order): + +1. **app.ini configuration** (recommended): + +```ini +[vault] +MASTER_KEY = 64_character_hex_string_here +``` + +2. **Environment variable**: + +```bash +export GITCADDY_VAULT_KEY=64_character_hex_string_here +``` + +3. **Key file**: + +```bash +# Create key file +echo "your_64_char_hex_key" > /etc/gitcaddy/vault.key + +# Or specify custom location +export GITCADDY_VAULT_KEY_FILE=/path/to/vault.key +``` + +### Generating a Master Key + +```bash +# Generate a 256-bit (32-byte) key as hex +openssl rand -hex 32 +``` + +**Important**: Store the master key securely. If lost, all encrypted secrets become permanently unrecoverable. + +### Fallback Behavior + +If no master key is configured, the vault will fall back to using Gitea's `SECRET_KEY`. This is **not recommended** for production as changing the `SECRET_KEY` will break all vault secrets. + +### License Configuration + +Place your license file at one of these locations: + +- `/etc/gitcaddy/license.key` +- `./custom/license.key` +- `./license.key` + +Or set via environment: + +```bash +export GITCADDY_LICENSE_KEY="base64_encoded_license_json" +export GITCADDY_LICENSE_FILE=/path/to/license.key +``` + +### Development Mode + +Skip license checks during development: + +```bash +export GITCADDY_DEV_MODE=1 +export GITCADDY_SKIP_LICENSE_CHECK=1 +``` + +--- + +## Authentication + +### Web UI Authentication + +Web routes use GitCaddy's standard session-based authentication. Users must have repository access (read for viewing, write for creating/updating). + +### API Authentication + +API routes support two authentication methods: + +#### 1. GitCaddy Session Token + +```bash +curl -H "Authorization: Bearer " \ + https://git.example.com/api/v1/repos/owner/repo/vault/secrets +``` + +#### 2. Vault Token (CI/CD) + +```bash +curl -H "Authorization: Bearer gvt_" \ + https://git.example.com/api/v1/repos/owner/repo/vault/secrets/SECRET_NAME +``` + +Vault tokens are prefixed with `gvt_` and provide scoped access to secrets. + +--- + +## API Endpoints + +All API endpoints are under `/api/v1/repos/{owner}/{repo}/vault`. + +### Secrets + +#### List Secrets + +```http +GET /api/v1/repos/{owner}/{repo}/vault/secrets +``` + +**Query Parameters:** + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `include_deleted` | boolean | `false` | Include soft-deleted secrets | + +**Response:** + +```json +[ + { + "name": "DATABASE_URL", + "description": "Production database connection string", + "type": "password", + "encryption_mode": "standard", + "current_version": 3, + "created_at": 1704067200, + "updated_at": 1704153600, + "is_deleted": false + } +] +``` + +**Supported Types:** `env-file`, `key-value`, `file`, `certificate`, `ssh-key` + +--- + +#### Get Secret + +```http +GET /api/v1/repos/{owner}/{repo}/vault/secrets/{name} +``` + +**Query Parameters:** + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `version` | integer | current | Specific version to retrieve | + +**Response:** + +```json +{ + "name": "DATABASE_URL", + "description": "Production database connection string", + "type": "password", + "encryption_mode": "standard", + "current_version": 3, + "created_at": 1704067200, + "updated_at": 1704153600, + "value": "postgresql://user:pass@localhost/db" +} +``` + +**Authentication:** Requires read permission or valid vault token with `read` scope. + +--- + +#### Create or Update Secret + +```http +PUT /api/v1/repos/{owner}/{repo}/vault/secrets/{name} +``` + +**Request Body:** + +```json +{ + "name": "DATABASE_URL", + "description": "Production database connection string", + "type": "password", + "value": "postgresql://user:pass@localhost/db", + "comment": "Updated password", + "encryption_mode": "standard" +} +``` + +**Parameters:** + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `name` | string | Yes | Secret name (alphanumeric, `_`, `-`) | +| `value` | string | Yes | Secret value (will be encrypted) | +| `description` | string | No | Human-readable description | +| `type` | string | No | Secret type (default: `env-file`) | +| `comment` | string | No | Version comment | +| `encryption_mode` | string | No | `standard` or `lockbox` (default: `standard`) | + +**Encryption Modes:** + +- `standard`: Server-side encryption (default) +- `lockbox`: Client-side E2E encryption (value must be pre-encrypted with `lockbox:v1:` prefix) + +**Response (201 Created or 200 OK):** + +```json +{ + "name": "DATABASE_URL", + "description": "Production database connection string", + "type": "password", + "encryption_mode": "standard", + "current_version": 1, + "created_at": 1704067200, + "updated_at": 1704067200 +} +``` + +**Authentication:** Requires write permission or vault token with `write` scope. + +--- + +#### Delete Secret + +```http +DELETE /api/v1/repos/{owner}/{repo}/vault/secrets/{name} +``` + +Soft-deletes the secret (can be restored later). + +**Response:** + +```json +{ + "message": "Secret deleted" +} +``` + +**Authentication:** Requires write permission. + +--- + +#### Restore Secret + +```http +POST /api/v1/repos/{owner}/{repo}/vault/secrets/{name}/restore +``` + +Restores a soft-deleted secret. + +**Response:** + +```json +{ + "message": "Secret restored" +} +``` + +**Authentication:** Requires write permission. + +--- + +### Versions + +#### List Versions + +```http +GET /api/v1/repos/{owner}/{repo}/vault/secrets/{name}/versions +``` + +**Response:** + +```json +[ + { + "version": 3, + "comment": "Updated password", + "created_by": 123, + "created_at": 1704153600 + }, + { + "version": 2, + "comment": "Rotated credentials", + "created_by": 123, + "created_at": 1704110400 + }, + { + "version": 1, + "comment": "", + "created_by": 123, + "created_at": 1704067200 + } +] +``` + +--- + +#### Rollback Secret + +```http +POST /api/v1/repos/{owner}/{repo}/vault/secrets/{name}/rollback +``` + +**Request Body:** + +```json +{ + "version": 2 +} +``` + +Creates a new version with the content from the specified version. + +**Response:** + +```json +{ + "message": "Secret rolled back to version 2" +} +``` + +**Authentication:** Requires write permission. + +--- + +### Tokens + +#### List Tokens + +```http +GET /api/v1/repos/{owner}/{repo}/vault/tokens +``` + +**Response:** + +```json +[ + { + "id": 1, + "description": "CI/CD Pipeline", + "scope": "read:*", + "created_at": 1704067200, + "expires_at": 1706745600, + "last_used_at": 1704153600, + "used_count": 42, + "is_revoked": false + } +] +``` + +**Authentication:** Requires admin permission. + +--- + +#### Create Token + +```http +POST /api/v1/repos/{owner}/{repo}/vault/tokens +``` + +**Request Body:** + +```json +{ + "description": "CI/CD Pipeline", + "scope": "read:*", + "ttl": "30d" +} +``` + +**Parameters:** + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `description` | string | Yes | Token description | +| `scope` | string | No | Access scope (default: `read`) | +| `ttl` | string | No | Time to live (default: `30d`) | + +**Scope Format:** + +- `read:*` - Read all secrets +- `write:*` - Read and write all secrets +- `read:prod.*` - Read secrets starting with `prod.` +- `write:DATABASE_URL` - Write only `DATABASE_URL` +- `admin` - Full admin access (manage tokens, rotate keys) + +**TTL Format:** + +- `24h` - 24 hours +- `7d` - 7 days +- `30d` - 30 days +- `1y` - 1 year +- `0` - Never expires + +**Response (201 Created):** + +```json +{ + "id": 1, + "description": "CI/CD Pipeline", + "scope": "read:*", + "created_at": 1704067200, + "expires_at": 1706745600, + "token": "gvt_a1b2c3d4e5f6..." +} +``` + +**Important:** The `token` field is only shown once. Store it securely. + +**Authentication:** Requires admin permission. + +--- + +#### Revoke Token + +```http +DELETE /api/v1/repos/{owner}/{repo}/vault/tokens/{id} +``` + +**Response:** + +```json +{ + "message": "Token revoked" +} +``` + +**Authentication:** Requires admin permission. + +--- + +#### Token Introspection + +```http +GET /api/v1/repos/{owner}/{repo}/vault/token/info +``` + +Returns information about the current token (useful for clients to check their permissions). + +**Authentication:** Requires valid vault token in `Authorization` header. + +**Response:** + +```json +{ + "scope": "read:*", + "description": "CI/CD Pipeline", + "expires_at": 1706745600, + "can_read": true, + "can_write": false, + "is_admin": false +} +``` + +--- + +### Audit Log + +#### Get Audit Entries + +```http +GET /api/v1/repos/{owner}/{repo}/vault/audit +``` + +**Query Parameters:** + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `page` | integer | 1 | Page number | +| `page_size` | integer | 50 | Entries per page (max 100) | + +**Response:** + +```json +{ + "entries": [ + { + "id": 123, + "secret_id": 45, + "action": "read", + "user_id": 1, + "ip_address": "192.168.1.100", + "success": true, + "message": "", + "timestamp": 1704067200 + } + ], + "total": 150, + "page": 1, + "pages": 3 +} +``` + +**Actions:** `list`, `read`, `write`, `delete`, `restore`, `rollback`, `rotate-key` + +**Authentication:** Requires admin permission. + +--- + +### Key Management + +#### Rotate DEK (Enterprise) + +```http +POST /api/v1/repos/{owner}/{repo}/vault/rotate-key +``` + +Generates a new Data Encryption Key (DEK) for the repository and re-encrypts all secrets. + +**Response:** + +```json +{ + "message": "DEK rotation completed successfully" +} +``` + +**Authentication:** Requires admin permission and Enterprise license. + +**Note:** This operation can take several minutes for repositories with many secrets. + +--- + +#### Migrate Key + +```http +POST /api/v1/repos/{owner}/{repo}/vault/migrate-key +``` + +Migrates secrets from an old master key to the current master key. Used when the master KEK changes. + +**Request Body:** + +```json +{ + "old_key": "old_64_char_hex_key_here", + "repo_id": 0 +} +``` + +**Parameters:** + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `old_key` | string | Yes | Old master key (hex or raw) | +| `repo_id` | integer | No | Specific repo (0 = all repos, admin only) | + +**Response:** + +```json +{ + "message": "Key migration completed", + "success_count": 10, + "failed_count": 0, + "failed_repos": [] +} +``` + +**Authentication:** Requires admin permission. + +--- + +## Go Package API + +### Crypto Package + +The `crypto` package provides encryption primitives. + +#### Manager + +```go +type Manager struct { + masterKey []byte + usingFallback bool + keySource string +} +``` + +**Methods:** + +```go +// NewManager creates a new crypto manager +func NewManager() *Manager + +// LoadMasterKey loads the master key from configured source +func (m *Manager) LoadMasterKey() error + +// HasMasterKey returns true if a master key is loaded +func (m *Manager) HasMasterKey() bool + +// IsUsingFallbackKey returns true if using Gitea's SECRET_KEY +func (m *Manager) IsUsingFallbackKey() bool + +// KeySource returns where the master key was loaded from +func (m *Manager) KeySource() string + +// SetKey sets the master key directly (32 bytes) +func (m *Manager) SetKey(key []byte) + +// Encrypt encrypts plaintext using AES-256-GCM +func (m *Manager) Encrypt(plaintext []byte, key []byte) ([]byte, error) + +// Decrypt decrypts ciphertext using AES-256-GCM +func (m *Manager) Decrypt(ciphertext []byte, key []byte) ([]byte, error) + +// EncryptWithMasterKey encrypts plaintext using the master KEK +func (m *Manager) EncryptWithMasterKey(plaintext []byte) ([]byte, error) + +// DecryptWithMasterKey decrypts ciphertext using the master KEK +func (m *Manager) DecryptWithMasterKey(ciphertext []byte) ([]byte, error) + +// EncryptDEK encrypts a DEK with the master KEK +func (m *Manager) EncryptDEK(dek []byte) ([]byte, error) + +// DecryptDEK decrypts a DEK with the master KEK +func (m *Manager) DecryptDEK(encryptedDEK []byte) ([]byte, error) +``` + +**Package-level Functions:** + +```go +// LoadMasterKey loads the master key using the default manager +func LoadMasterKey() error + +// HasMasterKey checks if the default manager has a master key +func HasMasterKey() bool + +// IsUsingFallbackKey checks if using Gitea's SECRET_KEY +func IsUsingFallbackKey() bool + +// KeySource returns the key source of the default manager +func KeySource() string + +// EncryptWithMasterKey encrypts using the default manager +func EncryptWithMasterKey(plaintext []byte) ([]byte, error) + +// DecryptWithMasterKey decrypts using the default manager +func DecryptWithMasterKey(ciphertext []byte) ([]byte, error) + +// EncryptDEK encrypts a DEK using the default manager +func EncryptDEK(dek []byte) ([]byte, error) + +// DecryptDEK decrypts a DEK using the default manager +func DecryptDEK(encryptedDEK []byte) ([]byte, error) + +// EncryptSecret encrypts a secret using the default manager +func EncryptSecret(plaintext []byte, dek []byte) ([]byte, error) + +// DecryptSecret decrypts a secret using the default manager +func DecryptSecret(ciphertext []byte, dek []byte) ([]byte, error) + +// GenerateDEK generates a new 32-byte DEK +func GenerateDEK() ([]byte, error) + +// GetFallbackKey returns Gitea's SECRET_KEY as bytes +func GetFallbackKey() []byte + +// HasDedicatedMasterKey returns true if not using fallback +func HasDedicatedMasterKey() bool +``` + +**Errors:** + +```go +var ( + ErrInvalidKey = errors.New("invalid encryption key") + ErrDecryptionFailed = errors.New("decryption failed") + ErrNoMasterKey = errors.New("no master key configured") +) +``` + +--- + +### License Package + +The `license` package manages license validation. + +#### Manager + +```go +type Manager struct { + license *License + publicKey ed25519.PublicKey +} +``` + +**Methods:** + +```go +// NewManager creates a new license manager +func NewManager() *Manager + +// Load loads the license from file or environment +func (m *Manager) Load() error + +// Validate validates the current license +func (m *Manager) Validate() error + +// Info returns information about the current license +func (m *Manager) Info() *Info + +// IsValid returns true if the license is valid +func (m *Manager) IsValid() bool + +// GetTier returns the current license tier +func (m *Manager) GetTier() Tier + +// GetLimits returns the feature limits for the current tier +func (m *Manager) GetLimits() Limits + +// CanUseFeature checks if a feature is available +func (m *Manager) CanUseFeature(feature string) bool +``` + +#### Types + +```go +type Tier string + +const ( + TierSolo Tier = "solo" + TierPro Tier = "pro" + TierTeam Tier = "team" + TierEnterprise Tier = "enterprise" +) + +type Limits struct { + Users int `json:"users"` + SecretsPerRepo int `json:"secrets_per_repo"` // -1 = unlimited + AuditRetentionDays int `json:"audit_retention_days"` + SSOEnabled bool `json:"sso_enabled"` + VersioningEnabled bool `json:"versioning_enabled"` + CICDTokensEnabled bool `json:"cicd_tokens_enabled"` +} + +type License struct { + LicenseID string `json:"license_id"` + CustomerEmail string `json:"customer_email"` + Tier Tier `json:"tier"` + Limits Limits `json:"limits"` + IssuedAt int64 `json:"issued_at"` + ExpiresAt int64 `json:"expires_at"` + Signature string `json:"signature"` +} + +type Info struct { + Valid bool + Tier string + CustomerEmail string + ExpiresAt int64 + GracePeriod bool + Limits Limits +} +``` + +**Default Limits:** + +```go +func DefaultLimitsForTier(tier Tier) Limits +``` + +| Tier | Users | Secrets/Repo | Audit Retention | SSO | Versioning | CI/CD Tokens | +|------|-------|--------------|-----------------|-----|------------|--------------| +| Solo | 1 | 5 | 7 days | No | No | No | +| Pro | 5 | Unlimited | 90 days | No | Yes | Yes | +| Team | 25 | Unlimited | 365 days | Yes | Yes | Yes | +| Enterprise | Unlimited | Unlimited | Custom | Yes | Yes | Yes | + +**Errors:** + +```go +var ( + ErrNoLicense = errors.New("no license file found") + ErrInvalidLicense = errors.New("invalid license") + ErrExpiredLicense = errors.New("license expired") + ErrInvalidSignature = errors.New("invalid license signature") +) +``` + +--- + +### Models Package + +The `models` package defines database models. + +#### VaultSecret + +```go +type VaultSecret struct { + ID int64 + RepoID int64 + Name string + Description string + Type SecretType + CurrentVersion int + CreatedUnix timeutil.TimeStamp + CreatedBy int64 + UpdatedUnix timeutil.TimeStamp + EncryptionMode string // "standard" or "lockbox" + DeletedUnix timeutil.TimeStamp // Soft delete + DeletedBy int64 + PurgedUnix timeutil.TimeStamp // Hard delete +} + +type SecretType string + +const ( + SecretTypeEnvFile SecretType = "env-file" + SecretTypeKeyValue SecretType = "key-value" + SecretTypeFile SecretType = "file" + SecretTypeCertificate SecretType = "certificate" + SecretTypeSSHKey SecretType = "ssh-key" +) +``` + +**Methods:** + +```go +func (s *VaultSecret) IsActive() bool +func (s *VaultSecret) IsDeleted() bool +func (s *VaultSecret) IsPurged() bool +``` + +**Functions:** + +```go +func CreateVaultSecret(ctx context.Context, secret *VaultSecret) error +func GetVaultSecretByID(ctx context.Context, id int64) (*VaultSecret, error) +func GetVaultSecretByName(ctx context.Context, repoID int64, name string) (*VaultSecret, error) +func UpdateVaultSecret(ctx context.Context, secret *VaultSecret, cols ...string) error +func SoftDeleteVaultSecret(ctx context.Context, secret *VaultSecret, deletedBy int64) error +func RestoreVaultSecret(ctx context.Context, secret *VaultSecret) error +func PurgeVaultSecret(ctx context.Context, secret *VaultSecret) error +func GetVaultSecretsByRepo(ctx context.Context, repoID int64, includeDeleted bool) ([]*VaultSecret, error) +func CountVaultSecretsByRepo(ctx context.Context, repoID int64) (int64, error) +``` + +--- + +#### VaultSecretVersion + +```go +type VaultSecretVersion struct { + ID int64 + SecretID int64 + Version int + EncryptedValue []byte + CreatedUnix timeutil.TimeStamp + CreatedBy int64 + Comment string + CreatorName string // Non-DB field + Creator any // Non-DB field (for UI) +} +``` + +**Functions:** + +```go +func CreateVaultSecretVersion(ctx context.Context, version *VaultSecretVersion) error +func GetVaultSecretVersion(ctx context.Context, secretID int64, versionNum int) (*VaultSecretVersion, error) +func GetLatestVaultSecretVersion(ctx context.Context, secretID int64) (*VaultSecretVersion, error) +func GetVaultSecretVersions(ctx context.Context, secretID int64) ([]*VaultSecretVersion, error) +func CountVaultSecretVersions(ctx context.Context, secretID int64) (int64, error) +func DeleteOldestVaultSecretVersion(ctx context.Context, secretID int64) error +func PruneVaultSecretVersions(ctx context.Context, secretID int64, maxVersions int) error +func CreateNewVersion(ctx context.Context, secret *VaultSecret, encryptedValue []byte, createdBy int64, comment string, maxVersions int) (*VaultSecretVersion, error) +func RollbackToVersion(ctx context.Context, secret *VaultSecret, targetVersion int, createdBy int64, maxVersions int) (*VaultSecretVersion, error) +``` + +--- + +#### VaultRepoKey + +```go +type VaultRepoKey struct { + ID int64 + RepoID int64 + EncryptedDEK []byte // DEK encrypted with master KEK + KeyVersion int + CreatedUnix timeutil.TimeStamp + RotatedUnix timeutil.TimeStamp + RotatedBy int64 + PreviousKeyData []byte // Old key for migration +} +``` + +**Functions:** + +```go +func GenerateDEK() ([]byte, error) +func GetVaultRepoKey(ctx context.Context, repoID int64) (*VaultRepoKey, error) +func CreateVaultRepoKey(ctx context.Context, key *VaultRepoKey) error +func UpdateVaultRepoKey(ctx context.Context, key *VaultRepoKey, cols ...string) error +func GetOrCreateVaultRepoKey(ctx context.Context, repoID int64, encryptDEK func([]byte) ([]byte, error)) (*VaultRepoKey, error) +func RotateVaultRepoKey(ctx context.Context, key *VaultRepoKey, userID int64, encryptDEK func([]byte) ([]byte, error), reEncryptSecrets func(oldDEK, newDEK []byte) error, decryptDEK func([]byte) ([]byte, error)) error +func MigrateVaultRepoKey(ctx context.Context, key *VaultRepoKey, decryptWithOldKey func([]byte) ([]byte, error), encryptWithNewKey func([]byte) ([]byte, error)) error +func GetAllVaultRepoKeys(ctx context.Context) ([]*VaultRepoKey, error) +func DeleteVaultRepoKey(ctx context.Context, repoID int64) error +``` + +--- + +#### VaultToken + +```go +type VaultToken struct { + ID int64 + RepoID int64 + TokenHash string // SHA-256 hash + Scope TokenScope + Description string + ExpiresUnix timeutil.TimeStamp + CreatedUnix timeutil.TimeStamp + CreatedBy int64 + UsedCount int64 + LastUsedUnix timeutil.TimeStamp + RevokedUnix timeutil.TimeStamp +} + +type TokenScope string +``` + +**Methods:** + +```go +func (t *VaultToken) IsActive() bool +func (t *VaultToken) IsExpired() bool +func (t *VaultToken) IsRevoked() bool + +func (scope TokenScope) Allows(action string, secretName string) bool +func (scope TokenScope) CanRead(secretName string) bool +func (scope TokenScope) CanWrite(secretName string) bool +func (scope TokenScope) IsAdmin() bool +``` + +**Functions:** + +```go +func GenerateToken() (plaintext string, hash string) +func HashToken(plaintext string) string +func CreateVaultToken(ctx context.Context, token *VaultToken) error +func GetVaultTokenByHash(ctx context.Context, hash string) (*VaultToken, error) +func GetVaultTokenByID(ctx context.Context, id int64) (*VaultToken, error) +func GetVaultTokensByRepo(ctx context.Context, repoID int64, includeRevoked bool) ([]*VaultToken, error) +func RevokeVaultToken(ctx context.Context, token *VaultToken) error +func RecordTokenUse(ctx context.Context, token *VaultToken) error +func ValidateAndUseToken(ctx context.Context, plaintext string, repoID int64) (*VaultToken, error) +func DeleteExpiredTokens(ctx context.Context, retentionDays int) (int64, error) +``` + +--- + +#### VaultAuditEntry + +```go +type VaultAuditEntry struct { + ID int64 + RepoID int64 + SecretID int64 + VersionID int64 + Action AuditAction + UserID int64 + RunnerID int64 // CI/CD runner + TokenID int64 // Token used + IPAddress string + UserAgent string + Timestamp timeutil.TimeStamp + Success bool + FailReason string + SecretName string // Non-DB field + UserName string // Non-DB field +} + +type AuditAction string + +const ( + AuditActionList AuditAction = "list" + AuditActionRead AuditAction = "read" + AuditActionWrite AuditAction = "write" + AuditActionDelete AuditAction = "delete" + AuditActionRestore AuditAction = "restore" + AuditActionPurge AuditAction = "purge" + AuditActionRollback AuditAction = "rollback" + AuditActionRotateKey AuditAction = "rotate-key" +) +``` + +**Functions:** + +```go +func CreateVaultAuditEntry(ctx context.Context, entry *VaultAuditEntry) error +func LogVaultAccess(ctx context.Context, repoID, secretID, versionID, userID, runnerID, tokenID int64, action AuditAction, ipAddress, userAgent string, success bool, failReason string) error +func GetVaultAuditEntries(ctx context.Context, opts FindVaultAuditOptions) ([]*VaultAuditEntry, error) +func CountVaultAuditEntries(ctx context.Context, opts FindVaultAuditOptions) (int64, error) +func DeleteVaultAuditEntriesOlderThan(ctx context.Context, repoID int64, before timeutil.TimeStamp) (int64, error) +func PruneVaultAuditEntries(ctx context.Context, repoID int64, retentionDays int) (int64, error) +``` + +--- + +### Services Package + +The `services` package provides high-level business logic. + +#### Secrets + +```go +type CreateSecretOptions struct { + Name string + Description string + Type string + Value string + EncryptionMode string // "standard" or "lockbox" + CreatorID int64 +} + +type UpdateSecretOptions struct { + Type string + Value string + Comment string + UpdaterID int64 +} +``` + +**Functions:** + +```go +func ListSecrets(ctx context.Context, repoID int64, includeDeleted bool) ([]*models.VaultSecret, error) +func GetSecret(ctx context.Context, repoID int64, name string) (*models.VaultSecret, error) +func GetSecretValue(ctx context.Context, repoID int64, name string, version int) (string, error) +func CreateSecret(ctx context.Context, repoID int64, opts CreateSecretOptions, limits *license.Limits) (*models.VaultSecret, error) +func UpdateSecret(ctx context.Context, repoID int64, name string, opts UpdateSecretOptions) (*models.VaultSecret, error) +func DeleteSecret(ctx context.Context, repoID int64, name string, userID int64) error +func RestoreSecret(ctx context.Context, repoID int64, name string) error +func RollbackSecret(ctx context.Context, repoID int64, name string, version int, userID int64) error +func ListVersions(ctx context.Context, repoID int64, name string) ([]*models.VaultSecretVersion, error) +func GetOrCreateRepoKey(ctx context.Context, repoID int64) (*models.VaultRepoKey, error) +``` + +**Errors:** + +```go +var ( + ErrSecretNotFound = errors.New("secret not found") + ErrVersionNotFound = errors.New("version not found") + ErrSecretExists = errors.New("secret already exists") + ErrEncryptionFailed = errors.New("encryption failed") + ErrDecryptionFailed = errors.New("decryption failed") + ErrSecretLimitReached = errors.New("secret limit reached for this tier") + ErrVersionLimitReached = errors.New("version limit reached for this tier") +) +``` + +--- + +#### Key Management + +```go +type MigrateRepoKeyOptions struct { + OldKey []byte // Old master key (32 bytes) + RepoID int64 // 0 = all repos + UserID int64 + IPAddress string +} + +type MigrateRepoKeyResult struct { + RepoID int64 + Success bool + Error string +} + +type RotateRepoKeyOptions struct { + RepoID int64 + UserID int64 + IPAddress string +} +``` + +**Functions:** + +```go +func MigrateRepoKeys(ctx context.Context, opts MigrateRepoKeyOptions) ([]MigrateRepoKeyResult, error) +func RotateRepoKey(ctx context.Context, opts RotateRepoKeyOptions) error +``` + +--- + +#### Tokens + +```go +type CreateTokenOptions struct { + Description string + Scope string + TTL string + CreatorID int64 +} +``` + +**Functions:** + +```go +func ListTokens(ctx context.Context, repoID int64) ([]*models.VaultToken, error) +func CreateToken(ctx context.Context, repoID int64, opts CreateTokenOptions, limits *license.Limits) (*models.VaultToken, string, error) +func RevokeToken(ctx context.Context, repoID int64, tokenID int64) error +func ValidateToken(ctx context.Context, rawToken string, action string, secretName string) (*models.VaultToken, error) +func GetTokenInfo(ctx context.Context, rawToken string) (*models.VaultToken, error) +``` + +**Errors:** + +```go +var ( + ErrTokenNotFound = errors.New("token not found") + ErrTokenRevoked = errors.New("token revoked") + ErrTokenExpired = errors.New("token expired") + ErrInvalidScope = errors.New("invalid token scope") + ErrAccessDenied = errors.New("access denied") + ErrInvalidToken = errors.New("invalid token") + ErrTokenLimitReached = errors.New("token limit reached for this tier") +) +``` + +--- + +#### Audit + +```go +func ListAuditEntries(ctx context.Context, repoID int64, page, pageSize int) ([]*models.VaultAuditEntry, int64, error) +func CreateAuditEntry(ctx context.Context, entry *models.VaultAuditEntry) error +``` + +--- + +## Error Codes + +### HTTP Status Codes + +| Code | Status | Description | +|------|--------|-------------| +| 200 | OK | Request successful | +| 201 | Created | Resource created | +| 400 | Bad Request | Invalid request parameters | +| 401 | Unauthorized | Authentication required | +| 402 | Payment Required | License upgrade required | +| 403 | Forbidden | Insufficient permissions | +| 404 | Not Found | Resource not found | +| 409 | Conflict | Encryption key mismatch | +| 500 | Internal Server Error | Server error | + +### Error Response Format + +```json +{ + "error": "error_code", + "message": "Human-readable error message" +} +``` + +### Common Error Codes + +| Code | HTTP Status | Description | +|------|-------------|-------------| +| `unauthorized` | 401 | Missing or invalid authentication | +| `forbidden` | 403 | Insufficient permissions | +| `not_found` | 404 | Secret, token, or version not found | +| `already_exists` | 409 | Secret with name already exists | +| `invalid_request` | 400 | Invalid JSON or missing required fields | +| `invalid_version` | 400 | Invalid version number | +| `invalid_token` | 401 | Invalid or expired vault token | +| `token_expired` | 401 | Vault token has expired | +| `token_revoked` | 401 | Vault token has been revoked | +| `access_denied` | 403 | Token scope insufficient | +| `limit_reached` | 402 | License limit reached | +| `enterprise_required` | 402 | Feature requires Enterprise license | +| `key_mismatch` | 409 | Encryption key changed | +| `decryption_failed` | 500 | Failed to decrypt secret | +| `encryption_failed` | 500 | Failed to encrypt secret | +| `internal_error` | 500 | Unexpected server error | + +--- + +## Code Examples + +### cURL Examples + +#### Create a Secret + +```bash +curl -X PUT \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{ + "name": "DATABASE_URL", + "description": "Production database connection", + "type": "password", + "value": "postgresql://user:pass@localhost/db" + }' \ + https://git.example.com/api/v1/repos/owner/repo/vault/secrets/DATABASE_URL +``` + +#### Read a Secret (with Vault Token) + +```bash +curl -H "Authorization: Bearer gvt_a1b2c3d4e5f6..." \ + https://git.example.com/api/v1/repos/owner/repo/vault/secrets/DATABASE_URL +``` + +#### List All Secrets + +```bash +curl -H "Authorization: Bearer " \ + https://git.example.com/api/v1/repos/owner/repo/vault/secrets +``` + +#### Rollback to Previous Version + +```bash +curl -X POST \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{"version": 2}' \ + https://git.example.com/api/v1/repos/owner/repo/vault/secrets/DATABASE_URL/rollback +``` + +#### Create a CI/CD Token + +```bash +curl -X POST \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{ + "description": "GitHub Actions", + "scope": "read:*", + "ttl": "90d" + }' \ + https://git.example.com/api/v1/repos/owner/repo/vault/tokens +``` + +--- + +### Go Examples + +#### Using the Crypto Package + +```go +package main + +import ( + "fmt" + "git.marketally.com/gitcaddy/gitcaddy-vault/crypto" +) + +func main() { + // Load master key + if err := crypto.LoadMasterKey(); err != nil { + panic(err) + } + + // Generate a DEK + dek, err := crypto.GenerateDEK() + if err != nil { + panic(err) + } + + // Encrypt the DEK with the master key + encryptedDEK, err := crypto.EncryptDEK(dek) + if err != nil { + panic(err) + } + + // Encrypt a secret value with the DEK + plaintext := []byte("my-secret-value") + ciphertext, err := crypto.EncryptSecret(plaintext, dek) + if err != nil { + panic(err) + } + + // Decrypt the secret + decrypted, err := crypto.DecryptSecret(ciphertext, dek) + if err != nil { + panic(err) + } + + fmt.Println(string(decrypted)) // "my-secret-value" +} +``` + +#### Using the Services Package + +```go +package main + +import ( + "context" + "fmt" + "git.marketally.com/gitcaddy/gitcaddy-vault/services" +) + +func main() { + ctx := context.Background() + repoID := int64(123) + + // Create a secret + secret, err := services.CreateSecret(ctx, repoID, services.CreateSecretOptions{ + Name: "API_KEY", + Description: "Third-party API key", + Type: "api_key", + Value: "sk_live_abc123", + CreatorID: 1, + }, nil) + if err != nil { + panic(err) + } + + // Get the secret value + value, err := services.GetSecretValue(ctx, repoID, "API_KEY", 0) + if err != nil { + panic(err) + } + fmt.Println(value) // "sk_live_abc123" + + // Update the secret + _, err = services.UpdateSecret(ctx, repoID, "API_KEY", services.UpdateSecretOptions{ + Type: "api_key", + Value: "sk_live_xyz789", + Comment: "Rotated key", + UpdaterID: 1, + }) + if err != nil { + panic(err) + } + + // List versions + versions, err := services.ListVersions(ctx, repoID, "API_KEY") + if err != nil { + panic(err) + } + fmt.Printf("Secret has %d versions\n", len(versions)) + + // Rollback to version 1 + err = services.RollbackSecret(ctx, repoID, "API_KEY", 1, 1) + if err != nil { + panic(err) + } +} +``` + +#### Token Validation + +```go +package main + +import ( + "context" + "fmt" + "git.marketally.com/gitcaddy/gitcaddy-vault/services" +) + +func main() { + ctx := context.Background() + rawToken := "gvt_a1b2c3d4e5f6..." + + // Validate token for reading a specific secret + token, err := services.ValidateToken(ctx, rawToken, "read", "DATABASE_URL") + if err != nil { + fmt.Println("Token validation failed:", err) + return + } + + fmt.Printf("Token is valid (scope: %s)\n", token.Scope) + + // Get token info (introspection) + info, err := services.GetTokenInfo(ctx, rawToken) + if err != nil { + panic(err) + } + + fmt.Printf("Token expires at: %d\n", info.ExpiresUnix) + fmt.Printf("Used %d times\n", info.UsedCount) +} +``` + +--- + +### Lockbox (E2E Encrypted) Secrets + +Lockbox secrets are encrypted client-side before being sent to the server. The server never sees the plaintext value. Use the SDKs for simplified lockbox operations. + +#### Go SDK + +```go +import vault "git.marketally.com/gitcaddy/vault-tools/sdk/go" + +client := vault.NewClient("https://git.example.com", "owner", "repo", token) + +// Create a lockbox secret +err := client.CreateLockbox(ctx, "prod.master-key", "super-secret-value", "my-passphrase") +if err != nil { + log.Fatal(err) +} + +// Get and decrypt a lockbox secret +value, err := client.GetLockbox(ctx, "prod.master-key", "my-passphrase") +if err != nil { + log.Fatal(err) +} +fmt.Println(value) // "super-secret-value" +``` + +#### TypeScript/JavaScript SDK + +```typescript +import { VaultClient } from '@gitcaddy/vault-sdk'; + +const client = new VaultClient('https://git.example.com', 'owner', 'repo', token); + +// Create a lockbox secret +await client.createLockbox('prod.master-key', 'super-secret-value', 'my-passphrase'); + +// Get and decrypt a lockbox secret +const value = await client.getLockbox('prod.master-key', 'my-passphrase'); +console.log(value); // "super-secret-value" +``` + +#### Python SDK + +```python +from gitcaddy_vault import VaultClient + +client = VaultClient('https://git.example.com', 'owner', 'repo', token) + +# Create a lockbox secret +client.create_lockbox('prod.master-key', 'super-secret-value', 'my-passphrase') + +# Get and decrypt a lockbox secret +value = client.get_lockbox('prod.master-key', 'my-passphrase') +print(value) # "super-secret-value" +``` + +#### Raw API (Manual Encryption) + +For manual lockbox operations, you must encrypt the value client-side using Argon2id + AES-256-GCM in the lockbox format before sending: + +```bash +# Create lockbox secret (value must be pre-encrypted) +curl -X PUT \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "prod.master-key", + "value": "lockbox:v1:BASE64_SALT:BASE64_NONCE_CIPHERTEXT_TAG", + "encryption_mode": "lockbox", + "type": "key-value" + }' \ + https://git.example.com/api/v1/repos/owner/repo/vault/secrets/prod.master-key +``` + +**Lockbox Format:** `lockbox:v1::` + +**Encryption Parameters:** +- Key derivation: Argon2id (time=1, memory=64MB, parallelism=4, keyLen=32) +- Cipher: AES-256-GCM (nonce=12 bytes, tag=16 bytes) +- Salt: 16 bytes random + +--- + +### Python Examples + +#### Using requests + +```python +import requests + +BASE_URL = "https://git.example.com/api/v1" +TOKEN = "your_gitea_token" +REPO = "owner/repo" + +headers = { + "Authorization": f"Bearer {TOKEN}", + "Content-Type": "application/json" +} + +# Create a secret +response = requests.put( + f"{BASE_URL}/repos/{REPO}/vault/secrets/API_KEY", + headers=headers, + json={ + "name": "API_KEY", + "description": "Third-party API key", + "type": "api_key", + "value": "sk_live_abc123" + } +) +print(response.json()) + +# Get secret value +response = requests.get( + f"{BASE_URL}/repos/{REPO}/vault/secrets/API_KEY", + headers=headers +) +secret = response.json() +print(f"Secret value: {secret['value']}") + +# List all secrets +response = requests.get( + f"{BASE_URL}/repos/{REPO}/vault/secrets", + headers=headers +) +secrets = response.json() +for s in secrets: + print(f"{s['name']}: v{s['current_version']}") +``` + +--- + +### JavaScript/Node.js Examples + +```javascript +const axios = require('axios'); + +const BASE_URL = 'https://git.example.com/api/v1'; +const TOKEN = 'your_gitea_token'; +const REPO = 'owner/repo'; + +const client = axios.create({ + baseURL: BASE_URL, + headers: { + 'Authorization': `Bearer ${TOKEN}`, + 'Content-Type': 'application/json' + } +}); + +// Create a secret +async function createSecret() { + const response = await client.put(`/repos/${REPO}/vault/secrets/DATABASE_URL`, { + name: 'DATABASE_URL', + description: 'Production database', + type: 'password', + value: 'postgresql://user:pass@localhost/db' + }); + console.log('Created:', response.data); +} + +// Get secret value +async function getSecret() { + const response = await client.get(`/repos/${REPO}/vault/secrets/DATABASE_URL`); + console.log('Value:', response.data.value); +} + +// Create a token +async function createToken() { + const response = await client.post(`/repos/${REPO}/vault/tokens`, { + description: 'CI/CD Pipeline', + scope: 'read:*', + ttl: '30d' + }); + console.log('Token:', response.data.token); + console.log('Save this token - you won\'t see it again!'); +} + +// Run examples +(async () => { + await createSecret(); + await getSecret(); + await createToken(); +})(); +``` + +--- + +### GitHub Actions Example + +```yaml +name: Deploy + +on: + push: + branches: [main] + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Get secrets from vault + env: + VAULT_TOKEN: ${{ secrets.GITCADDY_VAULT_TOKEN }} + VAULT_URL: https://git.example.com/api/v1/repos/owner/repo/vault + run: | + # Get database URL + DB_URL=$(curl -s -H "Authorization: Bearer $VAULT_TOKEN" \ + "$VAULT_URL/secrets/DATABASE_URL" | jq -r '.value') + echo "::add-mask::$DB_URL" + echo "DATABASE_URL=$DB_URL" >> $GITHUB_ENV + + # Get API key + API_KEY=$(curl -s -H "Authorization: Bearer $VAULT_TOKEN" \ + "$VAULT_URL/secrets/API_KEY" | jq -r '.value') + echo "::add-mask::$API_KEY" + echo "API_KEY=$API_KEY" >> $GITHUB_ENV + + - name: Deploy + run: | + echo "Deploying with DATABASE_URL and API_KEY from vault..." + # Your deployment script here +``` + +--- + +### GitLab CI Example + +```yaml +deploy: + stage: deploy + script: + - | + # Get secrets from vault + export DATABASE_URL=$(curl -s -H "Authorization: Bearer $VAULT_TOKEN" \ + "$VAULT_URL/secrets/DATABASE_URL" | jq -r '.value') + + export API_KEY=$(curl -s -H "Authorization: Bearer $VAULT_TOKEN" \ + "$VAULT_URL/secrets/API_KEY" | jq -r '.value') + + # Deploy + ./deploy.sh + variables: + VAULT_URL: https://git.example.com/api/v1/repos/owner/repo/vault + only: + - main +``` + +--- + +## License Tiers + +GitCaddy Vault uses a tiered licensing model: + +### Solo (Free) + +- **Users:** 1 +- **Secrets per repo:** 5 +- **Audit retention:** 7 days +- **Features:** Basic secret storage +- **CI/CD tokens:** No +- **Versioning:** No +- **SSO:** No + +### Pro + +- **Users:** 5 +- **Secrets per repo:** Unlimited +- **Audit retention:** 90 days +- **Features:** Version history, rollback +- **CI/CD tokens:** Yes +- **Versioning:** Yes +- **SSO:** No +- **Price:** Contact sales + +### Team + +- **Users:** 25 +- **Secrets per repo:** Unlimited +- **Audit retention:** 365 days +- **Features:** All Pro features + SSO +- **CI/CD tokens:** Yes +- **Versioning:** Yes +- **SSO:** Yes +- **Price:** Contact sales + +### Enterprise + +- **Users:** Unlimited +- **Secrets per repo:** Unlimited +- **Audit retention:** Custom +- **Features:** All Team features + DEK rotation +- **CI/CD tokens:** Yes +- **Versioning:** Yes +- **SSO:** Yes +- **DEK rotation:** Yes +- **Price:** Contact sales + +--- + +## Support + +- **Documentation:** https://docs.gitcaddy.com/vault +- **Issues:** https://git.marketally.com/gitcaddy/gitcaddy-vault/issues +- **Email:** support@marketally.com +- **Website:** https://gitcaddy.com/vault + +--- + +**Copyright © 2026 MarketAlly. All rights reserved.** +**License:** Business Source License 1.1 \ No newline at end of file diff --git a/README.md b/README.md index fdb772d..96d1ba7 100644 --- a/README.md +++ b/README.md @@ -200,50 +200,6 @@ curl -X DELETE \ https://gitcaddy.example.com/api/v1/repos/owner/repo/vault/secrets/prod.database.password ``` -### Lockbox API (E2E Encrypted Secrets) - -Lockbox secrets require client-side encryption before upload and client-side decryption after retrieval. Use our SDKs for simplified lockbox operations. - -**Create Lockbox Secret (using SDK):** -```go -// Go SDK -client := vault.NewClient("https://gitcaddy.example.com", token) -err := client.CreateLockbox(ctx, "prod.master-key", "super-secret-value", "my-passphrase") -``` - -```typescript -// TypeScript SDK -const client = new VaultClient('https://gitcaddy.example.com', token); -await client.createLockbox('prod.master-key', 'super-secret-value', 'my-passphrase'); -``` - -```python -# Python SDK -client = VaultClient('https://gitcaddy.example.com', token) -client.create_lockbox('prod.master-key', 'super-secret-value', 'my-passphrase') -``` - -**Get Lockbox Secret:** -```go -// Go SDK -value, err := client.GetLockbox(ctx, "prod.master-key", "my-passphrase") -``` - -**Raw API (manual encryption):** -```bash -# The value must be pre-encrypted in lockbox:v1:... format -curl -X PUT \ - -H "Authorization: Bearer $VAULT_TOKEN" \ - -H "Content-Type: application/json" \ - -d '{ - "name": "prod.master-key", - "value": "lockbox:v1:BASE64_SALT:BASE64_ENCRYPTED_DATA", - "encryption_mode": "lockbox", - "type": "key-value" - }' \ - https://gitcaddy.example.com/api/v1/repos/owner/repo/vault/secrets/prod.master-key -``` - ### CI/CD Integration **GitHub Actions / Gitea Actions:**