Post

Home-Assistant in Kubernetes [Second Part - Bastion SSH]

Build your SSH container por CI

Home-Assistant in Kubernetes [Second Part - Bastion SSH]

Build your own Bastion-SSH for Deployments from GitHub [Second Part - Bastion ]

In this post, I’ll show you how I automated the deployment of my Home Assistant configuration using GitHub Actions, a custom Docker container with SSH and Fail2Ban, and a shared persistent volume. This setup allows me to test amd update my Home Assistant configuration simply by pushing changes to a GitHub repository.


🚀 What We’re Building

The idea is to create a secure and automated way to update the configuration files of a running Home Assistant instance. Here’s the high-level workflow:

  1. A GitHub Action is triggered on push.
  2. It connects via SSH to a secure Docker container.
  3. This SSH container shares a volume with the Home Assistant container.
  4. It pulls the latest configuration using git.
  5. It restarts the Home Assistant container so the changes take effect.

By separating the deployment logic into a dedicated container, we keep things clean and secure.

[🔐 Important – KubeConfig!]
You’ll also need a Kubeconfig file with restricted permissions to allow container restarts. Here you have how to do it Limited Kubeconfig


🧱 The Architecture

We’ll be using two Docker containers:

  • SSH + Fail2Ban Container: Acts as a secure entry point for GitHub Actions.
  • Home Assistant Container: Uses the same shared volume for configuration files.

Both containers mount a ReadWriteMany PVC (Persistent Volume Claim), so any changes made by the SSH container are immediately visible to Home Assistant.

GitHub Actions ──SSH (w/ Fail2Ban)──┐ │ Shared Volume (PVC) │ Home Assistant Container

Alt text


🔐 SSH Authentication with GitHub Actions

The connection from GitHub Actions to the SSH container is done using public key authentication. Here’s the setup:

  1. Generate an SSH key pair.
    1
    
    ssh-keygen -t rsa -b 4096 -C "youremail.com"
    
  2. Add the public key to ~/.ssh/authorized_keys in the container.
  3. For the future we will store the private key in GitHub-Repository as a secret (e.g., SSH_PRIVATE_KEY).

This way, GitHub Actions can securely log in without exposing any password.


🐳 1 - Build the SSH + Fail2Ban Docker Container

We’ll build a custom Docker container that runs:

  • An OpenSSH server for secure access.
  • Fail2Ban to block IPs after several failed login attempts.
  • Logging for SSH connections.

Here’s the Dockerfile template (fill it in as needed):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
FROM ubuntu:20.04

ENV DEBIAN_FRONTEND=noninteractive

# Install
RUN apt-get update && apt-get upgrade -y && \
    apt-get install -y \
    nano \
    openssh-server \
    fail2ban \
    curl \
    software-properties-common \
    jq \
    git \
    iptables \
    moreutils 
 
# Kubectl
RUN curl -LO "https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl" && \
    chmod +x ./kubectl && mv ./kubectl /usr/local/bin/kubectl
# Create Directories
RUN mkdir -p /var/run/sshd /var/run/fail2ban /var/log && \
    touch /var/log/sshd.log /var/log/sshd-raw.log /var/log/fail2ban.log

# No root only keys
RUN sed -i 's/^#\?PermitRootLogin.*/PermitRootLogin prohibit-password/' /etc/ssh/sshd_config && \
    sed -i 's/^#\?PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config && \
    sed -i 's/^#\?PubkeyAuthentication.*/PubkeyAuthentication yes/' /etc/ssh/sshd_config && \
    sed -i 's/^#\?AuthorizedKeysFile.*/AuthorizedKeysFile .ssh\/authorized_keys/' /etc/ssh/sshd_config && \
    echo "LogLevel VERBOSE" >> /etc/ssh/sshd_config

# Fail2BAN
RUN echo "[sshd]\n\
enabled = true\n\
filter = sshd-no-ts\n\
port = ssh\n\
logpath = /var/log/sshd.log\n\
backend = polling\n\
maxretry = 3\n\
bantime = 60000\n\
findtime = 600" > /etc/fail2ban/jail.local

# Fail2BAN Regex
RUN printf "%s\n" "[Definition]" \
"failregex = ^.*Failed password for .* from <HOST> port \d+ ssh2" \
"            ^.*Failed publickey for .* from <HOST> port \d+ ssh2" \
"            ^.*ROOT LOGIN REFUSED FROM <HOST> port \d+" \
"            ^.*Invalid user .* from <HOST> port \d+" \
"            ^.*Connection closed by invalid user .* <HOST> port \d+" \
"            ^.*Connection closed by authenticating user .* <HOST> port \d+ \[preauth\]" \
"ignoreregex =" \
> /etc/fail2ban/filter.d/sshd-no-ts.conf

# Entrypoint.sh
RUN printf '%s\n' '#!/bin/bash' \
'echo "==> Starting..."' \
'mkdir -p /var/run/fail2ban' \
'chmod 755 /var/run/fail2ban' \
'touch /var/log/sshd.log /var/log/sshd-raw.log /var/log/fail2ban.log' \
'chmod 644 /var/log/*.log' \
'' \
'echo "==> Starting Fail2Ban..."' \
'fail2ban-client -x start' \
'sleep 1' \
'fail2ban-client status sshd || echo "⚠️  Jail sshd not exist"' \
'' \
'echo "==> Logging..."' \
'tail -F /var/log/sshd-raw.log | ts "%Y-%m-%d %H:%M:%S" >> /var/log/sshd.log &' \
'tail -n0 -F /var/log/sshd.log /var/log/fail2ban.log &' \
'' \
'echo "==> Starting SSHD..."' \
'/usr/sbin/sshd -D -E /var/log/sshd-raw.log' \
> /entrypoint.sh && chmod +x /entrypoint.sh

# PORT
EXPOSE 22

# Entrypoint
CMD ["/entrypoint.sh"]

🐳 2 - Run the SSH + Fail2Ban Docker Container in Kubernetes

Run this Container in the same namespace where Home Assistant lives, and use tree PVs:

  • PV for SSH configuration (bastion-data-pvc)
  • PV Shared with Home Assistant (home-assistant-pvc)
  • PV for Kubeconfig mounted as secret (kubeconfig-secret)

Example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: bastion-data-pvc
  namespace: home-assistant
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi
  storageClassName: longhorn
---  
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: bastion-ssh
  namespace: home-assistant
spec:
  replicas: 1
  serviceName: "bastion-ssh"
  selector:
    matchLabels:
      app: bastion-ssh
  template:
    metadata:
      labels:
        app: bastion-ssh 
    spec:
      containers:
      - name: bastion-ssh
        image: user/your-repo:tag
        command: ["/entrypoint.sh"]
        ports:
        - containerPort: 22
        resources:
          requests:
            memory: "100Mi"
            cpu: "0.1"
          limits: 
            memory: "150Mi" 
            cpu: "0.1"
        env:
          - name: NOTVISIBLE
            value: "in users profile"
          - name: PATH
            value: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
        securityContext:
          runAsUser: 0
          runAsGroup: 0        
          privileged: true            
        volumeMounts:
        - name: pv-home-assistant
          mountPath: /config          
        - name: pv-bastion
          mountPath: /root/.ssh
        - name: kubeconfig-volume
          mountPath: /kubeconfig
          readOnly: true   
      volumes:
      - name: pv-home-assistant
        persistentVolumeClaim:
          claimName: home-assistant-pvc
      - name: pv-bastion
        persistentVolumeClaim:
          claimName: bastion-data-pvc
      - name: kubeconfig-volume
        secret:
          secretName: kubeconfig-secret
---          
apiVersion: v1
kind: Service
metadata:
  name: bastion-ssh
  namespace: home-assistant
spec:
  type: LoadBalancer
  loadBalancerIP: 192.168.2.4
  selector:
    app: bastion-ssh
  ports:
    - name: ssh
      protocol: TCP
      port: 8752
      targetPort: 22
  externalTrafficPolicy: Local              

Deploy your Bastion SSH Kubernetes

[ Important -> SSH-Router ] You must forward in your router from Public Port (8752) to your Container-Port (loadBalancerIP:22)

🚀 With this the Bastion-SSH will be running in your Kubernetes cluster!

👉 Next Part: [CI Deployment–>Soon]

This post is licensed under CC BY 4.0 by the author.