Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ RUN go mod download

# Copy the go source
COPY cmd/main.go cmd/main.go
COPY cmd/http_deny.go cmd/http_deny.go
COPY api/ api/
COPY controllers/ controllers/
COPY pkg/ pkg/
Expand All @@ -22,7 +23,7 @@ ARG VERSION
ARG GITVERSION
RUN GO111MODULE=on CGO_ENABLED=0 GOOS=linux GOARCH=${TARGETARCH} \
go build -a -ldflags "-s -w -X github.com/kubevela/workflow/version.VelaVersion=${VERSION:-undefined} -X github.com/kubevela/workflow/version.GitRevision=${GITVERSION:-undefined}" \
-o vela-workflow-${TARGETARCH} cmd/main.go
-o vela-workflow-${TARGETARCH} ./cmd

FROM ${BASE_IMAGE:-alpine:3.15}
# This is required by daemon connecting with cri
Expand Down
1 change: 1 addition & 0 deletions Dockerfile.e2e
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ RUN go mod download

# Copy the go source
COPY cmd/main.go main.go
COPY cmd/http_deny.go http_deny.go
COPY cmd/main_e2e_test.go main_e2e_test.go
COPY api/ api/
COPY controllers/ controllers/
Expand Down
3 changes: 3 additions & 0 deletions charts/vela-workflow/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ helm install --create-namespace -n vela-system workflow kubevela/vela-workflow -
| `workflow.cueUpgradeGenericDefaultGuardEnabled` | Enable generic default-guard hazard compatibility rewrite pass | `false` |
| `workflow.cueUpgradeKeepValidatorsSingletonEnabled` | Enable keepvalidators singleton concretization compatibility pass | `false` |
| `workflow.cueUpgradeEvalv3SelfRefGuardEnabled` | Enable evalv3 self-reference default-guard compatibility rewrite pass | `false` |
| `workflow.disableWorkflowHTTP` | Disable outbound HTTP from workflow request/webhook steps | `false` |
| `workflow.blockPrivateHTTPAddresses` | Block outbound HTTP to RFC-1918 and ULA destinations | `false` |
| `workflow.httpDeny.configMapName` | ConfigMap name in the release namespace containing extra HTTP denylist entries (`denyCIDRs`, `denyHosts`) | `""` |
| `workflow.backoff.maxTime.waitState` | The max backoff time of workflow in a wait condition | `60` |
| `workflow.backoff.maxTime.failedState` | The max backoff time of workflow in a failed condition | `300` |
| `workflow.step.errorRetryTimes` | The max retry times of a failed workflow step | `10` |
Expand Down
28 changes: 28 additions & 0 deletions charts/vela-workflow/config-templates/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Workflow HTTP denylist config

Apply the system config template, then create a denylist ConfigMap in the
workflow controller namespace:

```bash
vela config-template apply \
-f charts/vela-workflow/config-templates/workflow-http-deny.cue

vela config create workflow-http-deny \
Comment thread
roguepikachu marked this conversation as resolved.
--template workflow-http-deny \
--namespace vela-system \
'denyHosts={metadata.google.internal,*.example.com}' \
'denyCIDRs={10.0.0.0/8,169.254.169.254}'
```

Configure the controller to load that ConfigMap:

```bash
helm upgrade workflow kubevela/vela-workflow \
--namespace vela-system \
--reuse-values \
--set workflow.httpDeny.configMapName=workflow-http-deny
```

The Helm chart does not install this config template or ConfigMap
automatically. Creating a raw ConfigMap with the `denyHosts` and `denyCIDRs`
data keys remains supported.
41 changes: 41 additions & 0 deletions charts/vela-workflow/config-templates/workflow-http-deny.cue
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import (
"net"
"strings"
)

metadata: {
name: "workflow-http-deny"
alias: "Workflow HTTP Denylist"
description: "Additional host, IP, and CIDR destinations denied for workflow HTTP requests."
scope: "system"
sensitive: false
}

#IPAddress: string & net.IP
#IPCIDR: string & net.IPCIDR

#Hostname: string & =~"^([A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)(\\.([A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?))*\\.?$"
#WildcardHostname: string & =~"^\\*\\.([A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)(\\.([A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?))*\\.?$"
#DenyHost: #IPAddress | #Hostname | #WildcardHostname
#DenyCIDR: #IPAddress | #IPCIDR

template: {
outputs: configMap: {
apiVersion: "v1"
kind: "ConfigMap"
metadata: {
name: context.name
namespace: context.namespace
}
data: {
denyHosts: strings.Join(parameter.denyHosts, "\n")
denyCIDRs: strings.Join(parameter.denyCIDRs, "\n")
}
}
parameter: {
// +usage=Exact hostnames, IP addresses, or leading wildcard hostnames to deny.
denyHosts: [...#DenyHost]
// +usage=IP addresses or CIDR ranges to deny.
denyCIDRs: [...#DenyCIDR]
}
}
12 changes: 10 additions & 2 deletions charts/vela-workflow/templates/workflow-controller.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,11 @@ spec:
- "--feature-gates=EnablePatchStatusAtOnce={{- .Values.workflow.enablePatchStatusAtOnce | toString -}}"
- "--feature-gates=EnableSuspendOnFailure={{- .Values.workflow.enableSuspendOnFailure | toString -}}"
- "--feature-gates=EnableBackupWorkflowRecord={{- .Values.backup.enabled | toString -}}"
- "--feature-gates=DisableWorkflowHTTP={{- .Values.workflow.disableWorkflowHTTP | toString -}}"
- "--feature-gates=BlockPrivateHTTPAddresses={{- .Values.workflow.blockPrivateHTTPAddresses | toString -}}"
{{ if .Values.workflow.httpDeny.configMapName }}
- "--workflow-http-deny-configmap-name={{ .Values.workflow.httpDeny.configMapName }}"
{{ end }}
- "--group-by-label={{ .Values.workflow.groupByLabel }}"
- "--enable-external-package-for-default-compiler={{- .Values.workflow.enableExternalPackageForDefaultCompiler | toString -}}"
- "--enable-external-package-watch-for-default-compiler={{- .Values.workflow.enableExternalPackageWatchForDefaultCompiler | toString -}}"
Expand All @@ -162,16 +167,19 @@ spec:
{{ end }}
image: {{ .Values.imageRegistry }}{{ .Values.image.repository }}:{{ .Values.image.tag }}
imagePullPolicy: {{ quote .Values.image.pullPolicy }}
{{- if or .Values.envVar .Values.featureGates.enableCueExpVariable }}
env:
- name: POD_NAMESPACE
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: metadata.namespace
{{- with .Values.envVar }}
{{- toYaml . | nindent 12 }}
{{- end }}
{{- if .Values.featureGates.enableCueExpVariable }}
- name: CUE_EXPERIMENT
value: "evalv3=0,keepvalidators=0"
{{- end }}
{{- end }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
{{ if .Values.admissionWebhooks.enabled }}
Expand Down
7 changes: 7 additions & 0 deletions charts/vela-workflow/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ ignoreWorkflowWithoutControllerRequirement: false
## @param workflow.cueUpgradeGenericDefaultGuardEnabled Enable generic default-guard hazard compatibility rewrite pass
## @param workflow.cueUpgradeKeepValidatorsSingletonEnabled Enable keepvalidators singleton concretization compatibility pass
## @param workflow.cueUpgradeEvalv3SelfRefGuardEnabled Enable evalv3 self-reference default-guard compatibility rewrite pass
## @param workflow.disableWorkflowHTTP Disable outbound HTTP from workflow request/webhook steps
## @param workflow.blockPrivateHTTPAddresses Block outbound HTTP to RFC-1918 and ULA destinations
## @param workflow.httpDeny.configMapName ConfigMap name in controller namespace containing extra HTTP denylist entries
## @param workflow.backoff.maxTime.waitState The max backoff time of workflow in a wait condition
## @param workflow.backoff.maxTime.failedState The max backoff time of workflow in a failed condition
## @param workflow.step.errorRetryTimes The max retry times of a failed workflow step
Expand All @@ -46,6 +49,10 @@ workflow:
cueUpgradeGenericDefaultGuardEnabled: false
cueUpgradeKeepValidatorsSingletonEnabled: false
cueUpgradeEvalv3SelfRefGuardEnabled: false
disableWorkflowHTTP: false
blockPrivateHTTPAddresses: false
httpDeny:
configMapName: ""
backoff:
maxTime:
waitState: 60
Expand Down
42 changes: 42 additions & 0 deletions cmd/http_deny.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
Copyright 2026 The KubeVela Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package main

import (
"context"
"fmt"

"github.com/kubevela/workflow/pkg/utils/httpguard"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/manager"
)

func configureWorkflowHTTPDeny(ctx context.Context, reader client.Reader, mgr manager.Manager, configMapName, namespace string, blockPrivate bool) error {
httpguard.SetEnhancer(func(p httpguard.Policy) httpguard.Policy {
if blockPrivate {
p.BlockPrivate = true
}
return p
})
if err := httpguard.LoadConfigMap(ctx, reader, configMapName, namespace); err != nil {
return fmt.Errorf("initialize workflow HTTP deny ConfigMap: %w", err)
}
if err := httpguard.SetupWatcher(mgr, configMapName, namespace); err != nil {
return fmt.Errorf("watch workflow HTTP deny ConfigMap: %w", err)
}
return nil
}
15 changes: 15 additions & 0 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ func init() {
func main() {
var metricsAddr, logFilePath, probeAddr, pprofAddr, leaderElectionResourceLock, userAgent, certDir string
var backupStrategy, backupIgnoreStrategy, backupPersistType, groupByLabel, backupConfigSecretName, backupConfigSecretNamespace string
var workflowHTTPDenyConfigMapName string
var enableLeaderElection, useWebhook, logDebug, backupCleanOnBackup bool
var qps float64
var logFileMaxSize uint64
Expand Down Expand Up @@ -128,6 +129,7 @@ func main() {
flag.BoolVar(&backupCleanOnBackup, "backup-clean-on-backup", false, "Set the auto clean for backup workflow records, default is false")
flag.StringVar(&backupConfigSecretName, "backup-config-secret-name", "backup-config", "Set the secret name for backup workflow configs, default is backup-config")
flag.StringVar(&backupConfigSecretNamespace, "backup-config-secret-namespace", "vela-system", "Set the secret namespace for backup workflow configs, default is backup-config")
flag.StringVar(&workflowHTTPDenyConfigMapName, "workflow-http-deny-configmap-name", "", "ConfigMap name (in controller namespace) containing workflow HTTP denylist")
flag.BoolVar(&providers.EnableExternalPackageForDefaultCompiler, "enable-external-package-for-default-compiler", true, "Enable external package for default compiler")
flag.BoolVar(&providers.EnableExternalPackageWatchForDefaultCompiler, "enable-external-package-watch-for-default-compiler", false, "Enable external package watch for default compiler")
flag.BoolVar(wfupgrade.EnableCUEVersionCompatibility, "enable-cue-version-compatibility", *wfupgrade.EnableCUEVersionCompatibility, "Automatically rewrite legacy CUE syntax in stored definitions at render time.")
Expand Down Expand Up @@ -235,6 +237,12 @@ func main() {
}

kubeClient := mgr.GetClient()
controllerNamespace := resolveControllerNamespace()
blockPrivate := feature.DefaultMutableFeatureGate.Enabled(features.BlockPrivateHTTPAddresses)
if err := configureWorkflowHTTPDeny(context.Background(), mgr.GetAPIReader(), mgr, workflowHTTPDenyConfigMapName, controllerNamespace, blockPrivate); err != nil {
klog.ErrorS(err, "unable to configure workflow HTTP deny policy", "name", workflowHTTPDenyConfigMapName, "namespace", controllerNamespace)
os.Exit(1)
}
if groupByLabel != "" {
if err := mgr.Add(utils.NewRecycleCronJob(kubeClient, recycleDuration, "0 0 * * *", groupByLabel)); err != nil {
klog.Error(err, "unable to start recycle cronjob")
Expand Down Expand Up @@ -372,3 +380,10 @@ func waitWebhookSecretVolume(certDir string, timeout, interval time.Duration) er
}
}
}

func resolveControllerNamespace() string {
if ns := strings.TrimSpace(os.Getenv("POD_NAMESPACE")); ns != "" {
return ns
}
return "vela-system"
}
100 changes: 100 additions & 0 deletions cmd/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/*
Copyright 2026 The KubeVela Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package main

import (
"context"
"os"
"testing"

"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/client/fake"

"github.com/kubevela/workflow/pkg/utils/httpguard"
)

func TestResolveControllerNamespace(t *testing.T) {
t.Setenv("POD_NAMESPACE", "")
require.Equal(t, "vela-system", resolveControllerNamespace())

t.Setenv("POD_NAMESPACE", " my-ns ")
require.Equal(t, "my-ns", resolveControllerNamespace())

t.Setenv("POD_NAMESPACE", " ")
require.Equal(t, "vela-system", resolveControllerNamespace())
}

func TestResolveControllerNamespace_unset(t *testing.T) {
require.NoError(t, os.Unsetenv("POD_NAMESPACE"))
require.Equal(t, "vela-system", resolveControllerNamespace())
}

func TestConfigureWorkflowHTTPDeny_emptyConfigMap(t *testing.T) {
t.Cleanup(func() {
httpguard.SetDenyFragment(httpguard.Policy{ExactHosts: map[string]struct{}{}})
httpguard.SetEnhancer(nil)
})
scheme := runtime.NewScheme()
require.NoError(t, corev1.AddToScheme(scheme))
cli := fake.NewClientBuilder().WithScheme(scheme).Build()

require.NoError(t, configureWorkflowHTTPDeny(context.Background(), cli, nil, "", "vela-system", false))
require.True(t, httpguard.Current().BlockLinkLocal)
}

func TestConfigureWorkflowHTTPDeny_blockPrivateEnhancer(t *testing.T) {
t.Cleanup(func() {
httpguard.SetDenyFragment(httpguard.Policy{ExactHosts: map[string]struct{}{}})
httpguard.SetEnhancer(nil)
})
scheme := runtime.NewScheme()
require.NoError(t, corev1.AddToScheme(scheme))
cli := fake.NewClientBuilder().WithScheme(scheme).Build()

require.NoError(t, configureWorkflowHTTPDeny(context.Background(), cli, nil, "", "vela-system", true))
require.True(t, httpguard.Current().BlockPrivate)
}

func TestConfigureWorkflowHTTPDeny_loadsConfigMap(t *testing.T) {
t.Cleanup(func() {
httpguard.SetDenyFragment(httpguard.Policy{ExactHosts: map[string]struct{}{}})
httpguard.SetEnhancer(nil)
})
scheme := runtime.NewScheme()
require.NoError(t, corev1.AddToScheme(scheme))
cm := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{Name: "workflow-http-deny", Namespace: "vela-system"},
Data: map[string]string{httpguard.ConfigMapKeyDenyHosts: "denied.example"},
}
cli := fake.NewClientBuilder().WithScheme(scheme).WithObjects(cm).Build()

require.NoError(t, httpguard.LoadConfigMap(context.Background(), cli, "workflow-http-deny", "vela-system"))
require.Error(t, httpguard.Current().BlockedHost("denied.example"))
}

func TestConfigureWorkflowHTTPDeny_missingConfigMap(t *testing.T) {
scheme := runtime.NewScheme()
require.NoError(t, corev1.AddToScheme(scheme))
cli := fake.NewClientBuilder().WithScheme(scheme).Build()

err := configureWorkflowHTTPDeny(context.Background(), cli, nil, "missing", "vela-system", false)
require.Error(t, err)
require.Contains(t, err.Error(), "initialize workflow HTTP deny ConfigMap")
}
6 changes: 6 additions & 0 deletions pkg/features/controller_features.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,19 @@ const (
EnablePatchStatusAtOnce featuregate.Feature = "EnablePatchStatusAtOnce"
// EnableWatchEventListener enable watch event listener
EnableWatchEventListener featuregate.Feature = "EnableWatchEventListener"
// DisableWorkflowHTTP if set, outbound HTTP from workflow request/webhook steps is disallowed
DisableWorkflowHTTP featuregate.Feature = "DisableWorkflowHTTP"
// BlockPrivateHTTPAddresses if set, outbound HTTP to RFC-1918 and ULA destinations is blocked
BlockPrivateHTTPAddresses featuregate.Feature = "BlockPrivateHTTPAddresses"
)

var defaultFeatureGates = map[featuregate.Feature]featuregate.FeatureSpec{
EnableSuspendOnFailure: {Default: false, PreRelease: featuregate.Alpha},
EnableBackupWorkflowRecord: {Default: false, PreRelease: featuregate.Alpha},
EnablePatchStatusAtOnce: {Default: false, PreRelease: featuregate.Alpha},
EnableWatchEventListener: {Default: false, PreRelease: featuregate.Alpha},
DisableWorkflowHTTP: {Default: false, PreRelease: featuregate.Alpha},
BlockPrivateHTTPAddresses: {Default: false, PreRelease: featuregate.Alpha},
}

func init() {
Expand Down
Loading
Loading