Implements explicit column migration logic to handle schema upgrades that xorm.Sync() doesn't reliably perform. Adds encryption_mode column migration for vault_secret table to support lockbox (E2E) encryption. Includes database-agnostic column existence checks and ALTER TABLE statement generation for PostgreSQL, MySQL, SQLite, and MSSQL.
GitCaddy Vault
Encrypted Secrets Management for GitCaddy
GitCaddy Vault is a commercial module compiled directly into GitCaddy Server that provides enterprise-grade secrets management within your GitCaddy repositories. Store, version, and securely access credentials, API keys, certificates, and other sensitive data without leaving your Git workflow.
Features
Core Capabilities
- Encrypted Storage - All secrets encrypted at rest using AES-256-GCM with per-repository encryption keys
- Version History - Full version tracking with rollback capability for all secrets
- Audit Logging - Complete audit trail of all secret access and modifications
- CI/CD Tokens - Scoped tokens for secure automated access during builds and deployments
Secret Types
| Type | Description |
|---|---|
key-value |
Simple key-value pairs |
env-file |
Environment file format (KEY=value) |
file |
Arbitrary file content |
certificate |
TLS/SSL certificates |
ssh-key |
SSH private keys |
Lockbox (End-to-End Encryption)
Lockbox provides optional client-side encryption where the server never sees your plaintext secrets. Perfect for highly sensitive data where you don't want even the server administrator to have access.
| Feature | Standard Mode | Lockbox Mode |
|---|---|---|
| Server sees plaintext | Yes | No |
| Passphrase required | No | Yes |
| Recovery if passphrase lost | Yes | No |
| Web UI viewing | Yes | CLI/SDK only |
How Lockbox Works:
- Client encrypts secret with your passphrase using Argon2id + AES-256-GCM
- Encrypted blob is sent to server in
lockbox:v1:...format - Server wraps the blob with repository DEK (double encryption)
- On retrieval, server unwraps DEK layer, returns lockbox blob
- Client decrypts with your passphrase
Encryption Scheme:
- Key derivation: Argon2id (time=1, memory=64MB, parallelism=4)
- Cipher: AES-256-GCM with 12-byte nonce
- Salt: 16 bytes random per secret
- Format:
lockbox:v1:<base64(salt)>:<base64(nonce||ciphertext||tag)>
Security Architecture
┌─────────────────────────────────────────────────────────┐
│ GitCaddy Server │
├─────────────────────────────────────────────────────────┤
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Web UI │ │ REST API │ │ CI/CD API │ │
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
│ │ │ │ │
│ └──────────────────┼──────────────────┘ │
│ │ │
│ ┌────────▼────────┐ │
│ │ Vault Service │ │
│ └────────┬────────┘ │
│ │ │
│ ┌──────────────────┼──────────────────┐ │
│ ┌─────▼─────┐ ┌──────▼──────┐ ┌──────▼──────┐ │
│ │ Crypto │ │ Models │ │ License │ │
│ │ Engine │ │ (XORM) │ │ Manager │ │
│ └───────────┘ └─────────────┘ └─────────────┘ │
│ │
│ (Compiled into GitCaddy Server) │
└─────────────────────────────────────────────────────────┘
Encryption Hierarchy:
- Master Key (KEK) - Server-level key encryption key
- Repository DEK - Per-repository data encryption key, encrypted by KEK
- Secret Values - Encrypted using repository DEK with AES-256-GCM
Installation
Requirements
- GitCaddy Server v1.0.0 or later (Vault is included automatically)
- Valid GitCaddy Vault license
Setup
GitCaddy Vault is compiled directly into GitCaddy Server - no separate installation required.
-
Add your license key via environment variable or file:
# Option 1: Environment variable export GITCADDY_LICENSE_KEY="<your-base64-license>" # Option 2: License file cp license.key /etc/gitcaddy/license.key -
Restart GitCaddy Server to activate the license
Configuration
Environment Variables
| Variable | Description | Default |
|---|---|---|
GITCADDY_LICENSE_KEY |
Base64-encoded license key | - |
GITCADDY_LICENSE_FILE |
Path to license file | /etc/gitcaddy/license.key |
GITCADDY_VAULT_KEK |
Master key encryption key (32 bytes, hex) | Auto-generated |
GITCADDY_DEV_MODE |
Skip license validation (dev only) | 0 |
License File Locations
GitCaddy Server searches for license files in this order:
- Path specified by
GITCADDY_LICENSE_FILE /etc/gitcaddy/license.key./custom/license.key./license.key
Usage
Web Interface
Access the Vault tab in any repository where you have admin permissions:
https://your-gitcaddy-instance/owner/repo/vault
Managing Secrets:
- Navigate to Repository > Vault > Secrets
- Click "New Secret" to create a secret
- Use dot notation for organization:
prod.database.password,staging.api.key
Creating CI/CD Tokens:
- Navigate to Repository > Vault > CI/CD Tokens
- Click "New Token"
- Define the scope (e.g.,
read:*,read:prod.*,write:db.credentials) - Set expiration time
- Copy the token immediately (shown only once)
REST API
All API endpoints require a valid vault token in the Authorization header.
Authentication:
# Bearer token format
Authorization: Bearer gvt_abc123...
# Or token format
Authorization: token gvt_abc123...
List Secrets:
curl -H "Authorization: Bearer $VAULT_TOKEN" \
https://gitcaddy.example.com/api/v1/repos/owner/repo/vault/secrets
Get Secret Value:
curl -H "Authorization: Bearer $VAULT_TOKEN" \
https://gitcaddy.example.com/api/v1/repos/owner/repo/vault/secrets/prod.database.password
Create Secret:
curl -X POST \
-H "Authorization: Bearer $VAULT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "prod.database.password",
"description": "Production database credentials",
"type": "key-value",
"value": "secret-password-here"
}' \
https://gitcaddy.example.com/api/v1/repos/owner/repo/vault/secrets
Update Secret:
curl -X PUT \
-H "Authorization: Bearer $VAULT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"type": "key-value",
"value": "new-secret-value",
"comment": "Rotated credentials"
}' \
https://gitcaddy.example.com/api/v1/repos/owner/repo/vault/secrets/prod.database.password
Delete Secret:
curl -X DELETE \
-H "Authorization: Bearer $VAULT_TOKEN" \
https://gitcaddy.example.com/api/v1/repos/owner/repo/vault/secrets/prod.database.password
CI/CD Integration
GitHub Actions / Gitea Actions:
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Fetch secrets
run: |
DB_PASSWORD=$(curl -s -H "Authorization: Bearer ${{ secrets.VAULT_TOKEN }}" \
"${{ github.server_url }}/api/v1/repos/${{ github.repository }}/vault/secrets/prod.database.password" \
| jq -r '.value')
echo "::add-mask::$DB_PASSWORD"
echo "DB_PASSWORD=$DB_PASSWORD" >> $GITHUB_ENV
GitLab CI:
deploy:
script:
- |
export DB_PASSWORD=$(curl -s -H "Authorization: Bearer $VAULT_TOKEN" \
"$CI_SERVER_URL/api/v1/repos/$CI_PROJECT_PATH/vault/secrets/prod.database.password" \
| jq -r '.value')
Token Scopes
Token scopes control access to secrets using a simple grammar:
| Scope | Description |
|---|---|
read:* |
Read access to all secrets |
write:* |
Read and write access to all secrets |
read:prod.* |
Read access to secrets starting with prod. |
write:db.credentials |
Write access to specific secret db.credentials |
admin |
Full administrative access |
Multiple scopes: Separate with commas: read:prod.*,write:staging.*
License Tiers
| Feature | Solo | Pro | Team | Enterprise |
|---|---|---|---|---|
| Users | 1 | 5 | 25 | Unlimited |
| Secrets per repo | 5 | Unlimited | Unlimited | Unlimited |
| Audit retention | 7 days | 90 days | 1 year | Custom |
| Version history | No | Yes | Yes | Yes |
| CI/CD tokens | No | Yes | Yes | Yes |
| SSO integration | No | No | Yes | Yes |
| Priority support | No | No | No | Yes |
Database Schema
GitCaddy Vault uses the following tables:
vault_secret- Secret metadatavault_secret_version- Versioned secret values (encrypted)vault_repo_key- Per-repository encryption keysvault_token- CI/CD access tokensvault_audit_entry- Audit log entries
Development
Architecture: Vault ↔ Server Sync
Important for contributors and AI assistants:
GitCaddy Vault is the source of truth for vault-related templates and locales. Due to Go plugin compilation limitations, vault code is compiled directly into GitCaddy Server rather than loaded as a dynamic plugin.
┌─────────────────────────────────────────────────────────────────┐
│ gitcaddy-vault (this repo) │
│ SOURCE OF TRUTH for: │
│ • templates/repo/vault/*.tmpl → UI templates │
│ • locale/*.json → Translation strings │
│ • models/, services/, crypto/ → Business logic │
│ • license/ → License validation │
└─────────────────────────┬───────────────────────────────────────┘
│
BUILD TIME SYNC
(scripts/sync-vault.sh)
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ gitcaddy-server │
│ RECEIVES from vault: │
│ • templates/repo/vault/*.tmpl ← Copied from vault │
│ • options/locale/*.json ← vault.* keys merged │
│ │
│ SERVER-ONLY (not in vault): │
│ • templates/repo/vault/feature_upgrade.tmpl │
│ • templates/repo/vault/not_installed.tmpl │
│ • templates/repo/vault/upgrade.tmpl │
│ • routers/web/repo/vault/vault.go ← Router glue code │
│ • services/vault/vault.go ← Service wrappers │
└─────────────────────────────────────────────────────────────────┘
When making changes:
- Edit templates/locales in gitcaddy-vault (this repo)
- The CI build automatically syncs to gitcaddy-server
- Server-specific templates (upgrade prompts) stay in server repo
- Go router code stays in server repo (thin integration layer)
Why not Go plugins? Go plugins require exact compiler version and dependency matches between plugin and host. This is fragile in practice, so we compile vault directly into the server binary.
Building
The Vault module is compiled directly into GitCaddy Server. To build the server with Vault:
# Clone GitCaddy Server (includes Vault)
git clone https://git.marketally.com/gitcaddy/server.git
cd server
# Build the server (Vault is included automatically)
make build
# Run tests
go test ./...
Keygen Utility
The license key generation tool is built separately:
# Clone the vault repository
git clone https://git.marketally.com/gitcaddy/vault.git
cd vault
# Build the keygen utility
go build -o keygen ./cmd/keygen
Generating License Keys
# Generate a new keypair (do this once, keep private key secure!)
go run ./cmd/keygen -generate-keys
# Sign a license
go run ./cmd/keygen -sign \
-email customer@example.com \
-tier pro \
-duration 365d \
-private-key /secure/path/private.key
Local Development
Set GITCADDY_DEV_MODE=1 to skip license validation during development:
export GITCADDY_DEV_MODE=1
Key Management
Master Key Configuration
The vault uses a master Key Encryption Key (KEK) to encrypt repository-level Data Encryption Keys (DEKs). Configure the master key using one of these methods (in priority order):
-
app.ini (recommended for production):
[vault] MASTER_KEY = <64-character-hex-string> -
Environment variable:
export GITCADDY_VAULT_KEY="<64-character-hex-string>" -
Key file:
export GITCADDY_VAULT_KEY_FILE="/etc/gitcaddy/vault.key" -
Fallback (not recommended): If none of the above are set, Gitea's
SECRET_KEYis used as a fallback.
Generate a secure master key:
openssl rand -hex 32
Key Migration
If you change your master key or need to migrate from the fallback key to a dedicated master key, use the Key Migration feature.
When to use key migration:
- You changed the
MASTER_KEYin app.ini and existing secrets are now inaccessible - Secrets were created using the fallback key before a dedicated master key was configured
- You see "Encryption Key Mismatch" errors when accessing vault secrets
Web UI:
- Navigate to Repository > Vault > Key Migration (admin only)
- Enter the old master key (the previous
MASTER_KEYor Gitea'sSECRET_KEY) - Choose the migration scope (this repository or all repositories)
- Click "Start Migration"
API:
curl -X POST \
-H "Authorization: Bearer $VAULT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"old_key": "<previous-master-key-or-secret-key>",
"repo_id": 0
}' \
https://gitcaddy.example.com/owner/repo/-/vault/api/migrate-key
The old_key can be:
- A 64-character hex string (will be decoded to 32 bytes)
- Raw text (will be used as-is, padded/truncated to 32 bytes)
Set repo_id to 0 to migrate the current repository, or specify a repo ID for a specific repository. Instance admins can migrate all repositories at once.
DEK Rotation (Enterprise)
For enhanced security, Enterprise license holders can rotate the Data Encryption Key (DEK) for a repository. This generates a new DEK and re-encrypts all secret versions.
Web UI:
- Navigate to Repository > Vault > Key Migration
- Click "Rotate DEK" in the DEK Rotation section
API:
curl -X POST \
-H "Authorization: Bearer $VAULT_TOKEN" \
https://gitcaddy.example.com/owner/repo/-/vault/api/rotate-key
Security Considerations
- Key Management - The master KEK should be stored securely (HSM, KMS, or secure environment variable)
- Token Storage - Vault tokens are hashed with SHA-256 before storage
- Audit Trail - All access is logged with IP address and user information
- Soft Delete - Deleted secrets are retained for recovery before permanent deletion
- TLS Required - Always use HTTPS in production
Support
- Documentation: https://docs.gitcaddy.com/vault
- Issues: https://git.marketally.com/gitcaddy/vault/issues
- Email: support@marketally.com
License
Business Source License 1.1 - See LICENSE file for details.
Copyright 2026 MarketAlly. All rights reserved.
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
- Architecture
- Installation
- Configuration
- Authentication
- API Endpoints
- Go Package API
- Error Codes
- Code Examples
- 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:
- Master Key (KEK - Key Encryption Key): A 256-bit key configured at the instance level
- 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 historyvault_repo_key: Per-repository encrypted DEKsvault_token: API access tokens for CI/CDvault_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
# 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
- Copy the built plugin to your GitCaddy plugins directory
- Configure the master encryption key (see Configuration)
- Restart GitCaddy
- 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):
- app.ini configuration (recommended):
[vault]
MASTER_KEY = 64_character_hex_string_here
- Environment variable:
export GITCADDY_VAULT_KEY=64_character_hex_string_here
- Key file:
# 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
# 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:
export GITCADDY_LICENSE_KEY="base64_encoded_license_json"
export GITCADDY_LICENSE_FILE=/path/to/license.key
Development Mode
Skip license checks during development:
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
curl -H "Authorization: Bearer <gitea_token>" \
https://git.example.com/api/v1/repos/owner/repo/vault/secrets
2. Vault Token (CI/CD)
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
GET /api/v1/repos/{owner}/{repo}/vault/secrets
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
include_deleted |
boolean | false |
Include soft-deleted secrets |
Response:
[
{
"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
GET /api/v1/repos/{owner}/{repo}/vault/secrets/{name}
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
version |
integer | current | Specific version to retrieve |
Response:
{
"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
PUT /api/v1/repos/{owner}/{repo}/vault/secrets/{name}
Request Body:
{
"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 withlockbox:v1:prefix)
Response (201 Created or 200 OK):
{
"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
DELETE /api/v1/repos/{owner}/{repo}/vault/secrets/{name}
Soft-deletes the secret (can be restored later).
Response:
{
"message": "Secret deleted"
}
Authentication: Requires write permission.
Restore Secret
POST /api/v1/repos/{owner}/{repo}/vault/secrets/{name}/restore
Restores a soft-deleted secret.
Response:
{
"message": "Secret restored"
}
Authentication: Requires write permission.
Versions
List Versions
GET /api/v1/repos/{owner}/{repo}/vault/secrets/{name}/versions
Response:
[
{
"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
POST /api/v1/repos/{owner}/{repo}/vault/secrets/{name}/rollback
Request Body:
{
"version": 2
}
Creates a new version with the content from the specified version.
Response:
{
"message": "Secret rolled back to version 2"
}
Authentication: Requires write permission.
Tokens
List Tokens
GET /api/v1/repos/{owner}/{repo}/vault/tokens
Response:
[
{
"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
POST /api/v1/repos/{owner}/{repo}/vault/tokens
Request Body:
{
"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 secretswrite:*- Read and write all secretsread:prod.*- Read secrets starting withprod.write:DATABASE_URL- Write onlyDATABASE_URLadmin- Full admin access (manage tokens, rotate keys)
TTL Format:
24h- 24 hours7d- 7 days30d- 30 days1y- 1 year0- Never expires
Response (201 Created):
{
"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
DELETE /api/v1/repos/{owner}/{repo}/vault/tokens/{id}
Response:
{
"message": "Token revoked"
}
Authentication: Requires admin permission.
Token Introspection
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:
{
"scope": "read:*",
"description": "CI/CD Pipeline",
"expires_at": 1706745600,
"can_read": true,
"can_write": false,
"is_admin": false
}
Audit Log
Get Audit Entries
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:
{
"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)
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:
{
"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
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:
{
"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:
{
"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
type Manager struct {
masterKey []byte
usingFallback bool
keySource string
}
Methods:
// 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:
// 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:
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
type Manager struct {
license *License
publicKey ed25519.PublicKey
}
Methods:
// 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
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:
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:
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
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:
func (s *VaultSecret) IsActive() bool
func (s *VaultSecret) IsDeleted() bool
func (s *VaultSecret) IsPurged() bool
Functions:
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
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:
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
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:
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
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:
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:
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
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:
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
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:
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:
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
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:
func MigrateRepoKeys(ctx context.Context, opts MigrateRepoKeyOptions) ([]MigrateRepoKeyResult, error)
func RotateRepoKey(ctx context.Context, opts RotateRepoKeyOptions) error
Tokens
type CreateTokenOptions struct {
Description string
Scope string
TTL string
CreatorID int64
}
Functions:
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:
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
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
{
"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
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)
curl -H "Authorization: Bearer gvt_a1b2c3d4e5f6..." \
https://git.example.com/api/v1/repos/owner/repo/vault/secrets/DATABASE_URL
List All Secrets
curl -H "Authorization: Bearer <gitea_token>" \
https://git.example.com/api/v1/repos/owner/repo/vault/secrets
Rollback to Previous Version
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
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
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
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
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
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
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
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:
# 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
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
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
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
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