fix(swarm): allow manager operation header in reverse proxy [BE-13066] (#3174)

This commit is contained in:
Devon Steenberg
2026-07-21 09:02:47 +12:00
committed by GitHub
parent e5b306eb4f
commit 9cae64cc3e
4 changed files with 53 additions and 47 deletions
+19 -15
View File
@@ -7,25 +7,29 @@ import (
"strings"
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libstack/swarm"
)
// Note that we discard any non-canonical headers by design
var allowedHeaders = map[string]struct{}{
"Accept": {},
"Accept-Encoding": {},
"Accept-Language": {},
"Cache-Control": {},
"Connection": {},
"Content-Length": {},
"Content-Type": {},
"Private-Token": {},
"Upgrade": {},
"User-Agent": {},
"X-Portaineragent-Target": {},
"X-Portainer-Volumename": {},
"X-Registry-Auth": {},
"X-Registry-Config": {},
"X-Stream-Protocol-Version": {},
"Accept": {},
"Accept-Encoding": {},
"Accept-Language": {},
"Cache-Control": {},
"Connection": {},
"Content-Length": {},
"Content-Type": {},
"Private-Token": {},
"Upgrade": {},
"User-Agent": {},
"X-Portaineragent-Target": {},
http.CanonicalHeaderKey(swarm.ManagerOperationHeader): {},
"X-Portainer-Volumename": {},
"X-Registry-Auth": {},
"X-Registry-Config": {},
"X-Stream-Protocol-Version": {},
// WebSocket headers those are required for kubectl exec/attach/port-forward operations
"Sec-Websocket-Key": {},
"Sec-Websocket-Version": {},
+29 -27
View File
@@ -65,22 +65,23 @@ func Test_createRewriteFn(t *testing.T) {
"GET",
"https://agent-portainer.io/test?c=7",
map[string]string{
"Authorization": "secret",
"Proxy-Authorization": "secret",
"Cookie": "secret",
"X-Csrf-Token": "secret",
"X-Api-Key": "secret",
"Accept": "application/json",
"Accept-Encoding": "gzip",
"Accept-Language": "en-GB",
"Cache-Control": "None",
"Content-Length": "100",
"Content-Type": "application/json",
"Private-Token": "test-private-token",
"User-Agent": "test-user-agent",
"X-Portaineragent-Target": "test-agent-1",
"X-Portainer-Volumename": "test-volume-1",
"X-Registry-Auth": "test-registry-auth",
"Authorization": "secret",
"Proxy-Authorization": "secret",
"Cookie": "secret",
"X-Csrf-Token": "secret",
"X-Api-Key": "secret",
"Accept": "application/json",
"Accept-Encoding": "gzip",
"Accept-Language": "en-GB",
"Cache-Control": "None",
"Content-Length": "100",
"Content-Type": "application/json",
"Private-Token": "test-private-token",
"User-Agent": "test-user-agent",
"X-Portaineragent-Target": "test-agent-1",
"X-Portaineragent-Manageroperation": "1",
"X-Portainer-Volumename": "test-volume-1",
"X-Registry-Auth": "test-registry-auth",
},
true,
),
@@ -89,17 +90,18 @@ func Test_createRewriteFn(t *testing.T) {
"GET",
"https://portainer.io/api/docker/test?a=5&b=6&c=7",
map[string]string{
"Accept": "application/json",
"Accept-Encoding": "gzip",
"Accept-Language": "en-GB",
"Cache-Control": "None",
"Content-Length": "100",
"Content-Type": "application/json",
"Private-Token": "test-private-token",
"User-Agent": "test-user-agent",
"X-Portaineragent-Target": "test-agent-1",
"X-Portainer-Volumename": "test-volume-1",
"X-Registry-Auth": "test-registry-auth",
"Accept": "application/json",
"Accept-Encoding": "gzip",
"Accept-Language": "en-GB",
"Cache-Control": "None",
"Content-Length": "100",
"Content-Type": "application/json",
"Private-Token": "test-private-token",
"User-Agent": "test-user-agent",
"X-Portaineragent-Target": "test-agent-1",
"X-Portaineragent-Manageroperation": "1",
"X-Portainer-Volumename": "test-volume-1",
"X-Registry-Auth": "test-registry-auth",
},
true,
),
+3 -3
View File
@@ -32,10 +32,10 @@ import (
"github.com/rs/zerolog/log"
)
// managerOperationHeader forces the Portainer agent to route requests to a swarm
// ManagerOperationHeader forces the Portainer agent to route requests to a swarm
// manager node. Swarm-scoped operations (e.g. network create/remove) fail on worker
// nodes without it. This constant is originally defined in the agent, see package/agent/README.md.
const managerOperationHeader = "X-PortainerAgent-ManagerOperation"
const ManagerOperationHeader = "X-PortainerAgent-ManagerOperation"
// cliOptions builds the Docker CLI options for swarm operations, always setting the
// manager-operation header so requests proxied through an agent target a manager node.
@@ -43,7 +43,7 @@ func cliOptions(host string, registries []configtypes.AuthConfig) libstack.Docke
return libstack.DockerCliOptions{
Host: host,
Registries: registries,
Headers: map[string]string{managerOperationHeader: "1"},
Headers: map[string]string{ManagerOperationHeader: "1"},
}
}
+2 -2
View File
@@ -101,13 +101,13 @@ func Test_cliOptions(t *testing.T) {
expected: libstack.DockerCliOptions{
Host: "tcp://127.0.0.1:2377",
Registries: registries,
Headers: map[string]string{managerOperationHeader: "1"},
Headers: map[string]string{ManagerOperationHeader: "1"},
},
},
{
name: "empty host and nil registries still set the header",
expected: libstack.DockerCliOptions{
Headers: map[string]string{managerOperationHeader: "1"},
Headers: map[string]string{ManagerOperationHeader: "1"},
},
},
}