Select your language

Migrating Portainer Agent to Edge Agent Standard across several OpenShift/OKD clusters exposed a chain of independent issues: filesystem permissions, immutable Kubernetes Service fields, DNS resolution, incomplete credentials, and deployment automation. This technical report documents the observed symptoms, confirmed causes, and corrective actions.

Note: this is an anonymized real-world technical case. Organization, cluster, node, domain, IP address, environment ID, and credential details have been removed. All examples use generic names and must be adapted before running commands.

1. Background: from the conventional Agent to Edge Agent

The original architecture used Portainer Community Edition with a conventional agent deployed in each cluster. Each agent was exposed through a Kubernetes LoadBalancer Service backed by MetalLB, and the central server accessed its API over TCP port 9001.

Portainer Server -- TCP 9001 -- LoadBalancer -- Portainer Agent -- Kubernetes API

The target architecture used Edge Agent Standard, which reverses the direction in which connections are established. The agent initiates outbound HTTPS polling to the Portainer API and opens the Edge tunnel over TCP port 8000 when required. Each agent no longer needs its own externally exposed load balancer.

OpenShift / Edge Agent -- HTTPS ----> Portainer API
                       -- TCP 8000 -> Portainer Edge Tunnel

2. Initial failure: insufficient permissions to generate certificates

Early tests with the conventional agent produced a filesystem error:

unable to generate self-signed certificates
open cert.pem: permission denied

OpenShift enforces container security policies through Security Context Constraints (SCC). These policies may prevent containers from running as root and restrict writes to paths inside the container image. The agent attempted to create a certificate in a location that was not writable by its effective user.

Diagnosis: a ServiceAccount's Kubernetes RBAC privileges are entirely separate from the permissions of the process inside the container. Even a Kubernetes administrative role cannot fix a filesystem permission denied error.

The deployment was therefore adapted to support unprivileged execution rather than permanently broadening the container's security privileges.

3. Migration failure: an immutable ClusterIP

Applying the new Edge Agent manifest to the existing Service failed with:

spec.clusterIPs[0]: Invalid value: ["None"]:
may not change once set

The old Service had an allocated ClusterIP. The new agent needed a headless Service using clusterIP: None, but Kubernetes does not allow this field to be changed on an existing Service.

Fix: introduce a controlled migration step into the deployment procedure. The automation checks the existing Service, identifies the legacy LoadBalancer, and deletes and recreates only that specific Service when necessary. It does not remove the namespace or the Portainer server.

apiVersion: v1
kind: Service
metadata:
  name: portainer-agent
spec:
  clusterIP: None
  publishNotReadyAddresses: true
  selector:
    app: portainer-agent

Recreating a Service can cause a brief interruption and should be performed as a controlled maintenance operation. Subsequent deployments reuse the headless Service.

4. DNS resolution failure during startup

After replacing the Service, the agent logged name-resolution failures:

lookup portainer-agent on <dns-server>:53:
no such host

The agent uses the Service name for member discovery. A headless Service may have no published endpoints while none of its pods are considered Ready. This creates a startup dependency loop: the agent needs DNS discovery to initialize, but the endpoints are not yet published because initialization has not completed.

Fix: set publishNotReadyAddresses: true to make the pods discoverable while they initialize. In this deployment, that change resolved the DNS startup failure.

5. Empty Edge credentials and the secondary port 80 error

Another deployment failed with an error that initially resembled a privileged-port restriction:

Unable to start Edge server
listen tcp 0.0.0.0:80: bind: permission denied

Inspecting the Kubernetes Secret showed that EDGE_KEY was zero bytes long. The agent had not received the association key generated by Portainer. In this configuration, it then attempted to start a server on port 80; the permission error was a secondary symptom of an incorrectly initialized agent.

The fix was to provide the correct, non-empty Edge key and verify that the expected startup sequence completed. Granting the container extra capabilities to bind privileged ports was unnecessary.

Credentials were centralized in HashiCorp Vault, with one record per environment containing:

Field Purpose
id Edge environment identifier
key Agent-to-Portainer association key

The deployment process reads both values from Vault, creates a ConfigMap for non-sensitive settings, and stores the key in a Kubernetes Secret. The Deployment refers to the key using secretKeyRef:

env:
  - name: EDGE_KEY
    valueFrom:
      secretKeyRef:
        name: portainer-agent-edge-key
        key: edge.key

Deployment now fails explicitly if either value is missing or empty. Credentials are not printed, committed to Git, or included in diagnostic logs. Their presence can be checked without revealing their contents.

6. An unwritable /tmp directory

With a valid key, the agent could start its API and establish some communication. However, internal features still failed:

failed to create healthy file:
open /tmp/.portainer-healthy: permission denied
failed to create alert rule evaluator:
open TSDB: mkdir /tmp/portainer-eval-tsdb-...:
permission denied

The container was running as an unprivileged user. The existing /data volume was writable, but the agent also needed /tmp for health files, metrics, and temporary storage. That directory was not writable under the OpenShift security context being enforced.

Fix: mount separate emptyDir volumes at /data and /tmp, while keeping an unprivileged security context:

spec:
  template:
    spec:
      securityContext:
        runAsNonRoot: true
        runAsUser: 1000
        runAsGroup: 1000
        fsGroup: 1000
      containers:
        - name: portainer-agent
          volumeMounts:
            - name: agent-data
              mountPath: /data
            - name: agent-tmp
              mountPath: /tmp
      volumes:
        - name: agent-data
          emptyDir: {}
        - name: agent-tmp
          emptyDir: {}

emptyDir volumes are ephemeral: their contents are lost when the pod is deleted. This matters if agent features requiring persistence are enabled. In this case, the mounts resolved the write failures without running the container as root.

7. Readiness: internal HTTP is not the Edge connection

The Deployment checked the agent's internal API via /ping on port 9001:

readinessProbe:
  httpGet:
    path: /ping
    port: 9001
    scheme: HTTP
  initialDelaySeconds: 5
  periodSeconds: 5
  timeoutSeconds: 3
  failureThreshold: 3

In the tested configuration, the internal API used HTTP. This is distinct from the external Edge Agent communications using HTTPS and the TCP 8000 tunnel.

Three states must be distinguished: Running means the main container process is executing; Ready means its configured local readiness probe passes; and Connected means Portainer receives the required traffic and can manage the environment. Neither Running nor Ready alone proves the Edge connection works.

8. Deployment automation: duplicated Makefile targets

The migration also exposed GNU Make warnings:

warning: overriding recipe for target 'configure'
warning: ignoring old recipe for target 'configure'

The Makefile contained duplicate recipes for targets including configure and deploy. GNU Make retains the last recipe for a target and overrides earlier ones. As a result, a deployment could follow an obsolete sequence that skipped loading the Edge credentials from Vault.

The Makefile was consolidated so that each operation had a single definition. The corrected flow prepares the namespace, retrieves credentials, validates the ConfigMap and Secret, migrates the Service when needed, applies the manifests, and verifies the rollout.

deploy: configure check-config

Error handling was also tightened so failures when reading Vault or applying Kubernetes resources could not be hidden by command chains or pipelines. Because the shared configuration was referenced through Git submodules, each cluster's pinned revision had to be checked: updating the shared repository does not automatically update every submodule.

9. Verifying the fix

Validation covered Kubernetes, the application, and network connectivity. First, the Deployment, pods, and logs were checked:

oc -n portainer get deployment portainer-agent
oc -n portainer get pods -l app=portainer-agent -o wide
oc -n portainer logs deployment/portainer-agent --tail=100

Successful initialization included messages similar to:

edge key loaded from options
Agent info updated successfully
evaluator successfully initialized

Checks confirmed that the agent was no longer repeatedly restarting and that persistent /tmp permission errors were gone. Outbound connectivity to the Portainer HTTPS endpoint and TCP port 8000 was tested from a network representative of the pods.

During rolling updates, member-discovery warnings may temporarily refer to old terminating pods. Those events must be distinguished from a persistent failure in the new pod. Likewise, a firewall or routing problem should not be declared the root cause without reproducible network evidence.

The decisive test was performed in the Portainer UI: the environments appeared available, their status updated, and Kubernetes resources could be accessed. A healthy Deployment is not a substitute for an end-to-end functional test in Portainer.

10. Outcome and technical lessons

Area Final configuration
Mode Edge Agent Standard
Communication Agent-initiated, HTTPS and TCP 8000
Local Service Headless; no per-agent LoadBalancer
Process identity Unprivileged user
Writable directories emptyDir volumes
Credential management HashiCorp Vault and Kubernetes Secret
Deployment Version-controlled manifests and consolidated Makefile
Verification Kubernetes health and functional Portainer connectivity

The migration did not fail for one reason. Filesystem permissions, Service design, DNS discovery, secret management, and automation defects overlapped. Resolving the incident required separating symptoms from root causes, correcting each component, and introducing repeatable checks.

Key lesson: repeatedly restarting a Deployment will not fix an empty Secret, an immutable field, an unwritable directory, or an incorrect deployment recipe. The diagnosis must end with a functional application-level check, not merely a pod reporting Running.

References