Creates new API.md with complete API reference including: - Architecture and encryption model documentation - Installation and configuration guides - Authentication methods and API endpoints - Go package API documentation - Error codes and code examples - License tier information Also updates README.md to reference the new API documentation.
1803 lines
43 KiB
Markdown
1803 lines
43 KiB
Markdown
# 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 <gitea_token>" \
|
|
https://git.example.com/api/v1/repos/owner/repo/vault/secrets
|
|
```
|
|
|
|
#### 2. Vault Token (CI/CD)
|
|
|
|
```bash
|
|
curl -H "Authorization: Bearer gvt_<vault_token>" \
|
|
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 <gitea_token>" \
|
|
-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 <gitea_token>" \
|
|
https://git.example.com/api/v1/repos/owner/repo/vault/secrets
|
|
```
|
|
|
|
#### Rollback to Previous Version
|
|
|
|
```bash
|
|
curl -X POST \
|
|
-H "Authorization: Bearer <gitea_token>" \
|
|
-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 <gitea_token>" \
|
|
-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:<base64(salt)>:<base64(nonce||ciphertext||tag)>`
|
|
|
|
**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 |