Building Secure Self-Hosted Connectors with TLS, Traefik, and Kubernetes
Self-hosted workloads need a reliable way to connect back to the control plane. For Shiper, that connection carries an SSH-based connector protocol, so it needs to be reachable over TCP while still being protected by TLS and routed to the correct tenant.
This post describes how the connector works in production and how users connect a server to Shiper.
Using a self-hosted connector
Create a self-hosted instance in the Shiper dashboard. The dashboard provides an API key and the Docker command for that instance. Run the command on the server where deployments should run:
docker run -d \--name shiper-connector \--restart always \-e API_KEY=YOUR_KEY \-v /var/run/docker.sock:/var/run/docker.sock \shiperapp/connector:latest
The agent connects outbound to Shiper, so the server does not need an inbound firewall rule. Once the instance appears as Connected in the dashboard, select it under Use Self-Hosted Instance when configuring a project. Builds continue to run in Shiper's infrastructure; the resulting application container is deployed through the connector to your server.
The connection model
Each connector runs as a Kubernetes workload and listens on port 3454. The public hostname is unique to the connector:
<connector-id>.connector.shiper.app
The client connects to that hostname using TLS. Traefik terminates neither TLS nor SSH; it reads the TLS SNI value and forwards the encrypted stream to the matching connector service. The connector server then performs the TLS and SSH handshake itself.
Docker agent││ TLS + SNI: <connector-id>.connector.shiper.app▼Load balancer│ TCP 3454▼Traefik connector entrypoint│ IngressRouteTCP + TLS passthrough▼Connector service│▼Connector server
Using SNI gives every connector an isolated route without requiring a separate public port for each instance.
TLS inside the connector
The connector server loads its certificate and private key from a Kubernetes Secret mounted at /certs:
/certs/tls.crt/certs/tls.key
The server wraps the incoming connection with TLS before handing it to the SSH server. The client follows the same order in reverse: it establishes TLS with the connector hostname and then opens the SSH session over that encrypted connection.
This detail matters. A plain TCP client, an SSH client that skips TLS, or a client using the wrong SNI hostname will not complete the handshake. Depending on where the connection stops, the symptom can be as vague as TLS handshake error: EOF.
Issuing certificates with cert-manager
Every connector receives a certificate whose DNS name matches its route:
apiVersion: cert-manager.io/v1kind: Certificatemetadata:name: connector-examplespec:secretName: connector-exampleissuerRef:name: connector-tlskind: ClusterIssuerdnsNames:- example.connector.shiper.app
cert-manager stores the resulting certificate and private key in the Secret mounted by the connector. The certificate authority can be selected according to the environment and trust requirements.
Routing encrypted TCP with Traefik
The connector route is an IngressRouteTCP with TLS passthrough enabled:
apiVersion: traefik.io/v1alpha1kind: IngressRouteTCPmetadata:name: connector-examplespec:entryPoints:- connectorroutes:- match: HostSNI(`example.connector.shiper.app`)services:- name: connector-example-svcport: 3454tls:passthrough: true
Traefik needs a TCP entrypoint for port 3454, and its Kubernetes CRD provider must be enabled. Without the CRD provider, the resource can exist successfully in Kubernetes while Traefik silently ignores it.
In production, the load balancer exposes the connector entrypoint on port 3454 and forwards it to Traefik.
DNS is part of the routing configuration
The wildcard record must point connector hostnames at the production load balancer:
*.connector.shiper.app -> production load balancer
The load balancer must preserve TCP traffic and forward port 3454 to Traefik. HTTP routing rules do not apply to this connection. The hostname in all three places must agree:
- The connector's certificate DNS name.
- The
HostSNIvalue inIngressRouteTCP. - The hostname used by the client for TLS and SSH.
A mismatch in any one of them can produce a connection that reaches the load balancer but never reaches the intended connector.
Before and after
The connector used to connect directly to a NodePort on a single Kubernetes node. That made the first deployment easy to understand, but it bypassed the production load balancer and Traefik's routing layer.
Before
- The connector endpoint pointed directly to a NodePort on one Kubernetes node.
- Traffic bypassed the production load balancer and Traefik.
- The endpoint was tied to a specific node rather than the connector's routed hostname.
- Scaling or replacing that node required endpoint and routing changes.
- Node upgrades and patches could interrupt the connector and introduce downtime for self-hosted instances.
- Connector access was managed separately from the platform's public routing layer.
After
- The connector is reached through the production load balancer on port
3454. - Traefik routes the encrypted connection using SNI.
- Each connector has its own
<connector-id>.connector.shiper.apphostname. - Node identity is no longer part of the public connector endpoint.
- Node upgrades and patches can happen behind the load balancer without changing the public connector address.
- TLS and certificate provisioning are integrated into the connector deployment.
For customers, the important change is that the server remains in their environment while the connection management moves into Shiper. The connector identifies the correct server using its instance-specific hostname, and the encrypted channel is carried through the production load balancer and Traefik without exposing application containers directly.
The result for customers
After the agent reports Connected, the workflow is the same as a regular Shiper deployment:
- Create or open a project.
- Enable Use Self-Hosted Instance.
- Select the connected server.
- Deploy as usual.
Shiper builds the application, sends the deployment through the secure connector, and starts the resulting container on the selected server. Domains, environment variables, and project settings continue to be managed through Shiper, while the workload runs on infrastructure controlled by the customer.
The final design keeps the connector protocol behind a TLS-protected TCP entrypoint while preserving tenant-level routing through SNI. Customers only need Docker, the command generated by the dashboard, and outbound network access from their server.