Running SaltStack Master in Kubernetes provides a modern, scalable approach to infrastructure management. This comprehensive guide walks you through deploying Salt Master on Kubernetes, from basics to production-ready configuration, including service exposure, firewall setup, and minion configuration.
Table of Contents
- Introduction
- Why Run Salt Master on Kubernetes?
- Prerequisites
- Understanding Salt Architecture
- Kubernetes Deployment Overview
- Step-by-Step Deployment
- Exposing Salt Master: NodePort vs LoadBalancer
- Firewall Configuration
- Setting Up Salt Minions
- GitFS Integration
- Common Pitfalls and Things to Watch Out For
- Troubleshooting
- Production Considerations
Introduction
SaltStack (now part of Broadcom’s portfolio) is a powerful configuration management and remote execution system. While traditionally deployed on VMs or bare metal, running Salt Master in Kubernetes offers several advantages including easier management, automatic recovery, and consistent deployment patterns.
According to recent data, while most implementations use Salt to deploy Kubernetes clusters, running Salt Master as a containerized workload is gaining traction for modern infrastructure management patterns.
Why Run Salt Master on Kubernetes?
Running Salt Master on Kubernetes provides several benefits:
- High Availability: Kubernetes automatically restarts failed pods
- Consistent Deployment: Infrastructure as code using YAML manifests
- Resource Management: CPU and memory limits/requests for predictable performance
- Persistent Storage: PersistentVolumes ensure minion keys and configurations survive pod restarts
- GitOps Integration: Configuration managed through version control
- Scalability: Easy to scale and manage alongside other workloads
- Portability: Run on any Kubernetes cluster (on-prem, cloud, or hybrid)
Prerequisites
Before starting, ensure you have:
Kubernetes Cluster (v1.19 or later)
- Running cluster with kubectl access
- Admin privileges to create namespaces and resources
Storage Provisioner
- Dynamic PersistentVolume provisioner or ability to create PVs manually
- At least 20GB of available storage
External Access Method
- LoadBalancer support (MetalLB, cloud provider LB) OR
- Ability to use NodePort with firewall rules
Network Access
- Ports 4505 and 4506 accessible from minions to master
- If using NodePort: Ports in 30000-32767 range accessible
Optional: Git Repository
- For storing Salt states and formulas (GitFS)
- SSH key or access token for private repositories
Understanding Salt Architecture
Salt uses a master-minion architecture with these key components:
Communication Ports
- Port 4505 (Publish Port): Master publishes commands to minions via ZeroMQ
- Port 4506 (Return Port): Minions return results and establish bi-directional communication
- Port 8000 (API Port): Optional Salt API for REST-based management
Communication Flow
1
2
3
4
5
6
7
8
9
10
11
12
| ┌──────────────────────────────────────────────────┐
│ Salt Master (Kubernetes) │
│ ┌────────────────────────────────────────────┐ │
│ │ Port 4505: Publish (Master → Minions) │ │
│ │ Port 4506: Return (Minions ↔ Master) │ │
│ │ Port 8000: REST API (Optional) │ │
│ └────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────┘
↓ ↓
┌───────────┐ ┌───────────┐
│ Minion 1 │ │ Minion 2 │
└───────────┘ └───────────┘
|
Important: Communication is always initiated by minions. The master never initiates connections to minions, meaning you only need to allow inbound connections to the master on ports 4505 and 4506. This is a critical security consideration that simplifies firewall configuration.
Kubernetes Deployment Overview
Our Salt Master deployment consists of these Kubernetes resources:
1
2
3
4
5
6
| ├── Namespace (salt-master)
├── ConfigMap (master configuration + GitFS settings)
├── Secret (API credentials, SSH keys)
├── PersistentVolumeClaims (data, keys, logs, config)
├── Deployment (Salt Master pod)
└── Service (LoadBalancer or NodePort)
|
The deployment uses the cdalvaro/docker-salt-master Docker image, which includes:
- Salt Master with all dependencies
- PyGit2 for GitFS support
- Salt API (CherryPy)
- Proper init system and logging
Step-by-Step Deployment
Step 1: Create Namespace
First, create a dedicated namespace for Salt Master:
1
2
3
4
5
6
7
8
| # namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
name: salt-master
labels:
name: salt-master
app: salt-master
|
Apply the namespace:
1
| kubectl apply -f namespace.yaml
|
Why a dedicated namespace? Isolating Salt Master in its own namespace provides better resource management, security boundaries, and easier cleanup if needed.
Salt Master requires persistent storage for:
- Keys: Minion authentication keys (critical - losing these requires re-keying all minions)
- Data: States, formulas, and pillar data
- Logs: Master logs for troubleshooting
- Config: Additional configuration files
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
| # pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: salt-master-data
namespace: salt-master
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
# Optionally specify storage class
# storageClassName: standard
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: salt-master-keys
namespace: salt-master
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: salt-master-logs
namespace: salt-master
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: salt-master-config
namespace: salt-master
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
|
Apply the PVCs:
1
| kubectl apply -f pvc.yaml
|
Storage Considerations:
- Use your cluster’s default storage class or specify one explicitly
- For production, use storage with backup capabilities
- Keys volume is most critical - consider using storage with replication
- Size data volume based on expected number of states and formulas
Step 3: Create ConfigMap for Salt Master Configuration
The ConfigMap contains the main Salt Master configuration, including GitFS settings:
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
87
88
89
90
91
92
93
94
95
96
97
98
99
| # configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: salt-master-config
namespace: salt-master
labels:
app: salt-master
data:
master: |
# Salt Master Configuration
# Interface to bind to (0.0.0.0 = all interfaces)
interface: 0.0.0.0
# Publish and request server ports
publish_port: 4505
ret_port: 4506
# User to run salt as (set by Docker image)
user: salt
# Auto-accept minion keys
# CRITICAL: Set to False in production!
# When False, you must manually accept each minion key
auto_accept: False
# Logging configuration
log_level: info
log_level_logfile: info
# File roots - where Salt looks for state files
file_roots:
base:
- /home/salt/data/srv/salt
- /home/salt/data/srv/formulas
# Pillar roots - where Salt looks for pillar data
pillar_roots:
base:
- /home/salt/data/srv/pillar
# Fileserver backend configuration
# Order matters: roots is checked first, then git
fileserver_backend:
- roots
- git
# GitFS provider (pygit2 is recommended and included in the image)
gitfs_provider: pygit2
# GitFS remotes - Git repositories containing Salt states
gitfs_remotes:
# Example with SSH authentication
- [email protected]:yourusername/salt-states.git:
- base: main
- root: salt # Subdirectory in repo containing states
- privkey: /etc/salt/ssh/deploy_key
- pubkey: /etc/salt/ssh/deploy_key.pub
# Example with HTTPS (for public repos)
# - https://github.com/yourusername/salt-states.git:
# - base: main
# - root: salt
# GitFS update interval (seconds)
# How often to check Git for changes
gitfs_update_interval: 300
# GitFS branches to make available
gitfs_branch_whitelist:
- main
- production
- staging
# GitFS SSL verification
gitfs_ssl_verify: True
# Worker threads - adjust based on number of minions
# Rule of thumb: 1 worker per 20-50 minions
worker_threads: 5
# Timeout settings (seconds)
timeout: 30
gather_job_timeout: 30
# Keep job results for 24 hours
keep_jobs: 24
# Enable job cache
job_cache: True
# Enable mine for service discovery
mine_enabled: True
# Optional: Reactor system for event-driven automation
# reactor:
# - 'salt/minion/*/start':
# - /home/salt/data/config/reactor/minion_start.sls
|
Configuration Explained:
- auto_accept: False: Critical security setting. In production, always manually accept minion keys to prevent unauthorized systems from connecting
- file_roots and pillar_roots: Define where Salt looks for states and pillar data. Multiple paths allow organizing code from different sources
- fileserver_backend: Order defines priority. “roots” checks local filesystem first, then “git” checks GitFS repositories
- worker_threads: Each worker handles minion connections. More workers = more concurrent minion management
- timeout settings: Adjust based on network latency and expected command execution times
Apply the ConfigMap:
1
| kubectl apply -f configmap.yaml
|
Step 4: Create Secrets
Create a secret for Salt API credentials:
1
2
3
4
5
6
7
8
9
10
| # secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: salt-api-credentials
namespace: salt-master
type: Opaque
stringData:
username: "salt_api"
password: "CHANGE_THIS_SECURE_PASSWORD"
|
IMPORTANT: Never commit secrets to Git! For production:
1
2
3
4
5
| # Create secret imperatively
kubectl create secret generic salt-api-credentials \
--from-literal=username=salt_api \
--from-literal=password=$(openssl rand -base64 32) \
-n salt-master
|
If using private Git repositories with SSH, create SSH key secret:
1
2
3
4
5
6
7
8
9
10
11
12
13
| # Generate SSH key for GitFS (if you don't have one)
ssh-keygen -t ed25519 -C "salt-gitfs" -f ./salt_deploy_key -N ""
# Add public key to your Git repository (GitHub/GitLab deploy keys)
# Create Kubernetes secret
kubectl create secret generic salt-ssh-keys \
--from-file=salt_deploy_key=./salt_deploy_key \
--from-file=salt_deploy_key.pub=./salt_deploy_key.pub \
-n salt-master
# Securely delete local keys after creation
shred -u ./salt_deploy_key ./salt_deploy_key.pub
|
Step 5: Create the Deployment
The Deployment defines the Salt Master pod specification:
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
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
| # deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: salt-master
namespace: salt-master
labels:
app: salt-master
spec:
replicas: 1
# Recreate strategy ensures only one master runs at a time
# RollingUpdate can cause split-brain with multiple masters
strategy:
type: Recreate
selector:
matchLabels:
app: salt-master
template:
metadata:
labels:
app: salt-master
spec:
# Init container to set up SSH keys with correct permissions
initContainers:
- name: setup-ssh-keys
image: busybox:latest
command:
- sh
- -c
- |
# Copy keys from read-only secret mount
cp /tmp/ssh-keys-ro/* /etc/salt/ssh/
# Set correct permissions (SSH is strict about this)
chmod 600 /etc/salt/ssh/salt_deploy_key
chmod 644 /etc/salt/ssh/salt_deploy_key.pub
# Set ownership to salt user (UID 1000)
chown -R 1000:1000 /etc/salt/ssh
volumeMounts:
- name: ssh-keys-ro
mountPath: /tmp/ssh-keys-ro
readOnly: true
- name: ssh-keys
mountPath: /etc/salt/ssh
containers:
- name: salt-master
image: ghcr.io/cdalvaro/docker-salt-master:latest
imagePullPolicy: Always
ports:
- name: publish
containerPort: 4505
protocol: TCP
- name: request
containerPort: 4506
protocol: TCP
- name: api
containerPort: 8000
protocol: TCP
env:
# Log level (debug, info, warning, error)
- name: SALT_LOG_LEVEL
value: "info"
# Enable Salt API for REST access
- name: SALT_API_ENABLED
value: "True"
# Salt API credentials from secret
- name: SALT_API_USER
valueFrom:
secretKeyRef:
name: salt-api-credentials
key: username
- name: SALT_API_USER_PASS
valueFrom:
secretKeyRef:
name: salt-api-credentials
key: password
# Auto-restart master when config changes
- name: SALT_RESTART_MASTER_ON_CONFIG_CHANGE
value: "True"
volumeMounts:
# Master configuration from ConfigMap
- name: master-config
mountPath: /home/salt/data/config/gitfs.conf
subPath: master
# Persistent volumes
- name: salt-data
mountPath: /home/salt/data/srv
- name: salt-keys
mountPath: /home/salt/data/keys
- name: salt-logs
mountPath: /home/salt/data/logs
- name: salt-config
mountPath: /home/salt/data/config
# SSH keys for GitFS
- name: ssh-keys
mountPath: /etc/salt/ssh
# Resource limits prevent one container from consuming all node resources
resources:
requests:
memory: "512Mi"
cpu: "250m"
limits:
memory: "2Gi"
cpu: "1000m"
# Liveness probe - restarts container if failing
livenessProbe:
tcpSocket:
port: 4505
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
# Readiness probe - removes from service if not ready
readinessProbe:
tcpSocket:
port: 4506
initialDelaySeconds: 15
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3
volumes:
- name: master-config
configMap:
name: salt-master-config
- name: salt-data
persistentVolumeClaim:
claimName: salt-master-data
- name: salt-keys
persistentVolumeClaim:
claimName: salt-master-keys
- name: salt-logs
persistentVolumeClaim:
claimName: salt-master-logs
- name: salt-config
persistentVolumeClaim:
claimName: salt-master-config
- name: ssh-keys-ro
secret:
secretName: salt-ssh-keys
defaultMode: 0400
- name: ssh-keys
emptyDir: {}
# Security context - run as salt user (UID 1000)
securityContext:
fsGroup: 1000
|
Deployment Configuration Explained:
- strategy: Recreate: Ensures only one Salt Master runs at a time. RollingUpdate could cause issues with minion connections
- initContainer: Sets up SSH keys with correct permissions before main container starts
- Resources: Adjust based on minion count. Start conservative and monitor actual usage
- Probes: Ensure pod is healthy and ready to accept connections
- fsGroup: 1000: Ensures persistent volumes are writable by salt user
Apply the deployment:
1
| kubectl apply -f deployment.yaml
|
Step 6: Verify Pod is Running
Check the pod status:
1
2
3
4
5
6
7
8
9
10
11
12
| # Check pod status
kubectl get pods -n salt-master
# Expected output:
# NAME READY STATUS RESTARTS AGE
# salt-master-xxxxxxxxxx-xxxxx 1/1 Running 0 2m
# View logs
kubectl logs -n salt-master -l app=salt-master -f
# Check Salt Master is ready
kubectl exec -n salt-master deployment/salt-master -- salt-run manage.status
|
Common startup issues:
- Pod in
Pending: Check PVC status with kubectl get pvc -n salt-master - Pod in
CrashLoopBackOff: Check logs for configuration errors - SSH key errors: Verify secret was created correctly
Exposing Salt Master: NodePort vs LoadBalancer
Salt minions need to reach the master on ports 4505 and 4506. Kubernetes provides multiple ways to expose services externally.
Understanding Service Types
According to Kubernetes best practices, there are several ways to expose services:
- ClusterIP: Internal only (default) - not suitable for external minions
- NodePort: Opens static ports (30000-32767) on all nodes
- LoadBalancer: Provisions external load balancer (cloud provider specific)
- Ingress: HTTP/HTTPS only - not suitable for Salt (requires TCP/UDP)
Option 1: LoadBalancer (Recommended for Production)
LoadBalancer is the standard way to expose services to the internet, providing a single IP address for all traffic.
Advantages:
- Simple configuration for minions (single IP)
- Automatic load balancing if running multiple masters
- Health checks handled by load balancer
- Standard ports (4505/4506) work without modification
Disadvantages:
- Requires cloud provider or MetalLB
- Cost (one load balancer per service)
- May not be available in all environments
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
| # service.yaml (LoadBalancer)
apiVersion: v1
kind: Service
metadata:
name: salt-master
namespace: salt-master
labels:
app: salt-master
spec:
type: LoadBalancer
selector:
app: salt-master
ports:
- name: publish
port: 4505
targetPort: 4505
protocol: TCP
- name: request
port: 4506
targetPort: 4506
protocol: TCP
# Optional: Request specific external IP (MetalLB)
# loadBalancerIP: 192.168.1.100
# Optional: Cloud provider specific annotations
# For AWS ELB
# annotations:
# service.beta.kubernetes.io/aws-load-balancer-type: "nlb"
# For GCP
# annotations:
# cloud.google.com/load-balancer-type: "External"
---
# Optional: Internal API service (cluster-only access)
apiVersion: v1
kind: Service
metadata:
name: salt-api
namespace: salt-master
spec:
type: ClusterIP
selector:
app: salt-master
ports:
- name: api
port: 8000
targetPort: 8000
protocol: TCP
|
Apply the service:
1
2
3
4
5
6
7
8
| kubectl apply -f service.yaml
# Get the external IP (may take a minute)
kubectl get svc salt-master -n salt-master -w
# Example output:
# NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S)
# salt-master LoadBalancer 10.96.100.50 52.49.97.175 4505:31505/TCP,4506:31506/TCP
|
Use this external IP in minion configuration as the master address.
Option 2: NodePort (For Environments Without LoadBalancer)
NodePort is suitable for small to medium deployments, development, or when LoadBalancer isn’t available.
Advantages:
- Works in any Kubernetes environment
- No external dependencies
- No additional cost
- Simple setup
Disadvantages:
- Must track which nodes have open ports
- Requires firewall rules on each node
- Port range limitation (30000-32767)
- Minions must use non-standard ports
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| # service-nodeport.yaml
apiVersion: v1
kind: Service
metadata:
name: salt-master
namespace: salt-master
labels:
app: salt-master
spec:
type: NodePort
selector:
app: salt-master
ports:
- name: publish
port: 4505
targetPort: 4505
protocol: TCP
nodePort: 30505 # Optional: specify exact port
- name: request
port: 4506
targetPort: 4506
protocol: TCP
nodePort: 30506 # Optional: specify exact port
|
Apply the NodePort service:
1
2
3
4
5
6
7
8
9
| kubectl apply -f service-nodeport.yaml
# Get node IPs and ports
kubectl get nodes -o wide
kubectl get svc salt-master -n salt-master
# Example output:
# NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S)
# salt-master NodePort 10.96.100.50 <none> 4505:30505/TCP,4506:30506/TCP
|
With NodePort, minions connect to:
- Master address: Any node’s external IP
- Publish port: 30505 (instead of 4505)
- Return port: 30506 (instead of 4506)
Example minion configuration with NodePort:
1
2
3
4
| # /etc/salt/minion
master: node-external-ip
publish_port: 30505
master_port: 30506
|
Hybrid Approach: External IP with NodePort
Some clusters support externalIPs, allowing you to use standard ports with NodePort:
1
2
3
4
5
6
7
8
| spec:
type: NodePort
externalIPs:
- 192.168.1.100 # Assign specific external IP
ports:
- name: publish
port: 4505
targetPort: 4505
|
Firewall Configuration
Proper firewall configuration is critical for Salt Master connectivity. Salt uses a master-initiated communication model where minions always initiate connections.
Understanding Salt’s Communication Model
According to Salt documentation, communication is always initiated by minions, never by the master. This means:
- Minions establish outbound connections to master ports 4505 and 4506
- Master never initiates inbound connections to minions
- Firewall rules only needed on the master (inbound on 4505/4506)
- No firewall rules needed on minions (outbound connections)
Salt uses AES-encrypted ZeroMQ connections over TCP:
- Port 4505: Publish port (master → minions broadcast)
- Port 4506: Return port (minions ↔ master bi-directional)
Kubernetes Node Firewall Rules
If using LoadBalancer: The load balancer typically handles firewall rules automatically.
If using NodePort: You must configure firewall rules on Kubernetes nodes.
For RHEL/CentOS/Rocky/AlmaLinux with firewalld
1
2
3
4
5
6
7
8
9
10
11
12
| # Allow NodePort range (if using NodePort service)
sudo firewall-cmd --permanent --add-port=30505/tcp
sudo firewall-cmd --permanent --add-port=30506/tcp
# Or allow entire NodePort range
sudo firewall-cmd --permanent --add-port=30000-32767/tcp
# Reload firewall
sudo firewall-cmd --reload
# Verify rules
sudo firewall-cmd --list-ports
|
For Ubuntu/Debian with UFW
1
2
3
4
5
6
7
8
9
| # Allow specific NodePorts
sudo ufw allow 30505/tcp
sudo ufw allow 30506/tcp
# Or allow entire NodePort range
sudo ufw allow 30000:32767/tcp
# Check status
sudo ufw status
|
For iptables
1
2
3
4
5
| # Allow NodePorts
sudo iptables -A INPUT -p tcp --dport 30505:30506 -j ACCEPT
# Save rules
sudo iptables-save > /etc/iptables/rules.v4
|
Cloud Provider Firewall Rules
AWS Security Groups
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
| # For LoadBalancer
aws ec2 authorize-security-group-ingress \
--group-id sg-xxxxxxxxx \
--protocol tcp \
--port 4505 \
--cidr 0.0.0.0/0
aws ec2 authorize-security-group-ingress \
--group-id sg-xxxxxxxxx \
--protocol tcp \
--port 4506 \
--cidr 0.0.0.0/0
# For NodePort
aws ec2 authorize-security-group-ingress \
--group-id sg-xxxxxxxxx \
--protocol tcp \
--port 30505 \
--cidr 0.0.0.0/0
aws ec2 authorize-security-group-ingress \
--group-id sg-xxxxxxxxx \
--protocol tcp \
--port 30506 \
--cidr 0.0.0.0/0
|
GCP Firewall Rules
1
2
3
4
5
6
7
8
9
10
11
| # For LoadBalancer
gcloud compute firewall-rules create salt-master \
--allow tcp:4505,tcp:4506 \
--source-ranges 0.0.0.0/0 \
--target-tags kubernetes-node
# For NodePort
gcloud compute firewall-rules create salt-master-nodeport \
--allow tcp:30505,tcp:30506 \
--source-ranges 0.0.0.0/0 \
--target-tags kubernetes-node
|
Azure Network Security Group
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| # For LoadBalancer
az network nsg rule create \
--resource-group myResourceGroup \
--nsg-name myNSG \
--name salt-master-publish \
--protocol tcp \
--priority 1000 \
--destination-port-range 4505
az network nsg rule create \
--resource-group myResourceGroup \
--nsg-name myNSG \
--name salt-master-request \
--protocol tcp \
--priority 1001 \
--destination-port-range 4506
|
Testing Connectivity
From a potential minion, test connectivity:
1
2
3
4
5
6
7
8
9
10
11
| # Test port 4505 (LoadBalancer)
telnet 52.49.97.175 4505
# or
nc -zv 52.49.97.175 4505
# Test port 4506
telnet 52.49.97.175 4506
# For NodePort
telnet node-ip 30505
telnet node-ip 30506
|
Successful connection shows port is open. If connection fails, check:
- Service has external IP assigned
- Firewall rules are in place
- Network path allows traffic
- No intermediate firewalls blocking ports
Setting Up Salt Minions
Once Salt Master is running and accessible, you can add minions (managed nodes).
Minion Installation
RHEL/CentOS/Rocky/AlmaLinux
1
2
3
4
5
6
| # Add Salt repository
curl -fsSL https://github.com/saltstack/salt-install-guide/releases/latest/download/salt.repo \
| sudo tee /etc/yum.repos.d/salt.repo
# Install Salt Minion
sudo dnf install salt-minion -y
|
Ubuntu/Debian
1
2
3
4
5
6
7
8
9
10
11
| # Add Salt repository GPG key
curl -fsSL https://packages.broadcom.com/artifactory/api/gpg/key/public \
| sudo gpg --dearmor -o /usr/share/keyrings/salt-archive-keyring.gpg
# Add repository
echo "deb [signed-by=/usr/share/keyrings/salt-archive-keyring.gpg] https://packages.broadcom.com/artifactory/saltproject-deb stable main" \
| sudo tee /etc/apt/sources.list.d/salt.list
# Update and install
sudo apt update
sudo apt install salt-minion -y
|
Minion Configuration
Basic Configuration
Edit /etc/salt/minion:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| # Salt Master address
master: 52.49.97.175 # Use your LoadBalancer IP or node IP
# Unique minion ID (defaults to hostname)
id: web-server-01
# Logging
log_level: info
# Enable mine for service discovery
mine_enabled: True
mine_functions:
network.ip_addrs: []
grains.items: []
# Auto-restart minion on config changes
autorestart_wait: 10
|
With NodePort (Non-Standard Ports)
If using NodePort, specify the custom ports:
1
2
3
4
5
6
7
| master: node-external-ip
# Use NodePort port numbers
publish_port: 30505
master_port: 30506
id: web-server-01
|
Advanced Configuration with Grains
Grains are static data about minions used for targeting:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| master: 52.49.97.175
id: prod-web-01
# Custom grains for targeting
grains:
environment: production
roles:
- webserver
- nginx
datacenter: aws-us-east-1
team: platform
# Backup mode
backup_mode: minion
# Optional: Schedule automatic state application
schedule:
highstate:
function: state.apply
minutes: 30
maxrunning: 1
|
Grains Usage: Target minions with salt -G 'environment:production' test.ping
Start and Enable Minion
1
2
3
4
5
6
7
8
9
10
11
| # Enable on boot
sudo systemctl enable salt-minion
# Start service
sudo systemctl start salt-minion
# Check status
sudo systemctl status salt-minion
# View logs (watch for connection to master)
sudo journalctl -fu salt-minion
|
Expected log output:
1
2
3
| salt-minion[1234]: [INFO] Setting up the Salt Minion "web-server-01"
salt-minion[1234]: [INFO] Connecting to master 52.49.97.175:4506
salt-minion[1234]: [INFO] Authentication credentials requested
|
Accept Minion Keys on Master
Minions generate a key pair and send their public key to the master for authentication.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| # List all keys (from your workstation with kubectl access)
kubectl exec -n salt-master deployment/salt-master -- salt-key -L
# Output shows:
# Accepted Keys:
# Denied Keys:
# Unaccepted Keys:
# web-server-01
# Rejected Keys:
# Accept a specific minion
kubectl exec -n salt-master deployment/salt-master -- salt-key -a web-server-01
# Accept all pending keys (use carefully!)
kubectl exec -n salt-master deployment/salt-master -- salt-key -A -y
# Verify acceptance
kubectl exec -n salt-master deployment/salt-master -- salt-key -L
|
Security Note: Only accept keys from known minions. Verify the minion ID matches the expected system.
Test Minion Connectivity
1
2
3
4
5
6
7
8
9
10
11
12
| # Ping all minions
kubectl exec -n salt-master deployment/salt-master -- salt '*' test.ping
# Expected output:
# web-server-01:
# True
# Get minion information
kubectl exec -n salt-master deployment/salt-master -- salt 'web-server-01' grains.items
# Run a command
kubectl exec -n salt-master deployment/salt-master -- salt 'web-server-01' cmd.run 'uptime'
|
Common Minion Setup Issues
Minion can’t connect to master
- Verify master IP:
ping 52.49.97.175 - Test ports:
telnet 52.49.97.175 4505 - Check firewall rules on minion (outbound should be allowed)
- Check master logs:
kubectl logs -n salt-master -l app=salt-master | grep web-server-01
Key not appearing on master
- Check minion logs:
sudo journalctl -fu salt-minion - Verify minion can reach master
- Restart minion:
sudo systemctl restart salt-minion
Minion accepted but not responding
- Check minion service:
sudo systemctl status salt-minion - Restart minion service
- Check for key mismatch: Delete keys on both sides and regenerate
GitFS Integration
GitFS allows Salt Master to pull states and formulas directly from Git repositories, enabling Infrastructure as Code workflows.
Benefits of GitFS
- Version Control: All state changes tracked in Git
- Code Review: Pull requests for infrastructure changes
- Branching: Test changes in branches before production
- Rollback: Easy revert to previous versions
- Collaboration: Multiple team members can contribute
- Consistency: Single source of truth for configurations
Git Repository Structure
Create a repository with this structure:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| salt-states/
├── salt/ # Salt states directory
│ ├── top.sls # Top file (maps states to minions)
│ ├── common/
│ │ └── init.sls # Common state for all minions
│ ├── webserver/
│ │ ├── init.sls # Webserver installation
│ │ ├── nginx.sls # Nginx configuration
│ │ └── files/
│ │ └── nginx.conf # Template files
│ ├── database/
│ │ └── init.sls # Database setup
│ └── firewall/
│ └── init.sls # Firewall configuration
└── pillar/ # Pillar data directory (optional)
├── top.sls
└── common.sls
|
Example State Files
salt/top.sls (maps states to minions):
1
2
3
4
5
6
7
8
9
10
11
12
13
| base:
"*":
- common
- firewall
"roles:webserver":
- match: grain
- webserver
- webserver.nginx
"roles:database":
- match: grain
- database
|
salt/common/init.sls:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| # Common packages for all servers
common_packages:
pkg.installed:
- pkgs:
- vim
- git
- htop
- curl
- wget
# Ensure timezone is set
timezone_config:
timezone.system:
- name: UTC
- utc: True
# Create admin group
admin_group:
group.present:
- name: admins
- gid: 5000
|
salt/webserver/init.sls:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| # Install web server
nginx:
pkg.installed:
- name: nginx
service.running:
- name: nginx
- enable: True
- require:
- pkg: nginx
- watch:
- file: /etc/nginx/nginx.conf
# Deploy nginx configuration
/etc/nginx/nginx.conf:
file.managed:
- source: salt://webserver/files/nginx.conf
- user: root
- group: root
- mode: 644
- require:
- pkg: nginx
|
salt/firewall/init.sls:
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
| # Install firewalld
firewalld:
pkg.installed:
- name: firewalld
service.running:
- name: firewalld
- enable: True
- require:
- pkg: firewalld
# Allow SSH
allow_ssh:
firewalld.present:
- name: public
- services:
- ssh
- require:
- service: firewalld
# Allow HTTP/HTTPS for web servers
{% if 'webserver' in grains['roles'] %}
allow_web:
firewalld.present:
- name: public
- services:
- http
- https
- require:
- service: firewalld
{% endif %}
|
Configuring GitFS in Salt Master
Update your ConfigMap with GitFS settings:
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
| # In configmap.yaml
gitfs_remotes:
# Public repository (HTTPS)
- https://github.com/yourusername/salt-states.git:
- base: main
- root: salt # Subdirectory containing states
# Private repository (SSH with deploy key)
- [email protected]:yourusername/private-salt-states.git:
- base: main
- root: salt
- privkey: /etc/salt/ssh/deploy_key
- pubkey: /etc/salt/ssh/deploy_key.pub
# Multiple branches
- https://github.com/yourusername/salt-states.git:
- base: production
- root: salt
- https://github.com/yourusername/salt-states.git:
- base: staging
- root: salt
# Specify which branches to make available
gitfs_branch_whitelist:
- main
- production
- staging
# Update interval (seconds)
gitfs_update_interval: 300
|
Apply updated configuration:
1
2
3
4
| kubectl apply -f configmap.yaml
# Restart deployment to load new config
kubectl rollout restart deployment/salt-master -n salt-master
|
Managing GitFS
1
2
3
4
5
6
7
8
9
10
11
12
| # Update fileserver (pull latest from Git)
kubectl exec -n salt-master deployment/salt-master -- salt-run fileserver.update
# List all available files
kubectl exec -n salt-master deployment/salt-master -- salt-run fileserver.file_list
# View specific file contents
kubectl exec -n salt-master deployment/salt-master -- \
salt-run fileserver.read salt://webserver/init.sls
# Check GitFS backends
kubectl exec -n salt-master deployment/salt-master -- salt-run fileserver.backends
|
Testing States
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| # Test state application (dry run)
kubectl exec -n salt-master deployment/salt-master -- \
salt 'web-server-01' state.apply webserver test=True
# Apply state
kubectl exec -n salt-master deployment/salt-master -- \
salt 'web-server-01' state.apply webserver
# Apply all states from top.sls
kubectl exec -n salt-master deployment/salt-master -- \
salt '*' state.highstate
# Apply to specific grain
kubectl exec -n salt-master deployment/salt-master -- \
salt -G 'roles:webserver' state.apply webserver
|
Common Pitfalls and Things to Watch Out For
1. SSH Key Permissions for GitFS
Problem: GitFS can’t authenticate with Git repository
Solution: SSH keys must have correct permissions (600 for private key). The init container handles this:
1
2
3
4
5
6
7
8
9
10
| initContainers:
- name: setup-ssh-keys
command:
- sh
- -c
- |
cp /tmp/ssh-keys-ro/* /etc/salt/ssh/
chmod 600 /etc/salt/ssh/salt_deploy_key
chmod 644 /etc/salt/ssh/salt_deploy_key.pub
chown -R 1000:1000 /etc/salt/ssh
|
2. Auto-Accept in Production
Problem: auto_accept: True allows any system to connect
Solution: Always set auto_accept: False in production. Manually review and accept each minion key:
1
2
| # In ConfigMap
auto_accept: False
|
3. Losing Minion Keys
Problem: If the keys PVC is deleted, all minions lose authentication
Solution:
- Ensure keys PVC has appropriate backup strategy
- Consider using storage with replication
- Document key recovery process
4. Pod Restart Causes Minion Disconnections
Problem: When Salt Master pod restarts, minions temporarily lose connection
Solution:
- Minions automatically reconnect (default retry)
- Use liveness/readiness probes to ensure pod is ready
- Consider StatefulSet for more stable network identity (advanced)
5. NodePort Port Conflicts
Problem: NodePort might conflict with other services
Solution:
- Explicitly specify nodePort values in service
- Document which NodePorts are allocated
- Use LoadBalancer in production if possible
6. Resource Limits Too Low
Problem: Salt Master OOMKilled or CPU throttled with many minions
Solution: Scale resources based on minion count:
- Start: 512Mi memory, 250m CPU
- Medium (50-100 minions): 2Gi memory, 1000m CPU
- Large (500+ minions): 4Gi+ memory, 2000m+ CPU
- Monitor actual usage and adjust
7. GitFS Not Updating
Problem: Changes pushed to Git don’t appear on master
Solution:
1
2
3
4
5
6
7
8
9
| # Manually trigger update
kubectl exec -n salt-master deployment/salt-master -- salt-run fileserver.update
# Check update interval in config
gitfs_update_interval: 300 # 5 minutes
# Verify Git authentication
kubectl exec -n salt-master deployment/salt-master -- \
ssh -T [email protected]
|
8. Timeout Issues with Slow Minions
Problem: Commands timeout on slow networks or busy minions
Solution: Increase timeout values in ConfigMap:
1
2
| timeout: 30 # Increase to 60 or more
gather_job_timeout: 30 # Increase proportionally
|
9. Wrong GitFS Root Path
Problem: States not found even though they’re in repository
Solution: Verify root path matches repository structure:
1
2
3
4
| gitfs_remotes:
- [email protected]:user/repo.git:
- base: main
- root: salt # Must match directory in repo
|
If states are at repository root, use root: . or omit root parameter.
10. Firewall Rules Not Applied
Problem: Minions can’t connect despite service being exposed
Solution:
- Check cloud provider firewall/security groups
- Verify node-level firewall (firewalld/ufw)
- Test connectivity with telnet/nc from minion
- Check for intermediate firewalls
11. ConfigMap Changes Not Applied
Problem: Updated ConfigMap doesn’t take effect
Solution:
1
2
3
4
5
| # ConfigMaps mounted as volumes don't trigger pod restart
# Must manually restart deployment
kubectl rollout restart deployment/salt-master -n salt-master
# Or set SALT_RESTART_MASTER_ON_CONFIG_CHANGE=True in deployment
|
12. Storage Class Issues
Problem: PVCs stuck in Pending state
Solution:
1
2
3
4
5
6
7
8
9
| # Check PVC status
kubectl get pvc -n salt-master
# Check events
kubectl describe pvc salt-master-keys -n salt-master
# Specify storage class explicitly
spec:
storageClassName: standard # or your cluster's default
|
Troubleshooting
Debugging Pod Issues
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| # Check pod status
kubectl get pods -n salt-master
# Describe pod (shows events)
kubectl describe pod -n salt-master -l app=salt-master
# View logs
kubectl logs -n salt-master -l app=salt-master -f
# Get shell in container
kubectl exec -n salt-master -it deployment/salt-master -- /bin/bash
# Inside container:
ps aux | grep salt-master
cat /etc/salt/master
salt-key -L
salt '*' test.ping
|
Debugging Minion Connection Issues
From master:
1
2
3
4
5
6
7
8
| # Check if minion key arrived
kubectl exec -n salt-master deployment/salt-master -- salt-key -L
# Check master logs for minion authentication
kubectl logs -n salt-master -l app=salt-master | grep minion-id
# Try pinging specific minion
kubectl exec -n salt-master deployment/salt-master -- salt 'minion-id' test.ping -v
|
From minion:
1
2
3
4
5
6
7
8
9
10
11
12
13
| # Check minion service
sudo systemctl status salt-minion
# View minion logs
sudo journalctl -fu salt-minion
# Test master connectivity
telnet master-ip 4505
telnet master-ip 4506
# Run minion in debug mode
sudo systemctl stop salt-minion
sudo salt-minion -l debug
|
Debugging GitFS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| # Check GitFS backends
kubectl exec -n salt-master deployment/salt-master -- salt-run fileserver.backends
# Update fileserver
kubectl exec -n salt-master deployment/salt-master -- salt-run fileserver.update
# List files in fileserver
kubectl exec -n salt-master deployment/salt-master -- salt-run fileserver.file_list
# Check master logs for Git errors
kubectl logs -n salt-master -l app=salt-master | grep -i git
# Test SSH key from container
kubectl exec -n salt-master -it deployment/salt-master -- ssh -T [email protected]
|
Debugging Service Exposure
1
2
3
4
5
6
7
8
9
10
11
12
| # Check service status
kubectl get svc -n salt-master
# Describe service
kubectl describe svc salt-master -n salt-master
# For LoadBalancer, check external IP assignment
kubectl get svc salt-master -n salt-master -w
# Test from outside cluster
telnet external-ip 4505
nc -zv external-ip 4505
|
Common Error Messages
“Authentication failed” on minion:
- Minion key not accepted on master
- Key mismatch (delete both sides and regenerate)
- Master IP incorrect in minion config
“Connection refused” on minion:
- Firewall blocking ports
- Service not exposed correctly
- Master not running
“gitfs: Failed to authenticate with remote”:
- SSH key permissions incorrect
- SSH key not mounted correctly
- Git repository URL incorrect
- Deploy key not added to repository
“Permission denied” in pod:
- fsGroup not set correctly
- PVC permissions issue
- Check securityContext in deployment
Production Considerations
High Availability
For production, consider running multiple Salt Masters:
1
2
3
4
5
6
| spec:
replicas: 2 # Run 2 masters
# Use StatefulSet instead of Deployment
# Provides stable network identity
|
Configure minions with multiple masters:
1
2
3
4
5
6
| # /etc/salt/minion
master:
- master1-ip
- master2-ip
master_type: failover # Try next master if first fails
|
Monitoring and Alerts
Integrate with monitoring systems:
1
2
3
4
5
6
7
8
9
10
11
12
13
| # Prometheus ServiceMonitor
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: salt-master
namespace: salt-master
spec:
selector:
matchLabels:
app: salt-master
endpoints:
- port: api
path: /metrics
|
Monitor these metrics:
- Pod CPU and memory usage
- PVC usage
- Minion connection count
- Job execution times
- Failed state applications
Security Hardening
- Network Policies: Restrict access to Salt Master
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: salt-master-policy
namespace: salt-master
spec:
podSelector:
matchLabels:
app: salt-master
policyTypes:
- Ingress
ingress:
- from:
- ipBlock:
cidr: 10.0.0.0/8 # Internal network only
ports:
- protocol: TCP
port: 4505
- protocol: TCP
port: 4506
|
Pod Security Standards: Run with restricted permissions
Secrets Management: Use external secrets management (Vault, Sealed Secrets)
Audit Logging: Enable Salt audit logging
1
2
3
4
| # In ConfigMap
publish_session: 86400
keep_jobs: 24
job_cache: True
|
Backup Strategy
Automate backups of critical data:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| #!/bin/bash
# backup-salt-master.sh
NAMESPACE=salt-master
BACKUP_DIR=/backups/salt-master/$(date +%Y%m%d-%H%M%S)
mkdir -p $BACKUP_DIR
# Backup minion keys
POD=$(kubectl get pod -n $NAMESPACE -l app=salt-master -o jsonpath='{.items[0].metadata.name}')
kubectl exec -n $NAMESPACE $POD -- \
tar czf /tmp/keys-backup.tar.gz -C /home/salt/data/keys .
kubectl cp $NAMESPACE/$POD:/tmp/keys-backup.tar.gz \
$BACKUP_DIR/keys-backup.tar.gz
# Backup configurations
kubectl get configmap salt-master-config -n $NAMESPACE -o yaml \
> $BACKUP_DIR/configmap.yaml
kubectl get secret salt-api-credentials -n $NAMESPACE -o yaml \
> $BACKUP_DIR/secret.yaml
echo "Backup completed: $BACKUP_DIR"
|
Run via CronJob:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| apiVersion: batch/v1
kind: CronJob
metadata:
name: salt-master-backup
namespace: salt-master
spec:
schedule: "0 2 * * *" # Daily at 2 AM
jobTemplate:
spec:
template:
spec:
containers:
- name: backup
image: bitnami/kubectl:latest
command: ["/scripts/backup-salt-master.sh"]
|
Version Pinning
Never use latest tag in production:
1
| image: ghcr.io/cdalvaro/docker-salt-master:3007.1 # Specific version
|
Resource Scaling Guidelines
| Minions | Memory | CPU | Workers |
|---|
| 1-50 | 512Mi-1Gi | 250m-500m | 5 |
| 50-200 | 1Gi-2Gi | 500m-1000m | 10 |
| 200-500 | 2Gi-4Gi | 1000m-2000m | 20 |
| 500+ | 4Gi+ | 2000m+ | 30+ |
Monitor actual usage and adjust accordingly.
Conclusion
Running Salt Master on Kubernetes provides a modern, scalable approach to configuration management. Key takeaways:
- Service Exposure: Use LoadBalancer for production, NodePort for development
- Firewall Rules: Only master needs inbound rules on 4505/4506
- Persistent Storage: Critical for minion keys - ensure backups
- GitFS: Enables Infrastructure as Code workflows
- Security: Never auto-accept keys in production
- Monitoring: Track resource usage and scale accordingly
- Testing: Always test state changes with
test=True first
With this setup, you have a production-ready Salt Master running on Kubernetes, capable of managing infrastructure at scale while maintaining the flexibility and reliability of modern cloud-native applications.
Resources and References
This guide was created for citizix.com based on practical experience deploying Salt Master on Kubernetes. For questions or improvements, please reach out.