A Go application for automatically managing and rotating Linode API tokens with configurable validity periods and secure storage in HashiCorp Vault.
- Automatic Token Rotation: Automatically rotates tokens based on configurable thresholds (default: 10% validity remaining)
- Secure Storage: Stores rotated tokens in HashiCorp Vault (KV v2)
- State Tracking: Tracks token rotation history and state via Vault metadata
- Graceful Token Management: Keeps old tokens until expiration (configurable pruning)
- Multiple Tokens: Manage multiple API tokens with different configurations
- Team Metadata: Associate tokens with owning teams for organization
- Flexible Configuration: Single YAML file or glob pattern support
- Daemon or One-Shot: Run as a long-running daemon or one-time execution
- Dry-Run Mode: Test configuration without making changes
- OpenTelemetry Support: Observability via traces, metrics, and logs
- Go 1.21+
- Linode account with API token (for creating/managing tokens)
- HashiCorp Vault with AppRole authentication
- Valid Vault role and secret IDs
# Pull the latest image from GitHub Container Registry
docker pull ghcr.io/linode-obs/latr:latest
# Or pull a specific version
docker pull ghcr.io/linode-obs/latr:v1.0.0docker run --rm \
-v $(pwd)/config.yaml:/config/config.yaml:ro \
-e LINODE_TOKEN="your-linode-api-token" \
-e VAULT_ROLE_ID="your-vault-role-id" \
-e VAULT_SECRET_ID="your-vault-secret-id" \
ghcr.io/linode-obs/latr:latest \
-config /config/config.yamldocker run -d \
--name latr \
--restart unless-stopped \
-v $(pwd)/config.yaml:/config/config.yaml:ro \
-e LINODE_TOKEN="your-linode-api-token" \
-e VAULT_ROLE_ID="your-vault-role-id" \
-e VAULT_SECRET_ID="your-vault-secret-id" \
ghcr.io/linode-obs/latr:latest \
-config /config/config.yaml
# View logs
docker logs -f latr
# Stop gracefully
docker stop latrservices:
latr:
image: ghcr.io/linode-obs/latr:latest
container_name: latr
restart: unless-stopped
volumes:
- ./config.yaml:/config/config.yaml:ro
environment:
- LINODE_TOKEN=${LINODE_TOKEN}
- VAULT_ROLE_ID=${VAULT_ROLE_ID}
- VAULT_SECRET_ID=${VAULT_SECRET_ID}
command: ["-config", "/config/config.yaml"]Images are available for:
linux/amd64(x86_64)linux/arm64(ARM 64-bit, including Apple Silicon and Raspberry Pi 4+)
Download the latest release for your platform from the releases page.
# Clone the repository
git clone https://github.com/linode-obs/latr.git
cd latr
# Build the binary
go build -o latr ./cmd/latr
# Or install directly
go install ./cmd/latrLINODE_TOKEN: Your Linode API token (required)VAULT_ROLE_ID: Vault AppRole role ID (optional if in config)VAULT_SECRET_ID: Vault AppRole secret ID (optional if in config)
Create a YAML configuration file (see examples/config.yaml):
# Global daemon settings
daemon:
mode: "daemon" # "daemon" or "one-shot"
check_interval: "30m" # How often to check tokens (daemon mode only)
dry_run: false # If true, no actual changes are made
# Rotation behavior
rotation:
threshold_percent: 10 # Rotate when <=10% of validity remains
# Vault configuration
vault:
address: "https://vault.example.com"
role_id: "${VAULT_ROLE_ID}" # Can use env vars
secret_id: "${VAULT_SECRET_ID}"
mount_path: "secret" # KV v2 mount path
# Observability settings
observability:
otel_endpoint: "localhost:4317" # OpenTelemetry collector endpoint (optional)
log_level: "info" # debug, info, warn, error
# Token definitions
tokens:
- label: "my-api-token"
team: "platform-team"
validity: "90d" # Must be <= 6 months (180d)
scopes: "*" # "*" for all scopes, or comma-separated list
storage:
- type: "vault"
path: "secret/data/linode/tokens/my-api-token"
- label: "backup-token"
team: "sre-team"
validity: "180d" # Maximum allowed
scopes: "linodes:read_only,domains:read_only"
rotation_threshold: 15 # Override global threshold for this token
storage:
- type: "vault"
path: "secret/data/linode/tokens/backup"Run once and exit:
export LINODE_TOKEN="your-linode-token"
./latr -config config.yamlRun continuously with periodic checks:
# In config.yaml
daemon:
mode: "daemon"
check_interval: "30m"export LINODE_TOKEN="your-linode-token"
./latr -config config.yamlTest configuration without making changes:
daemon:
dry_run: trueUse a glob pattern to load multiple config files:
./latr -config "configs/*.yaml"./latr -version- Token Discovery: latr checks if each configured token exists in Linode
- Rotation Check: Calculates percentage of validity remaining
- Token Creation/Rotation:
- If token doesn't exist: Creates it
- If rotation needed: Creates new token with same label
- Storage: Stores new token value in configured Vault paths
- State Tracking: Updates Vault metadata with:
- Current token ID and value
- Previous token ID and expiry
- Rotation count and timestamp
- Automatic cleanup: Expired tokens are automatically pruned by the Linode API - no manual cleanup needed
- Only manages configured tokens: Only rotates tokens specified in the configuration
- Vault retry on failure: If Linode succeeds but Vault fails, state is tracked for retry on next run
- Graceful shutdown: Handles SIGTERM/SIGINT for clean daemon shutdown
- Maximum validity: 6 months (180 days)
- Supported formats:
90d,180d,6mo,30m,1h - Default rotation threshold: 10% of validity remaining
- Token-specific thresholds can override global setting
The Dockerfile is optimized for GoReleaser and expects a pre-built binary. For local development builds:
# Build the binary first
go build -o latr ./cmd/latr
# Build the container image
docker build -t latr:dev .
# Or build and run in one step
go build -o latr ./cmd/latr && docker build -t latr:dev . && \
docker run --rm latr:dev -versionReleases are automated via GoReleaser and GitHub Actions:
-
Tag a new version (must follow semantic versioning):
git tag -a v1.0.0 -m "Release v1.0.0" git push origin v1.0.0 -
Automated release process:
- Builds binaries for Linux, macOS, Windows (amd64 and arm64)
- Creates multi-arch Docker images (amd64, arm64)
- Publishes images to
ghcr.io/linode-obs/latr - Generates release notes from conventional commits
- Uploads release artifacts to GitHub Releases
-
Testing releases locally (without pushing):
# Install GoReleaser go install github.com/goreleaser/goreleaser/v2@latest # Test release build (snapshot mode) goreleaser release --snapshot --clean # Check output in dist/ ls -la dist/
-
Conventional Commit Format (for better changelogs):
feat:New featuresfix:Bug fixesdocs:Documentation changeschore:Maintenance taskstest:Test additions/changes
# Run unit tests
go test ./...
make test
# Run unit tests with coverage
go test ./... -cover
# Run e2e tests (requires Docker)
make test-e2e
# Run all tests
make test-all
# Run specific package tests
go test ./internal/rotation -vlatr/
├── cmd/latr/ # Main application entrypoint
├── internal/
│ ├── config/ # Configuration parsing and validation
│ ├── linode/ # Linode API client wrapper
│ ├── vault/ # Vault client with AppRole auth
│ ├── rotation/ # Core rotation engine logic
│ ├── scheduler/ # Daemon/one-shot scheduler
│ └── observability/ # OpenTelemetry setup
├── pkg/models/ # Shared domain models
└── examples/ # Example configurations
This project was built using Test-Driven Development (TDD):
- All features have comprehensive unit tests
- Mock implementations for external dependencies (Linode, Vault)
- Table-driven tests for validation logic
- ✅ Integration tests with Docker Compose
latr supports OpenTelemetry for observability:
latr_tokens_total- Total configured tokenslatr_rotations_total{status="success|failure"}- Rotation attemptslatr_rotation_duration_seconds- Rotation operation durationlatr_token_validity_remaining_seconds- Time until rotation neededlatr_vault_storage_errors_total- Vault write failures
A Prometheus monitoring mixin lives in latr-mixin/. It provides a Grafana dashboard and alerting rules for the metrics above.
cd latr-mixin
make build
# → dashboards_out/latr.json
# → prometheus_alerts.yamlSee latr-mixin/README.md for configuration (job selectors, thresholds) and kube-prometheus usage.
Distributed tracing for rotation operations (when OTel endpoint configured)
Structured logging with rotation events, errors, and state changes
- Store
LINODE_TOKENsecurely (env var, secrets manager) - Use Vault AppRole with minimal required permissions
- Enable TLS for Vault communication in production
- Rotate Vault AppRole secret IDs regularly
- Review token scopes - use least privilege principle
- Add proper tracing with OTel (#11)
- Use structured logging (#16)
- Additional storage backends (#17)
- Prometheus metrics exporter (#11)
- Integration tests with Docker Compose (#2)
Contributions welcome! Please:
- Write tests for new features
- Follow existing code structure
- Update documentation
- Run
go test ./...before submitting