---
title: "Kubernetes Traefik Public and Private Ingress Setup | Hostman Docs"
description: "Set up dual Traefik instances in Kubernetes to handle public and internal services securely. Discover how to configure IngressClasses, deploy Helm releases, and route traffic using public and private load balancers."
---

> For the complete documentation index for AI agents, see [llms.txt](https://hostman.com/llms.txt).

When running a Kubernetes cluster that hosts both internet-facing and internal services, the cleanest approach is to split incoming traffic at the ingress controller level. You can do this by deploying two separate Traefik instances: one that handles public ingress, and one that handles private ingress.

This guide walks through that setup. You will deploy two Helm releases of Traefik with different `IngressClasses`, then route traffic to public and internal services accordingly.

## How It Works

Two independent Traefik instances run in the cluster:

-   `traefik-public` processes only Ingress resources with `ingressClassName: public`
-   `traefik-private` processes only Ingress resources with `ingressClassName: private`

Each instance creates its own `LoadBalancer` service with different parameters:

-   The public Traefik receives an external IP and accepts traffic from the internet
-   The private Traefik creates an internal load balancer with no public IP, accessible only within the private network

This lets you use a single cluster for both external and internal applications without mixing their entry points.

## Prerequisites

Before you begin, make sure you have:

-   [helm](https://hostman.com/docs/kubernetes/helm/) installed
-   A running Kubernetes cluster
-   Access to the cluster's [private network](https://hostman.com/docs/vpc/) for testing the private ingress (for example, via a separate cloud server in the same network)

## Configure Traefik

Both Traefik instances are deployed from the same Helm chart, but with separate values files.

### Public Traefik configuration

Create a file named `traefik-public-values.yaml` with the following content:

```yaml
fullnameOverride: traefik-public
 
ingressClass:
  enabled: true
  isDefaultClass: false
  name: public
 
service:
  enabled: true
  type: LoadBalancer
 
providers:
  kubernetesCRD:
    enabled: true
    allowCrossNamespace: false
    ingressClass: public
  kubernetesIngress:
    enabled: true
    ingressClass: public
    publishedService:
      enabled: true
```

This configures the public Traefik instance to handle ingress resources of the `public` class and create a standard `LoadBalancer` with an external IP.

Key settings to note:

-   `ingressClass.name: public` sets the class name that public Ingress resources will reference
-   `providers.kubernetesCRD.ingressClass` and `providers.kubernetesIngress.ingressClass` restrict Traefik to resources of its own class
-   `publishedService.enabled: true` enables correct publishing of the ingress service address

### Private Traefik configuration

Create a file named `traefik-private-values.yaml` with the following content:

```yaml
fullnameOverride: traefik-private
 
ingressClass:
  enabled: true
  isDefaultClass: false
  name: private
 
service:
  enabled: true
  type: LoadBalancer
  annotations:
    k8s.hostman.com/attached-loadbalancer-no-external-ip: "true"
 
providers:
  kubernetesCRD:
    enabled: true
    allowCrossNamespace: false
    ingressClass: private
  kubernetesIngress:
    enabled: true
    ingressClass: private
    publishedService:
      enabled: true
```

This configures the private Traefik instance to handle ingress resources of the private class and create an internal load balancer with no public IP.

The key parameter here is `k8s.hostman.com/attached-loadbalancer-no-external-ip: "true"`, which tells the platform to provision an internal load balancer without a public IP address.

## Install Both Traefik Instances

Add the Traefik Helm repository:

```shell
helm repo add traefik https://helm.traefik.io/traefik
helm repo update
```

Deploy the public instance:

```shell
helm install traefik-public traefik/traefik \
  -n traefik-public --create-namespace \
  -f traefik-public-values.yaml
```

Deploy the private instance:

```shell
helm install traefik-public traefik/traefik \
  -n traefik-public --create-namespace \
  -f traefik-public-values.yaml
```

Once both are installed, check that the `LoadBalancer` services have been created:

```shell
kubectl get svc -n traefik-public
kubectl get svc -n traefik-private
```

> [!NOTE]
> Load balancer provisioning can take up to 10 minutes. The `EXTERNAL-IP` column may show `pending` while the resource is being created.

Expected behavior differs between the two:

-   `traefik-public` should receive an external IP address
-   `traefik-private` will not receive an external IP, since it uses an internal load balancer

You can also check the status of both load balancers from the Hostman dashboard. Wait until both have finished provisioning before proceeding.

At this stage, also confirm that both ingress classes are registered in the cluster:

```shell
kubectl get ingressclass
```

The output should list both `public` and `private`.

## Routing Traffic to Public and Private Services

Once both Traefik instances are running, routing is as simple as setting the appropriate `ingressClassName` in each Ingress manifest.

For a public service:

```yaml
spec:
  ingressClassName: public
```

For a private service:

```yaml
spec:
  ingressClassName: private
```

All traffic is then routed through the corresponding ingress controller automatically.

## Practical Example

The following manifests demonstrate the full setup. The example includes:

-   Two public services: `service1` and `service2`
-   One private service: `service3`
-   Two Ingress resources with different `IngressClasses`

#### ConfigMap with test HTML pages

Create `config-map.yaml`:

```yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: service-config
  namespace: ingress-example
data:
  service1.html: |
    <html>
      <head><title>Service 1</title></head>
      <body><h1>Welcome to Service 1!</h1></body>
    </html>
  service2.html: |
    <html>
      <head><title>Service 2</title></head>
      <body><h1>Welcome to Service 2!</h1></body>
    </html>
  service3.html: |
    <html>
      <head><title>Service 3</title></head>
      <body><h1>Welcome to Service 3!</h1></body>
    </html>
```

#### Public service1

Create `service1-deployment.yaml`:

```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: service1
  namespace: ingress-example
spec:
  replicas: 2
  selector:
    matchLabels:
      app: service1
  template:
    metadata:
      labels:
        app: service1
    spec:
      containers:
        - name: nginx
          image: nginx:latest
          ports:
            - containerPort: 80
          volumeMounts:
            - name: config-volume
              mountPath: /usr/share/nginx/html
      volumes:
        - name: config-volume
          configMap:
            name: service-config
            items:
              - key: service1.html
                path: service1.html
---
apiVersion: v1
kind: Service
metadata:
  name: service1
  namespace: ingress-example
spec:
  selector:
    app: service1
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80
```

#### Public service2

Create `service2-deployment.yaml`:

```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: service2
  namespace: ingress-example
spec:
  replicas: 2
  selector:
    matchLabels:
      app: service2
  template:
    metadata:
      labels:
        app: service2
    spec:
      containers:
        - name: nginx
          image: nginx:latest
          ports:
            - containerPort: 80
          volumeMounts:
            - name: config-volume
              mountPath: /usr/share/nginx/html
      volumes:
        - name: config-volume
          configMap:
            name: service-config
            items:
              - key: service2.html
                path: service2.html
---
apiVersion: v1
kind: Service
metadata:
  name: service2
  namespace: ingress-example
spec:
  selector:
    app: service2
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80
```

#### Private service3

Create `service3-deployment.yaml`:

```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: service3
  namespace: ingress-example
spec:
  replicas: 2
  selector:
    matchLabels:
      app: service3
  template:
    metadata:
      labels:
        app: service3
    spec:
      containers:
        - name: nginx
          image: nginx:latest
          ports:
            - containerPort: 80
          volumeMounts:
            - name: config-volume
              mountPath: /usr/share/nginx/html
      volumes:
        - name: config-volume
          configMap:
            name: service-config
            items:
              - key: service3.html
                path: index.html
---
apiVersion: v1
kind: Service
metadata:
  name: service3
  namespace: ingress-example
spec:
  selector:
    app: service3
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80
```

#### Public ingress

Create `ingress-public.yaml`:

```shell
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: example-public-ingress
  namespace: ingress-example
spec:
  ingressClassName: public
  rules:
    - host: ingress1.example.com
      http:
        paths:
          - path: /service1
            pathType: Prefix
            backend:
              service:
                name: service1
                port:
                  number: 80
          - path: /service2
            pathType: Prefix
            backend:
              service:
                name: service2
                port:
                  number: 80
```

Note that `spec.ingressClassName` is set to `public`, which means this Ingress will be handled by the public ingress controller.

#### Private ingress

Create `ingress-private.yaml`:

```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: example-private-ingress
  namespace: ingress-example
spec:
  ingressClassName: private
  rules:
    - host: ingress2.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: service3
                port:
                  number: 80
```

Here `spec.ingressClassName` is set to `private`, so this Ingress will be handled by the private ingress controller.

#### Apply the manifests

Create the `ingress-example` namespace:

```shell
kubectl create namespace ingress-example
```

Apply all manifests:

```shell
kubectl apply -f config-map.yaml
kubectl apply -f service1-deployment.yaml
kubectl apply -f service2-deployment.yaml
kubectl apply -f service3-deployment.yaml
kubectl apply -f ingress-public.yaml
kubectl apply -f ingress-private.yaml
```

Verify that deployments, services, and ingress resources were created:

```shell
kubectl get deploy,svc,ingress -n ingress-example
```

You should see three services, three deployments, and two ingress resources. If anything is missing, the corresponding manifest either was not applied or contains an error.

With this setup:

-   Requests to `ingress1.example.com/service1...` are routed to `service1`
-   Requests to `ingress1.example.com/service2...` are routed to `service2`
-   Requests to `ingress2.example.com/` are routed to `service3`, accessible only from within the private network via the internal Traefik load balancer

## Verify the Setup

Before sending test requests, check that each ingress has the correct class assigned:

```shell
kubectl describe ingress example-public-ingress -n ingress-example
kubectl describe ingress example-private-ingress -n ingress-example
```

Confirm that `example-public-ingress` shows `Ingress Class: public` and `example-private-ingress` shows `Ingress Class: private`. If the class does not match, Traefik will not process that resource.

> [!NOTE]
> The example uses `ingress1.example.com` and `ingress2.example.com` as placeholder hostnames. You can use them with `curl --resolve` without configuring real DNS. If you want to open the public ingress in a browser, replace `ingress1.example.com` with a real domain and configure a DNS record pointing to the public load balancer IP.

Once the public load balancer has an external IP, test the routes:

```shell
curl http://ingress1.example.com/service1.html \
  --resolve ingress1.example.com:80:PUBLIC_LB_EXTERNAL_IP
 
curl http://ingress1.example.com/service2.html \
  --resolve ingress1.example.com:80:PUBLIC_LB_EXTERNAL_IP
```

Where `PUBLIC_LB_EXTERNAL_IP` is the external IP assigned to the public load balancer.

If you receive a 404, double-check that you are using the hostname `ingress1.example.com` and the paths `/service1` or `/service2`.

To test the private service, run the following from a machine inside the same private network as the cluster:

```shell
curl http://ingress2.example.com/ \
  --resolve ingress2.example.com:80:PRIVATE_LB_IP
```

Where `PRIVATE_LB_IP` is the private IP of the internal load balancer.

You should receive an HTML page served by `service3`.

If the private route does not respond, confirm that the request is coming from within the private network. External internet traffic cannot reach this ingress, even if the cluster resources are configured correctly.
