Skip to content

Commit 026c899

Browse files
authored
feat: support self-signed certificates for remote taskfiles (#2537)
1 parent f672076 commit 026c899

File tree

16 files changed

+520
-8
lines changed

16 files changed

+520
-8
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
- Included Taskfiles with `silent: true` now properly propagate silence to their
99
tasks, while still allowing individual tasks to override with `silent: false`
1010
(#2640, #1319 by @trulede).
11+
- Added TLS certificate options for Remote Taskfiles: use `--cacert` for
12+
self-signed certificates and `--cert`/`--cert-key` for mTLS authentication
13+
(#2537, #2242 by @vmaerten).
1114

1215
## v3.47.0 - 2026-01-24
1316

completion/bash/task.bash

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ function _task()
2626
_filedir -d
2727
return $?
2828
;;
29+
--cacert|--cert|--cert-key)
30+
_filedir
31+
return $?
32+
;;
2933
-t|--taskfile)
3034
_filedir yaml || return $?
3135
_filedir yml

completion/fish/task.fish

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,9 @@ complete -c $GO_TASK_PROGNAME -n "__task_is_experiment_enabled REMOTE_TASKFILES"
111111
complete -c $GO_TASK_PROGNAME -n "__task_is_experiment_enabled REMOTE_TASKFILES" -l timeout -d 'timeout for remote Taskfile downloads'
112112
complete -c $GO_TASK_PROGNAME -n "__task_is_experiment_enabled REMOTE_TASKFILES" -l expiry -d 'cache expiry duration'
113113
complete -c $GO_TASK_PROGNAME -n "__task_is_experiment_enabled REMOTE_TASKFILES" -l remote-cache-dir -d 'directory to cache remote Taskfiles' -xa "(__fish_complete_directories)"
114+
complete -c $GO_TASK_PROGNAME -n "__task_is_experiment_enabled REMOTE_TASKFILES" -l cacert -d 'custom CA certificate for TLS' -r
115+
complete -c $GO_TASK_PROGNAME -n "__task_is_experiment_enabled REMOTE_TASKFILES" -l cert -d 'client certificate for mTLS' -r
116+
complete -c $GO_TASK_PROGNAME -n "__task_is_experiment_enabled REMOTE_TASKFILES" -l cert-key -d 'client certificate private key' -r
114117

115118
# RemoteTaskfiles experiment - Operations
116119
complete -c $GO_TASK_PROGNAME -n "__task_is_experiment_enabled REMOTE_TASKFILES" -l download -d 'download remote Taskfile'

completion/ps/task.ps1

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@ Register-ArgumentCompleter -CommandName task -ScriptBlock {
7777
$completions += [CompletionResult]::new('--timeout', '--timeout', [CompletionResultType]::ParameterName, 'download timeout')
7878
$completions += [CompletionResult]::new('--expiry', '--expiry', [CompletionResultType]::ParameterName, 'cache expiry')
7979
$completions += [CompletionResult]::new('--remote-cache-dir', '--remote-cache-dir', [CompletionResultType]::ParameterName, 'cache directory')
80+
$completions += [CompletionResult]::new('--cacert', '--cacert', [CompletionResultType]::ParameterName, 'custom CA certificate')
81+
$completions += [CompletionResult]::new('--cert', '--cert', [CompletionResultType]::ParameterName, 'client certificate')
82+
$completions += [CompletionResult]::new('--cert-key', '--cert-key', [CompletionResultType]::ParameterName, 'client private key')
8083
# Operations
8184
$completions += [CompletionResult]::new('--download', '--download', [CompletionResultType]::ParameterName, 'download remote Taskfile')
8285
$completions += [CompletionResult]::new('--clear-cache', '--clear-cache', [CompletionResultType]::ParameterName, 'clear cache')

completion/zsh/_task

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,9 @@ _task() {
117117
'(--timeout)--timeout[timeout for remote Taskfile downloads]:duration: '
118118
'(--expiry)--expiry[cache expiry duration]:duration: '
119119
'(--remote-cache-dir)--remote-cache-dir[directory to cache remote Taskfiles]:cache dir:_dirs'
120+
'(--cacert)--cacert[custom CA certificate for TLS]:file:_files'
121+
'(--cert)--cert[client certificate for mTLS]:file:_files'
122+
'(--cert-key)--cert-key[client certificate private key]:file:_files'
120123
)
121124
fi
122125

executor.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ type (
3838
Timeout time.Duration
3939
CacheExpiryDuration time.Duration
4040
RemoteCacheDir string
41+
CACert string
42+
Cert string
43+
CertKey string
4144
Watch bool
4245
Verbose bool
4346
Silent bool
@@ -287,6 +290,45 @@ func (o *remoteCacheDirOption) ApplyToExecutor(e *Executor) {
287290
e.RemoteCacheDir = o.dir
288291
}
289292

293+
// WithCACert sets the path to a custom CA certificate for TLS connections.
294+
func WithCACert(caCert string) ExecutorOption {
295+
return &caCertOption{caCert: caCert}
296+
}
297+
298+
type caCertOption struct {
299+
caCert string
300+
}
301+
302+
func (o *caCertOption) ApplyToExecutor(e *Executor) {
303+
e.CACert = o.caCert
304+
}
305+
306+
// WithCert sets the path to a client certificate for TLS connections.
307+
func WithCert(cert string) ExecutorOption {
308+
return &certOption{cert: cert}
309+
}
310+
311+
type certOption struct {
312+
cert string
313+
}
314+
315+
func (o *certOption) ApplyToExecutor(e *Executor) {
316+
e.Cert = o.cert
317+
}
318+
319+
// WithCertKey sets the path to a client certificate key for TLS connections.
320+
func WithCertKey(certKey string) ExecutorOption {
321+
return &certKeyOption{certKey: certKey}
322+
}
323+
324+
type certKeyOption struct {
325+
certKey string
326+
}
327+
328+
func (o *certKeyOption) ApplyToExecutor(e *Executor) {
329+
e.CertKey = o.certKey
330+
}
331+
290332
// WithWatch tells the [Executor] to keep running in the background and watch
291333
// for changes to the fingerprint of the tasks that are run. When changes are
292334
// detected, a new task run is triggered.

internal/flags/flags.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,9 @@ var (
8383
Timeout time.Duration
8484
CacheExpiryDuration time.Duration
8585
RemoteCacheDir string
86+
CACert string
87+
Cert string
88+
CertKey string
8689
Interactive bool
8790
)
8891

@@ -168,6 +171,9 @@ func init() {
168171
pflag.BoolVar(&ClearCache, "clear-cache", false, "Clear the remote cache.")
169172
pflag.DurationVar(&CacheExpiryDuration, "expiry", getConfig(config, func() *time.Duration { return config.Remote.CacheExpiry }, 0), "Expiry duration for cached remote Taskfiles.")
170173
pflag.StringVar(&RemoteCacheDir, "remote-cache-dir", getConfig(config, func() *string { return config.Remote.CacheDir }, env.GetTaskEnv("REMOTE_DIR")), "Directory to cache remote Taskfiles.")
174+
pflag.StringVar(&CACert, "cacert", getConfig(config, func() *string { return config.Remote.CACert }, ""), "Path to a custom CA certificate for HTTPS connections.")
175+
pflag.StringVar(&Cert, "cert", getConfig(config, func() *string { return config.Remote.Cert }, ""), "Path to a client certificate for HTTPS connections.")
176+
pflag.StringVar(&CertKey, "cert-key", getConfig(config, func() *string { return config.Remote.CertKey }, ""), "Path to a client certificate key for HTTPS connections.")
171177
}
172178
pflag.Parse()
173179

@@ -236,6 +242,11 @@ func Validate() error {
236242
return errors.New("task: --nested only applies to --json with --list or --list-all")
237243
}
238244

245+
// Validate certificate flags
246+
if (Cert != "" && CertKey == "") || (Cert == "" && CertKey != "") {
247+
return errors.New("task: --cert and --cert-key must be provided together")
248+
}
249+
239250
return nil
240251
}
241252

@@ -278,6 +289,9 @@ func (o *flagsOption) ApplyToExecutor(e *task.Executor) {
278289
task.WithTimeout(Timeout),
279290
task.WithCacheExpiryDuration(CacheExpiryDuration),
280291
task.WithRemoteCacheDir(RemoteCacheDir),
292+
task.WithCACert(CACert),
293+
task.WithCert(Cert),
294+
task.WithCertKey(CertKey),
281295
task.WithWatch(Watch),
282296
task.WithVerbose(Verbose),
283297
task.WithSilent(Silent),

setup.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,11 @@ func (e *Executor) Setup() error {
5555
}
5656

5757
func (e *Executor) getRootNode() (taskfile.Node, error) {
58-
node, err := taskfile.NewRootNode(e.Entrypoint, e.Dir, e.Insecure, e.Timeout)
58+
node, err := taskfile.NewRootNode(e.Entrypoint, e.Dir, e.Insecure, e.Timeout,
59+
taskfile.WithCACert(e.CACert),
60+
taskfile.WithCert(e.Cert),
61+
taskfile.WithCertKey(e.CertKey),
62+
)
5963
if os.IsNotExist(err) {
6064
return nil, errors.TaskfileNotFoundError{
6165
URI: fsext.DefaultDir(e.Entrypoint, e.Dir),
@@ -87,6 +91,9 @@ func (e *Executor) readTaskfile(node taskfile.Node) error {
8791
taskfile.WithTrustedHosts(e.TrustedHosts),
8892
taskfile.WithTempDir(e.TempDir.Remote),
8993
taskfile.WithCacheExpiryDuration(e.CacheExpiryDuration),
94+
taskfile.WithReaderCACert(e.CACert),
95+
taskfile.WithReaderCert(e.Cert),
96+
taskfile.WithReaderCertKey(e.CertKey),
9097
taskfile.WithDebugFunc(debugFunc),
9198
taskfile.WithPromptFunc(promptFunc),
9299
)

taskfile/node.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,14 @@ func NewRootNode(
3434
dir string,
3535
insecure bool,
3636
timeout time.Duration,
37+
opts ...NodeOption,
3738
) (Node, error) {
3839
dir = fsext.DefaultDir(entrypoint, dir)
3940
// If the entrypoint is "-", we read from stdin
4041
if entrypoint == "-" {
4142
return NewStdinNode(dir)
4243
}
43-
return NewNode(entrypoint, dir, insecure)
44+
return NewNode(entrypoint, dir, insecure, opts...)
4445
}
4546

4647
func NewNode(

taskfile/node_base.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ type (
1010
parent Node
1111
dir string
1212
checksum string
13+
caCert string
14+
cert string
15+
certKey string
1316
}
1417
)
1518

@@ -54,3 +57,21 @@ func (node *baseNode) Checksum() string {
5457
func (node *baseNode) Verify(checksum string) bool {
5558
return node.checksum == "" || node.checksum == checksum
5659
}
60+
61+
func WithCACert(caCert string) NodeOption {
62+
return func(node *baseNode) {
63+
node.caCert = caCert
64+
}
65+
}
66+
67+
func WithCert(cert string) NodeOption {
68+
return func(node *baseNode) {
69+
node.cert = cert
70+
}
71+
}
72+
73+
func WithCertKey(certKey string) NodeOption {
74+
return func(node *baseNode) {
75+
node.certKey = certKey
76+
}
77+
}

0 commit comments

Comments
 (0)