Universal remote templating for DevOps tools
Duckfile lets you keep your Makefiles, Taskfiles, Helm values, and other config as remote templates, render them locally with variables, and run the tool seamlessly.
- Git-sourced templates: repo + ref + path
- Variable tags: !env, !cmd, !file, and literals
- Automatic .env file loading for environment variable management
- Target descriptions +
duck listfor discoverability - Go templates with Sprig functions
- Custom delimiters to avoid collisions (e.g., Taskfile)
- Deterministic caching with stable symlinks
- Checksum validation of remote templates
- Commit hash tracking and validation for reproducible builds
- Host allow/deny lists for supply-chain security
- Enterprise-grade security with digital signatures and access control
- Simple CLI that forwards args to your tool (make, task, helm, …)
- Render-only workflow via
duck syncwhen you don't wantduckto execute your tools
Duck includes comprehensive security features to protect your DevOps workflows:
- 🔐 Digital Signatures: Ed25519 cryptographic signatures for configuration integrity
- 🛡️ Host Access Control: Allow/deny lists to prevent supply chain attacks
- 📁 File Permissions: Secure configuration file handling with validation
- ⚡ Precedence System: 5-tier security hierarchy with signed configs taking priority
- 🔧 Security CLI: Complete security management command suite
- 🌍 Environment Integration: Secure CI/CD and automation support
Quick security setup:
# Create security configuration
mkdir -p .duckfile
echo "version: 1
allowedHosts: [github.com, gitlab.com]
strictMode: true" > .duckfile/security.yaml
# Check security status
duck security status
# Optional: Add digital signatures
duck security generate-keys
duck security sign .duckfile/security.yaml📖 Complete Security User Guide - Comprehensive guide covering all security features
go install github.com/CyberDuck79/duckfile/cmd/duck@latestGo 1.24.0+ recommended.
- Create duck.yaml at the repo root:
version: 1
default: build
targets:
build:
binary: make
fileFlag: -f
template:
repo: https://github.com/CyberDuck79/duckfile-test-templates.git
ref: main
path: Makefile.tpl
checksum: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
trackCommitHash: true # Track commit changes
autoUpdateOnChange: true # Auto-update when changed
variables:
PROJECT: my-service
DATE: !cmd date +%Y-%m-%d
renderedPath: Makefile
test:
binary: task
fileFlag: --taskfile
template:
repo: https://github.com/CyberDuck79/duckfile-test-templates.git
ref: v2.3.3
path: task/Taskfile.yml.tpl
delims: { left: "[[", right: "]]" } # avoid Task's {{ }}
allowMissing: true # missing vars => ""
trackCommitHash: true # Track tags for updates
variables:
GO_VERSION: !env GO_VERSION
PLATFORM: linux/amd64
args: ["--silent"]
docs:
# No binary: this target is sync-only. Use `duck sync docs` to render.
template:
repo: https://github.com/CyberDuck79/duckfile-test-docs-templates.git
ref: main
path: index.md.tpl
variables:
AUTHOR: Cyberduck- Run
# print version
duck version
# run default target (renders Makefile and calls make -f Makefile)
duck
# run a named target and pass additional args after --
duck test --
# explicitly run a target (useful when target names conflict with subcommands)
duck exec build
duck exec sync # runs 'sync' target, not sync subcommand
duck exec test -- --verbose
# list targets (names, binaries, descriptions)
duck list
# include remote info / variable kinds / execution line
duck list -rve
# render-only workflows (no binary execution)
# sync all targets into cache and update symlinks
duck sync
# force re-render ignoring cache
duck sync -f
# clean cache for all or a single target
duck clean
duck clean test
# verbosity / debugging
# show high-level steps (render, cache hits, pruning, exec line)
duck --log-level=info
# extremely detailed (includes variable values, paths, clone steps)
duck --log-level=debug
duck sync --log-level=debug # set log level for subcommands
# commit hash tracking and validation
# enable tracking with manual updates (warns about changes)
duck build --track-commit-hash
# enable tracking with automatic updates (transparent updates)
duck sync --track-commit-hash --auto-update-on-change
# disable tracking entirely (backward compatible)
duck --no-track-commit-hash build
# security management commands
# check security configuration status
duck security status
# verify security configuration and signatures
duck security verify
# generate cryptographic key pairs
duck security generate-keys
# sign security configurations
duck security sign .duckfile/security.yaml
# check and fix file permissions
duck security check-permissions
duck security fix-permissions --all
# host access control via CLI flags
# restrict to specific hosts
duck --allowed-hosts github.com,gitlab.com build
# block specific hosts
duck --denied-hosts malicious-host.com sync
# enforce strict host validation
duck --strict-hosts buildDuckfile includes comprehensive supply-chain security features to prevent malicious template injection and enforce organizational policies:
✅ Implemented Phases:
- Phase 1: Enhanced SecurityConfig with comprehensive validation
- Phase 2: Ed25519 digital signatures for configuration integrity
- Phase 3: File permission validation and security checks
- Phase 4: Policy Enforcement Points - Comprehensive policy validation with violation reporting
Control which Git hosts can be accessed for templates. Security configurations are kept separate from duck.yaml to prevent attackers from modifying both targets and security policies together.
Configuration Precedence (highest to lowest):
- CLI Flags - Override everything else
- Environment Variables - System-level defaults
- Security Config File - Centralized policy management
- No Restrictions - Allow all hosts (default)
# CLI flags (highest precedence)
duck build --allowed-hosts github.com,gitlab.internal.com
duck sync --denied-hosts malicious-host.com --strict-hosts
# Environment variables (system-level)
export DUCK_ALLOWED_HOSTS="github.com,gitlab.internal.com"
export DUCK_DENIED_HOSTS="malicious-host.com"
export DUCK_STRICT_MODE="true" # Fail if no restrictions configured
# Security config file (recommended for organizations)
# Create ~/.duck/security.yaml or use --security-config flag
duck build --security-config /etc/duck/production-security.yamlComprehensive policy validation ensures compliance with organizational security requirements:
Policy Types:
- Checksum Validation: Force all templates to have checksums
- Commit Tracking: Require commit hash tracking for reproducibility
- Auto-Update Control: Override auto-update settings for stability
- File Permissions: Validate file ownership and permissions
- Template Validation: Ensure secure template configurations
- Host Access Control: Enforce repository access policies
Policy Violation Reporting:
# Policy violations are reported with detailed messages
duck sync --security-config production-security.yaml
# Example output:
# ❌ Policy Violations Found:
# • Template 'config-template' missing required checksum (forceChecksumValidation)
# • Template 'app-config' has trackCommitHash=false (forceCommitTracking)
# • File '/etc/app/config.yaml' has insecure permissions 0644 (enforceFilePermissions)
#
# 🔧 Policy Overrides Applied:
# • Template 'dynamic-config': autoUpdateOnChange disabled (disableAutoUpdate)
#
# 📊 Policy Enforcement Summary: 3 violations, 1 override, operation failedSecurity Rules:
- Deny beats allow: Denied hosts are blocked even if in allow list
- Strict mode: Fail if no restrictions are configured
- Fast validation: Host checking happens before git operations
- Case insensitive:
GitHub.COMmatchesgithub.com - Policy enforcement: Mandatory overrides ensure compliance
- Detailed reporting: Clear violation messages for remediation
See the security schema, example configurations, and full specification for complete details.
Duckfile can track and validate commit hashes to detect when remote templates change, ensuring reproducible builds and providing early warning of template updates.
Configuration in duck.yaml:
targets:
build:
template:
repo: https://github.com/example/templates.git
ref: main # Use branch/tag names, not commit hashes
path: Makefile.tpl
trackCommitHash: true # Enable tracking
autoUpdateOnChange: true # Auto-update when commits change
# Or set globally in settings
settings:
trackCommitHash: true
autoUpdateOnChange: false # Manual updates by defaultCLI flags override configuration:
# Enable tracking with manual updates
duck build --track-commit-hash
# Enable tracking with automatic updates
duck sync --track-commit-hash --auto-update-on-change
# Disable tracking entirely
duck --no-track-commit-hash buildEnvironment variables for system-wide defaults:
export DUCK_TRACK_COMMIT_HASH="true"
export DUCK_AUTO_UPDATE_ON_CHANGE="true"
duck build # Uses environment settingsFirst Run (tracking enabled):
- Fetches template from
repo@ref - Resolves branch/tag to actual commit hash
- Stores commit hash alongside cached template
- Renders and executes normally
Subsequent Runs:
- Checks if remote commit hash has changed
- Without auto-update: Warns about changes, uses cached template
- With auto-update: Automatically re-fetches and updates cache
Settings are resolved in this order (highest to lowest priority):
- CLI flags:
--track-commit-hash,--no-track-commit-hash,--auto-update-on-change,--no-auto-update-on-change - Environment variables:
DUCK_TRACK_COMMIT_HASH,DUCK_AUTO_UPDATE_ON_CHANGE - Template config:
template.trackCommitHash,template.autoUpdateOnChange - Global config:
settings.trackCommitHash,settings.autoUpdateOnChange - Default:
false(disabled)
Development (auto-update enabled):
# Templates update automatically as upstream changes
duck build --track-commit-hash --auto-update-on-change
# Output: "📦 Updating template cache: repo@main" (if changed)Production (manual updates):
# Get warned about changes but continue with cached template
duck build --track-commit-hash --no-auto-update-on-change
# Output: "🔄 commit hash changed for repo@main: abc123 -> def456"Strict reproducibility (tracking disabled):
# Use exact commit hashes in config, disable tracking
duck build --no-track-commit-hash
# Template ref: "a1b2c3d4e5f6..." (40-char commit hash)- Branch/tag only: Commit hash tracking requires
refto be a branch or tag name, not a commit hash - Auto-update dependency:
autoUpdateOnChangerequirestrackCommitHashto be enabled - Network resilience: Network failures during validation result in warnings, not errors
- Fast feedback: Remote checking happens before expensive git operations
Duck automatically discovers your configuration file or you can specify a custom path using the --config flag or DUCK_CONFIG environment variable.
Discovery Precedence (highest to lowest):
- CLI flag:
--config custom.yamlor-c custom.yaml - Environment variable:
DUCK_CONFIG=custom.yaml - Auto-discovery:
duck.yaml,duck.yml,.duck.yaml,.duck.yml
Examples:
# Use custom config file (highest precedence)
duck --config prod.yaml build
duck -c staging.yaml deploy
# Use environment variable
export DUCK_CONFIG="configs/dev.yaml"
duck build
duck sync
# Auto-discovery (default behavior)
duck build # Uses duck.yaml, duck.yml, .duck.yaml, or .duck.yml
# Multi-environment workflows
duck --config configs/dev.yaml sync
duck --config configs/staging.yaml deploy --dry-run
duck --config configs/prod.yaml deploy
# CI/CD integration
DUCK_CONFIG="ci/pipeline.yaml" duck test
DUCK_CONFIG="ci/deploy.yaml" duck deployUse Cases:
- Environment-specific configs: Separate files for dev/staging/prod
- Team customization: Personal config variants
- CI/CD pipelines: Different configs per build stage
- Multi-project support: Multiple duck files in the same workspace
Control verbosity with log levels, configured via CLI flag, environment variable, or config file.
Precedence (highest to lowest):
- CLI flag:
--log-level=debug - Environment variable:
DUCK_LOG_LEVEL=info - Config file:
settings.logLevel: warn - Default:
info
Log Levels (from least to most verbose):
error: Only critical errorswarn: Warnings and errorsinfo: High-level steps (default)debug: Detailed debugging information
Examples:
# Set via CLI flag (highest precedence)
duck --log-level=debug build
duck sync --log-level=warn
# Set via environment variable
export DUCK_LOG_LEVEL=debug
duck build
# Set via config file (duck.yaml)
settings:
logLevel: infoWhen target names conflict with Duck's built-in subcommands (like sync, list, clean), Duck shows warnings and provides the exec command for explicit target execution:
Example conflict:
targets:
sync: # Conflicts with 'duck sync' subcommand
binary: rsync
# ... template configBehavior:
# This runs the subcommand, not the target
duck sync
# Warning is shown for subcommands when conflicts exist
duck list
# ⚠️ Target 'sync' conflicts with subcommand. Use 'duck exec sync' to run the target.
# Use exec to explicitly run the target
duck exec sync # Runs 'sync' target
duck exec sync -- -v # Runs 'sync' target with argsAvailable commands that can conflict:
sync,list,clean,add,init,security,version
Duckfile automatically loads environment variables from .env files to streamline configuration management:
Create a .env file:
# Project defaults
PROJECT_NAME=myapp
GO_VERSION=1.21
DOCKER_REGISTRY=ghcr.io/myorg
DEBUG=trueUse in duck.yaml:
targets:
build:
variables:
PROJECT: !env PROJECT_NAME # Uses value from .env
GO_VERSION: !env GO_VERSION # Uses value from .env
REGISTRY: !env DOCKER_REGISTRY # Uses value from .envFile discovery order:
.env(current directory) - highest priority.duck/.env(duck cache directory).env.duck(duck-specific variant) - lowest priority
Precedence: CLI environment variables > .env file > defaults
Duckfile automatically exposes repository and template paths as environment variables during execution, enabling advanced template workflows:
# Available in your executed commands
echo "Repository: ${DUCK_REPO_PATH}"
echo "Template: ${DUCK_TEMPLATE_PATH}"
echo "Target: ${DUCK_TARGET_NAME}"
# Copy repository assets
cp -r "${DUCK_REPO_PATH}/assets" ./
# Execute repository scripts
"${DUCK_REPO_PATH}/scripts/setup.sh"Available Variables:
| Variable | Description | Example |
|---|---|---|
DUCK_REPO_PATH |
Path to cloned repository | .duck/objects/remote/abc123 |
DUCK_REPO_URL |
Repository URL | https://github.com/org/templates.git |
DUCK_REPO_REF |
Git reference used | main |
DUCK_TEMPLATE_PATH |
Source template file path | .duck/objects/template/ghi789/raw.tpl |
DUCK_RENDERED_PATH |
Rendered template file path | .duck/objects/rendered/def456/Makefile |
DUCK_SYMLINK_PATH |
Symlink path (what user sees) | .duck/build/Makefile |
DUCK_TARGET_NAME |
Target name being executed | build |
DUCK_CACHE_DIR |
Per-target cache directory | .duck/build |
Example Use Cases:
# Template that copies assets alongside rendering
targets:
frontend:
binary: npm
fileFlag: --config
template:
repo: https://github.com/company/frontend-templates.git
path: package.json.tpl
args: ["run", "build"]
# Script can use: cp -r "${DUCK_REPO_PATH}/public/*" ./public/
deploy:
binary: bash
fileFlag: -c
template:
repo: https://github.com/company/deploy-scripts.git
path: deploy.sh.tpl
# Template renders to a script that references other files in the repo
# Script content: source "${DUCK_REPO_PATH}/common.sh"See the specification for complete documentation.
- Load .env files automatically before any operation
- Resolve variables:
- !env NAME → os.Getenv(NAME)
- !cmd SHELL → /bin/sh -c SHELL (trimmed)
- !file PATH → file contents
- literal scalars (string/number/bool)
- Two‑tier deterministic caching:
- Remote layer: key = SHA1(repo + ref + path)
- Directory:
.duck/objects/remote/<remoteKey>/ - Contains: raw template file (
<basename>),commit.hash(always written on fetch), optionalchecksum.sha256
- Directory:
- Rendered layer: key = SHA1(sorted resolvedVarsJSON)
- Directory:
.duck/objects/rendered/<renderedKey>/ - Contains: rendered file (
<basename>),remote.key(points back to remote layer key)
- Directory:
- Remote layer: key = SHA1(repo + ref + path)
- Remote fetch happens only when the remote layer is missing, explicitly forced (
-f), checksum mismatch, or commit hash validation (when tracking enabled) says it changed and auto‑update is on. - Variable changes never trigger a remote fetch; they only produce a new rendered layer directory and prune the old one for that target.
- Commit hash behavior:
- The actual commit hash is always captured and stored in
commit.hashon every remote fetch (even if tracking disabled later). - When
trackCommitHashis enabled, Duck validates the stored hash against the current remote; if changed:- With
autoUpdateOnChange: remote layer is refreshed, then re-rendered. - Without auto-update: warning logged; existing remote layer kept.
- With
- The actual commit hash is always captured and stored in
- Rendering uses Go
text/template+ Sprig with optional custom delimiters. - A stable symlink at
renderedPath(or.duck/<target>/<basename>) points to the current rendered object. - Execute the tool:
binary fileFlag renderedPath [args …](unless target has nobinary). - Use
duck syncfor render-only workflows (nobinaryrequired).
- Use Sprig to transform values: {{ .PROJECT | upper }}
- Add now/env helpers: {{ now }} and {{ env "HOME" }}
- When the generated file itself uses Go templates (e.g., Taskfile), set
delimsso our engine renders only your placeholders and leaves the downstream engine’s{{ ... }}intact. - If you want missing variables to become empty strings, set
allowMissing: true. Default is strict.
| Path | Purpose |
|---|---|
cmd/ |
Entry point (main.go) |
cmd/duck/ |
Cobra command (root.go) |
internal/config/ |
Parser for duck.yaml |
internal/git/ |
Git wrapper for clone/fetch/checkout |
internal/run/ |
Render + cache + exec |
- git exit status 128: usually wrong ref or network; error message includes git’s stderr.
- “map has no entry …” during rendering: you are missing a variable and
allowMissingis false, or your delimiters collide with the target tool (setdelims). - On macOS, if a symlink isn’t resolving, remove it and re-run; Duck recreates it.
See the full specification: docs/spec.md
- Configuration Specification - Complete duck.yaml configuration reference
- Security User Guide - Comprehensive security features guide
- Security Schema - JSON schema for security configurations
If you prefer Duckfile to only manage templates and never launch external binaries, omit binary from your targets. You can then:
duck sync [target] [-f]to render and refresh symlinksduck clean [target]to purge caches
If a target has no binary, attempting to execute it via the root command will error with guidance to use sync instead.