From 5a32b358f5d34db7e6b395aedc143e56c5ce87c2 Mon Sep 17 00:00:00 2001 From: Varun Athreya Date: Tue, 23 Jun 2026 16:02:46 +0530 Subject: [PATCH 1/6] feat(cmd): add kong-admin-token flag --- cmd/root.go | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index e91cd2619..10ca600be 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -227,6 +227,13 @@ It can be used to export, import, or sync entities to Kong.`, rootCmd.MarkFlagsMutuallyExclusive("konnect-runtime-group-name", "konnect-control-plane-name") + rootCmd.PersistentFlags().String("kong-admin-token", "", + "Token to use for authentication with Kong's Admin API.\n"+ + "This value can also be set using DECK_KONG_ADMIN_TOKEN "+ + "environment variable.") + viper.BindPFlag("kong-admin-token", + rootCmd.PersistentFlags().Lookup("kong-admin-token")) + rootCmd.AddCommand(newVersionCmd()) rootCmd.AddCommand(newCompletionCmd()) rootCmd.AddCommand(newSyncCmd(true)) // deprecated, to exist under the `gateway` subcommand only @@ -332,7 +339,11 @@ func initConfig() { tlsSkipVerify := viper.GetBool("tls-skip-verify") tlsCACert := caCertContent - rootConfig.Headers = extendHeaders(viper.GetStringSlice("headers")) + rootConfig.Headers = extendHeaders( + viper.GetStringSlice("headers"), + header{name: "Kong-Admin-Token", value: viper.GetString("kong-admin-token")}, + ) + rootConfig.SkipWorkspaceCrud = viper.GetBool("skip-workspace-crud") rootConfig.Debug = (viper.GetInt("verbose") >= 1) rootConfig.Timeout = (viper.GetInt("timeout")) @@ -451,9 +462,19 @@ func initKonnectConfig() error { return nil } -func extendHeaders(headers []string) []string { - userAgentHeader := fmt.Sprintf("User-Agent:decK/%s", VERSION) - headers = append(headers, userAgentHeader) +type header struct { + name string + value string +} + +func extendHeaders(headers []string, extra ...header) []string { + headers = append(headers, fmt.Sprintf("User-Agent:decK/%s", VERSION)) + + for _, h := range extra { + if h.value != "" { + headers = append(headers, fmt.Sprintf("%s:%s", h.name, h.value)) + } + } return headers } From 9c4a99f5023456c2a05a2ecfcb10c1235caf1900 Mon Sep 17 00:00:00 2001 From: Varun Athreya Date: Wed, 24 Jun 2026 11:46:13 +0530 Subject: [PATCH 2/6] feat: add support for RBAC admin token in integration tests --- .ci/setup_kong_ee.sh | 10 ++- .github/workflows/integration-enterprise.yaml | 66 +++++++++++++++++++ tests/integration/rbac_admin_token_test.go | 64 ++++++++++++++++++ tests/integration/test_utils.go | 6 ++ 4 files changed, 145 insertions(+), 1 deletion(-) create mode 100644 tests/integration/rbac_admin_token_test.go diff --git a/.ci/setup_kong_ee.sh b/.ci/setup_kong_ee.sh index c07bfbbc7..d080a12a4 100755 --- a/.ci/setup_kong_ee.sh +++ b/.ci/setup_kong_ee.sh @@ -64,11 +64,17 @@ readonly GATEWAY_CONTAINER_NAME=kong KONG_ROUTER_FLAVOR=${KONG_ROUTER_FLAVOR:-'traditional_compatible'} KONG_CUSTOM_PLUGIN_STREAMING_ENABLED=${KONG_CUSTOM_PLUGIN_STREAMING_ENABLED:-'on'} +KONG_ENFORCE_RBAC=${KONG_ENFORCE_RBAC:-'off'} +if [[ "${KONG_ENFORCE_RBAC}" == "on" ]]; then + KONG_PASSWORD=${KONG_PASSWORD:-'kong_admin_token'} +fi +KONG_PASSWORD=${KONG_PASSWORD:-''} initNetwork initDb initMigrations "${KONG_IMAGE}" \ - -e "KONG_LICENSE_DATA=${KONG_LICENSE_DATA}" + -e "KONG_LICENSE_DATA=${KONG_LICENSE_DATA}" \ + -e "KONG_PASSWORD=${KONG_PASSWORD}" # Start Kong Gateway EE docker run \ @@ -83,6 +89,8 @@ docker run \ -e "MY_SECRET_KEY=$MY_SECRET_KEY" \ -e "KONG_ROUTER_FLAVOR=${KONG_ROUTER_FLAVOR}" \ -e "KONG_CUSTOM_PLUGIN_STREAMING_ENABLED=${KONG_CUSTOM_PLUGIN_STREAMING_ENABLED}" \ + -e "KONG_ENFORCE_RBAC=${KONG_ENFORCE_RBAC}" \ + -e "KONG_PASSWORD=${KONG_PASSWORD}" \ -p 8000:8000 \ -p 8443:8443 \ -p 8001:8001 \ diff --git a/.github/workflows/integration-enterprise.yaml b/.github/workflows/integration-enterprise.yaml index d4de82fd1..8a6170f3f 100644 --- a/.github/workflows/integration-enterprise.yaml +++ b/.github/workflows/integration-enterprise.yaml @@ -72,3 +72,69 @@ jobs: KONG_LICENSE_DATA: ${{ steps.license.outputs.license }} run: make test-integration continue-on-error: ${{ matrix.kong_image == 'kong/kong-gateway-dev:latest' }} + + # RBAC enforcement is orthogonal to router flavor, so this job runs only the + # --kong-admin-token test (not the whole suite) against the same Kong versions + # as the matrix above — version-only, deliberately NOT crossed with + # router_flavor to avoid a combinatorial blow-up. + integration-rbac: + # Skip if the PR has the 'skip-ee' label + if: ${{ github.event_name == 'push' || !contains(github.event.pull_request.labels.*.name, 'skip-ee') }} + timeout-minutes: ${{ fromJSON(vars.DECK_WORKFLOW_GW_TIMEOUT || '10') }} + strategy: + matrix: + kong_image: + - 'kong/kong-gateway:2.8' + - 'kong/kong-gateway:3.4' + - 'kong/kong-gateway:3.5' + - 'kong/kong-gateway:3.6' + - 'kong/kong-gateway:3.7' + - 'kong/kong-gateway:3.8' + - 'kong/kong-gateway:3.9' + - 'kong/kong-gateway:3.10' + - 'kong/kong-gateway:3.11' + - 'kong/kong-gateway:3.12' + - 'kong/kong-gateway:3.13' + - 'kong/kong-gateway:3.14' + - 'kong/kong-gateway-dev:latest' + env: + KONG_ANONYMOUS_REPORTS: "off" + KONG_IMAGE: ${{ matrix.kong_image }} + # Enforce RBAC and seed the kong_admin super-admin token. KONG_PASSWORD is + # consumed by the bootstrap migration, KONG_ADMIN_TOKEN by the go-kong + # test client. DECK_KONG_ADMIN_TOKEN is intentionally left unset so the + # test controls authentication explicitly via the --kong-admin-token flag. + KONG_ENFORCE_RBAC: 'on' + KONG_PASSWORD: 'kong_admin_token' + KONG_ADMIN_TOKEN: 'kong_admin_token' + + runs-on: ubuntu-latest + steps: + - name: Harden Runner + uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 + with: + egress-policy: audit + - name: Checkout repository + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - name: Setup go + uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 + with: + go-version-file: go.mod + - name: Login to Docker Hub + uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef + with: + username: ${{secrets.DOCKER_ORG_NAME}} + password: ${{secrets.DOCKER_ORG_TOKEN}} + - uses: Kong/kong-license@master + id: license + with: + op-token: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} + - name: Setup Kong + env: + KONG_LICENSE_DATA: ${{ steps.license.outputs.license }} + run: make setup-kong-ee + - name: Run RBAC admin-token integration test + env: + KONG_LICENSE_DATA: ${{ steps.license.outputs.license }} + run: make test-integration GOTESTFLAGS='-run Test_RBAC_AdminToken' + continue-on-error: ${{ matrix.kong_image == 'kong/kong-gateway-dev:latest' }} diff --git a/tests/integration/rbac_admin_token_test.go b/tests/integration/rbac_admin_token_test.go new file mode 100644 index 000000000..2fededecc --- /dev/null +++ b/tests/integration/rbac_admin_token_test.go @@ -0,0 +1,64 @@ +//go:build integration + +package integration + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// Test_RBAC_AdminToken exercises the --kong-admin-token flag (added to +// authenticate against an RBAC-enabled Kong Admin API). +// +// It only runs against an Enterprise Kong instance with RBAC enforcement +// enabled, which is provided by the dedicated `integration-rbac` job in +// .github/workflows/integration-enterprise.yaml. When RBAC is enforced, a +// command that talks to the Admin API must fail without a valid admin token +// and succeed once the token is supplied. +func Test_RBAC_AdminToken(t *testing.T) { + runWhenRBAC(t, ">=2.8.0") + + // disable analytics for integration tests + t.Setenv("DECK_ANALYTICS", "off") + + // The CI job seeds the kong_admin token in KONG_ADMIN_TOKEN; fall back to + // the decK CLI variable for local runs. + adminToken := os.Getenv("KONG_ADMIN_TOKEN") + if adminToken == "" { + adminToken = os.Getenv("DECK_KONG_ADMIN_TOKEN") + } + require.NotEmpty(t, adminToken, + "KONG_ADMIN_TOKEN or DECK_KONG_ADMIN_TOKEN must be set when running RBAC tests") + + // online validation hits the Admin API but does not mutate state, so no + // reset/cleanup (which would itself require the token) is needed. + const stateFile = "testdata/validate/kong-ee.yaml" + + t.Run("fails when kong-admin-token is not passed", func(t *testing.T) { + // make sure the CLI cannot pick the token up from the environment, so + // the request reaches Kong unauthenticated. + t.Setenv("DECK_KONG_ADMIN_TOKEN", "") + + err := validate(ONLINE, stateFile) + require.Error(t, err, + "online validate should fail against an RBAC-enabled Kong without an admin token") + // Assert it fails *specifically* because of authentication (HTTP 401) + // rather than some unrelated error (bad file, gateway down, etc.). + // go-kong formats API errors as `HTTP status 401 (message: ...)`. + assert.Contains(t, err.Error(), "401", + "expected an authentication failure (HTTP 401), got: %v", err) + }) + + t.Run("succeeds when kong-admin-token is passed", func(t *testing.T) { + // scrub the env so the token can only come from the flag, proving the + // flag is what authenticates the request. + t.Setenv("DECK_KONG_ADMIN_TOKEN", "") + + err := validate(ONLINE, stateFile, "--kong-admin-token", adminToken) + require.NoError(t, err, + "online validate should succeed against an RBAC-enabled Kong with a valid admin token") + }) +} diff --git a/tests/integration/test_utils.go b/tests/integration/test_utils.go index 2bf199d52..0356a9bd8 100644 --- a/tests/integration/test_utils.go +++ b/tests/integration/test_utils.go @@ -129,6 +129,12 @@ func runWhenExpressions(t *testing.T, semverRange string) { kong.RunWhenKongRouterFlavor(t, "expressions") } +func runWhenRBAC(t *testing.T, semverRange string) { + t.Helper() + skipWhenKonnect(t) + kong.RunWhenEnterprise(t, semverRange, kong.RequiredFeatures{RBAC: true}) +} + func sortSlices(x, y interface{}) bool { var xName, yName string switch xEntity := x.(type) { From 78d8db18d8ca27bd061a7f4c3a46907ab48b114f Mon Sep 17 00:00:00 2001 From: Varun Athreya Date: Wed, 24 Jun 2026 13:41:33 +0530 Subject: [PATCH 3/6] fix: give --kong-admin-token precedence over colliding headers --- .github/workflows/integration-enterprise.yaml | 8 ------ cmd/root.go | 25 ++++++++++++++++--- tests/integration/rbac_admin_token_test.go | 15 +++++++++++ 3 files changed, 37 insertions(+), 11 deletions(-) diff --git a/.github/workflows/integration-enterprise.yaml b/.github/workflows/integration-enterprise.yaml index 8a6170f3f..739e79d56 100644 --- a/.github/workflows/integration-enterprise.yaml +++ b/.github/workflows/integration-enterprise.yaml @@ -73,10 +73,6 @@ jobs: run: make test-integration continue-on-error: ${{ matrix.kong_image == 'kong/kong-gateway-dev:latest' }} - # RBAC enforcement is orthogonal to router flavor, so this job runs only the - # --kong-admin-token test (not the whole suite) against the same Kong versions - # as the matrix above — version-only, deliberately NOT crossed with - # router_flavor to avoid a combinatorial blow-up. integration-rbac: # Skip if the PR has the 'skip-ee' label if: ${{ github.event_name == 'push' || !contains(github.event.pull_request.labels.*.name, 'skip-ee') }} @@ -100,10 +96,6 @@ jobs: env: KONG_ANONYMOUS_REPORTS: "off" KONG_IMAGE: ${{ matrix.kong_image }} - # Enforce RBAC and seed the kong_admin super-admin token. KONG_PASSWORD is - # consumed by the bootstrap migration, KONG_ADMIN_TOKEN by the go-kong - # test client. DECK_KONG_ADMIN_TOKEN is intentionally left unset so the - # test controls authentication explicitly via the --kong-admin-token flag. KONG_ENFORCE_RBAC: 'on' KONG_PASSWORD: 'kong_admin_token' KONG_ADMIN_TOKEN: 'kong_admin_token' diff --git a/cmd/root.go b/cmd/root.go index 10ca600be..d67ab1997 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -468,14 +468,33 @@ type header struct { } func extendHeaders(headers []string, extra ...header) []string { - headers = append(headers, fmt.Sprintf("User-Agent:decK/%s", VERSION)) + result := make([]string, 0, len(headers)+len(extra)+1) + result = append(result, fmt.Sprintf("User-Agent:decK/%s", VERSION)) + + for _, h := range headers { + name, _, _ := strings.Cut(h, ":") + if !overriddenBy(name, extra) { + result = append(result, h) + } + } for _, h := range extra { if h.value != "" { - headers = append(headers, fmt.Sprintf("%s:%s", h.name, h.value)) + result = append(result, fmt.Sprintf("%s:%s", h.name, h.value)) + } + } + return result +} + +// reports whether the given header name collides with an explicit +func overriddenBy(name string, extra []header) bool { + name = strings.TrimSpace(name) + for _, h := range extra { + if h.value != "" && strings.EqualFold(name, h.name) { + return true } } - return headers + return false } func init() { diff --git a/tests/integration/rbac_admin_token_test.go b/tests/integration/rbac_admin_token_test.go index 2fededecc..75362a565 100644 --- a/tests/integration/rbac_admin_token_test.go +++ b/tests/integration/rbac_admin_token_test.go @@ -61,4 +61,19 @@ func Test_RBAC_AdminToken(t *testing.T) { require.NoError(t, err, "online validate should succeed against an RBAC-enabled Kong with a valid admin token") }) + + t.Run("kong-admin-token takes precedence over a colliding --headers value", func(t *testing.T) { + // scrub the env so the token can only come from the flags under test. + t.Setenv("DECK_KONG_ADMIN_TOKEN", "") + + // Supply an invalid Kong-Admin-Token via --headers alongside the valid + // token via --kong-admin-token. The explicit flag must win, so the + // invalid header value is dropped and the request authenticates. + err := validate(ONLINE, stateFile, + "--headers", "Kong-Admin-Token:invalid-token", + "--kong-admin-token", adminToken) + require.NoError(t, err, + "online validate should succeed: --kong-admin-token must override the "+ + "colliding Kong-Admin-Token supplied via --headers") + }) } From 1097eb29960c1f78f6280f79aaca2614c291239e Mon Sep 17 00:00:00 2001 From: Varun Athreya Date: Wed, 24 Jun 2026 16:13:05 +0530 Subject: [PATCH 4/6] fix: add comment to skip headers set by dedicated flags to avoid collisions --- cmd/root.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cmd/root.go b/cmd/root.go index d67ab1997..e0615525c 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -473,6 +473,7 @@ func extendHeaders(headers []string, extra ...header) []string { for _, h := range headers { name, _, _ := strings.Cut(h, ":") + // skip --headers entries already set by a dedicated flag to avoid header collisions if !overriddenBy(name, extra) { result = append(result, h) } @@ -486,7 +487,7 @@ func extendHeaders(headers []string, extra ...header) []string { return result } -// reports whether the given header name collides with an explicit +// reports whether the given header name collides with an explicit header set by a dedicated flag func overriddenBy(name string, extra []header) bool { name = strings.TrimSpace(name) for _, h := range extra { From 8bbf38f3338f8b11e69da7fa0d24424d6146999f Mon Sep 17 00:00:00 2001 From: Varun Athreya Date: Wed, 24 Jun 2026 16:23:58 +0530 Subject: [PATCH 5/6] fix: remove non lts Kong gateway versions from integration tests --- .github/workflows/integration-enterprise.yaml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/.github/workflows/integration-enterprise.yaml b/.github/workflows/integration-enterprise.yaml index 739e79d56..4b53bf3f8 100644 --- a/.github/workflows/integration-enterprise.yaml +++ b/.github/workflows/integration-enterprise.yaml @@ -82,15 +82,7 @@ jobs: kong_image: - 'kong/kong-gateway:2.8' - 'kong/kong-gateway:3.4' - - 'kong/kong-gateway:3.5' - - 'kong/kong-gateway:3.6' - - 'kong/kong-gateway:3.7' - - 'kong/kong-gateway:3.8' - - 'kong/kong-gateway:3.9' - 'kong/kong-gateway:3.10' - - 'kong/kong-gateway:3.11' - - 'kong/kong-gateway:3.12' - - 'kong/kong-gateway:3.13' - 'kong/kong-gateway:3.14' - 'kong/kong-gateway-dev:latest' env: From 33af6ff297860b936b6b6d6b3f6e611cb8f699aa Mon Sep 17 00:00:00 2001 From: Varun Athreya Date: Fri, 26 Jun 2026 13:31:05 +0530 Subject: [PATCH 6/6] fix: replace assert with require for authentication error validation in RBAC tests --- tests/integration/rbac_admin_token_test.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/integration/rbac_admin_token_test.go b/tests/integration/rbac_admin_token_test.go index 75362a565..8371de1b0 100644 --- a/tests/integration/rbac_admin_token_test.go +++ b/tests/integration/rbac_admin_token_test.go @@ -6,7 +6,6 @@ import ( "os" "testing" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -45,10 +44,10 @@ func Test_RBAC_AdminToken(t *testing.T) { err := validate(ONLINE, stateFile) require.Error(t, err, "online validate should fail against an RBAC-enabled Kong without an admin token") - // Assert it fails *specifically* because of authentication (HTTP 401) + // Require that it fails *specifically* because of authentication (HTTP 401) // rather than some unrelated error (bad file, gateway down, etc.). // go-kong formats API errors as `HTTP status 401 (message: ...)`. - assert.Contains(t, err.Error(), "401", + require.Contains(t, err.Error(), "401", "expected an authentication failure (HTTP 401), got: %v", err) })