# 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