Here’s how to turn a Raspberry Pi into a touchscreen kiosk running Ubuntu and Home Assistant.
This guide uses:
- Wayland
- Sway +
- Firefox (Optional: Chromium)
Result:
- Boots straight into fullscreen Home Assistant
- Auto-starts on power-up, no login required
- Turns the display off after idle, wakes instantly on touch
- Supports a handy three-finger tap to reload when Wi-Fi or HA hangs
Hardware used:
Flash & Prepare Ubuntu
- Flash Ubuntu Server for Raspberry Pi (26.04 LTS or newer) using Raspberry Pi Images.
- Configure your
user-data and network-config before first boot (see Configure Cloud Init). - Boot the Pi and let
cloud-init configure everything automatically.
user-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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
| #cloud-config
# https://cloudinit.readthedocs.io/
# Hostname
hostname: ha-kiosk
manage_etc_hosts: true
# Default user with sudo rights
users:
- name: ubuntu
primary_group: ubuntu
groups: sudo,adm,users
shell: /bin/bash
sudo: ["ALL=(ALL) ALL"]
lock_passwd: false
ssh-authorized-keys:
- ssh-ed25519 AAAA...your-ssh-key
# Lock out password-based SSH login for security
ssh_pwauth: false
# Expire default password (forces password change if ever used)
chpasswd:
expire: true
users:
- name: ubuntu
password: ubuntu
type: text
## Fix Wi-Fi Stability (Broadcom driver)
#write_files:
# - path: /etc/modprobe.d/brcmfmac.conf
# content: |
# options cfg80211 ieee80211_regdom=DE
# options brcmfmac power_save=0
# options brcmfmac feature_disable=0x82000
# Update apt database and upgrade packages on first boot
package_update: true
package_upgrade: true
## Install additional packages on first boot
#packages:
#- ...
|
network-config
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| network:
version: 2
ethernets:
eth0:
optional: true
dhcp4: true
dhcp6: false
wifis:
wlan0:
optional: false
dhcp4: true
dhcp6: false
access-points:
"your-ssid":
password: "your-wifi-password" # pragma: allowlist secret
regulatory-domain: DE
|
config.txt
1
2
3
4
5
6
7
8
9
10
11
12
13
| [pi4]
dtoverlay=vc4-fkms-v3d
max_framebuffers=2
gpu_mem=512
gpu_freq=600
arm_freq=2000
arm_boost=1
#enable_uart=1
disable_overscan=1
disable_splash=1
boot_delay=0
temp_limit=80
hdmi_force_hotplug=1
|
cmdline.txt
console=serial0,115200 zswap.enabled=1 zswap.compressor=zstd multipath=off dwc_otg.lpm_enable=0 console=tty1 root=LABEL=writable rootfstype=ext4 panic=10 rootwait fixrtc quiet splash
Pre Setup
1
2
3
4
5
6
7
| # Update System
sudo apt update
sudo apt upgrade -y
# Set locale and timezone for correct time and language
sudo timedatectl set-timezone Europe/Berlin
sudo update-locale LANG=en_US.UTF-8
|
Secure Ubuntu (Optional)
To harden your Pi, you can run my Ansible security playbooks. This step is optional but recommended.
1
2
3
4
5
6
| git clone --recursive https://github.com/MVladislav/ansible-env-setup.git
cd ansible-env-setup
python3 -m venv .venv
source .venv/bin/activate
python3 -m pip install -r requirements.txt
ansible-galaxy collection install --upgrade -r requirements.yml
|
Create Inventory
Replace:
<SSH_PRIV_KEY_FILENAME>: Path to your private ssh key e.g. ~/.ssh/example<SSH_PUB_KEY_STRING>: Add the public key content e.g. cat ~/.ssh/example.pub<PI_IP_ADDRESS>: IP address from your Pi<PI_USER_PASSWORD>: Can be created with sudo apt install whois && mkpasswd --method=sha-512
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
| tee ./inventory/inventory-pi.yml <<'EOF'
all:
hosts:
server:
cis_ubuntu2404_rule_1_1_1_9: false
cis_ubuntu2404_rule_1_3_1_2: false # no grub available
cis_ubuntu2404_rule_3_1_1: false # no grub available
cis_ubuntu2404_rule_6_2_1_3: false # no grub available
cis_ubuntu2404_rule_6_2_1_4: false # no grub available
## ANSIBLE DEFAULTS
## _______________________
ansible_ssh_private_key_file: ~/.ssh/<SSH_PRIV_KEY_FILENAME>
pl_a_cis_setup_ssh_pub_key: "<SSH_PUB_KEY_STRING>"
ansible_user: ubuntu
ansible_host: <PI_IP_ADDRESS>
ansible_port: 22
ansible_host_hostname: "kiosk"
pl_a_service_name: "server_"
# NOTE: change to your required dns and ntp server, or change in playbooks
pl_a_host_default_ntp: time.cloudflare.com
pl_a_host_fallback_ntp: ntp.ubuntu.com
## ANSIBLE CLIENT ACCESS SETUP
## _______________________
pl_a_user_setup: true
pl_a_user_config:
is_update: true
password: "<PI_USER_PASSWORD>"
group: sudo,adm,dip,lxd,plugdev
system: false
append: true
add_ssh_key: true
## UPDATER
## _______________________
updater_time_timezone: Europe/Berlin
updater_ntp_server: ""
updater_ntp_fallback_server: ""
## NETPLAN
## _______________________
pl_a_netplan_setup: false
netplan_remove_existing: false
## SSH
## NOTE: ssh not needed, is general setup by CIS, here only default conf to setup per user defaults
## _______________________
pl_a_ssh_setup: true
security_ssh_only_client_setup: true
## UFW
## _______________________
## NOTE: ufw is general setup by CIS, use here to set logging level and ssh if not setup by cis
pl_a_ufw_setup_enable: true
pl_a_ufw_logging_level: "off" # off | low | medium
## SERVICES (disable to not setup)
## _______________________
pl_a_snmp_setup: false
pl_a_nullmailer_setup: false
pl_a_postfix_setup: false
pl_a_hardening_scan_setup: false
## CIS
## _______________________
pl_a_cis_setup: true
pl_a_cis_setup_aide: false
pl_a_cis_ipv6_required: true
## HARDENING SECURITY
## _______________________
pl_a_auditd_setup: true
pl_a_fail2ban_setup: true
## ---------------------------------------------------------------------------
vars:
ansible_python_interpreter: /usr/bin/python3
children:
servers:
hosts:
server:
EOF
|
Create PlayBook SSH-Default
Replace:
<SSH_PUB_KEY_STRING>: Add the public key content e.g. cat ~/.ssh/example.pub<PI_IP_ADDRESS>: IP address from your Pi
1
2
3
4
5
6
7
| tee ./playbooks/vars/default.yml <<'EOF'
keystodeploy:
# id_ed25519
"ubuntu-<PI_IP_ADDRESS>":
- sshkey: <SSH_PUB_KEY_STRING>
EOF
|
Run PlayBook
1
| ansible-playbook ./playbooks/playbook-sec-short.yml --ask-become-pass -k -i ./inventory/inventory-pi.yml
|
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
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
| sudo tee /etc/sysctl.d/99-sysctl.conf <<'EOF'
#
# /etc/sysctl.conf - Configuration file for setting system variables
# See /etc/sysctl.d/ for additional system variables.
# See sysctl.conf (5) for information.
#
# To apply changes: `sudo sysctl -p --system`
#
###################################################################
# ==> Kernel Parameters
# Control kernel logging to console (severity levels).
# See: https://en.wikipedia.org/wiki/Syslog#Severity_levels
# - CUR: Current message level (default: 3, "error").
# - DEF: Default level for messages without a specified level.
# - MIN: Minimum CUR level allowed.
# - BTDEF: Boot-time default for CUR.
# | | | CUR | DEF | MIN | BTDEF |
# | :-- | :------------ | :-- | :-- | :-- | :---- |
# | 0 | emergency | x | x | x | x |
# | 1 | alert | x | x | x | x |
# | 2 | critical | x | x | | x |
# | 3 | error | x | x | | x |
# | 4 | warning | | x | | |
# | 5 | notice | | | | |
# | 6 | informational | | | | |
# | 7 | debug | | | | |
kernel.printk=3 4 1 7
# Enable Address Space Layout Randomization (ASLR) for process memory.
# Enhances security by making it more difficult for attackers to predict memory addresses.
kernel.randomize_va_space=2
# Restrict access to dmesg for non-root users, preventing potential leakage of sensitive system information.
kernel.dmesg_restrict=1
# Controls access to kernel pointer addresses in /proc files.
# Restricting this prevents unauthorized users from reading kernel addresses.
kernel.kptr_restrict=1
# Disable core dumps for setuid programs to prevent sensitive data leaks.
fs.suid_dumpable=0
# Max inotify instances and watches per user (for applications that require more file monitoring).
fs.inotify.max_user_instances=8192
fs.inotify.max_user_watches=524288
# Restrict ptrace() debugging to parent processes only.
# Prevents exploitation of ptrace by malicious processes.
kernel.yama.ptrace_scope=1
###################################################################
# ==> Magic SysRq Key Configuration
# Enable SysRq key functions selectively.
# See https://www.kernel.org/doc/html/latest/admin-guide/sysrq.html
# - 0: Disable completely.
# - 1: Enable all.
# - 176: Allow only reboot, remount, kill, sync, etc.
kernel.sysrq=176
#kernel.sysrq=438
###################################################################
# ==> Virtual Memory
# Prevent null-pointer dereference attacks by restricting minimum address mappable via mmap().
vm.mmap_min_addr=65536
# Memory overcommit handling:
# - 0: Default overcommit handling.
# - 1: Always overcommit.
# - 2: No overcommit beyond set ratio.
vm.overcommit_memory=0
# In case overcommit ratio needs to be manually set (in percent).
#vm.overcommit_ratio=100
# Set swappiness value. Lower values reduce swap usage and prefer keeping data in RAM.
vm.swappiness=1
# Transparent Huge Pages (THP) can be enabled for memory allocation efficiency if necessary.
#vm.nr_hugepages=128
#vm.nr_hugepages=2048
#vm.nr_hugepages_mempolicy=1
# Controls when dirty data (modified pages) is written to disk.
# See https://lonesysadmin.net/2013/12/22/better-linux-disk-caching-performance-vm-dirty_ratio/
# The `dirty_background_ratio` defines the threshold when background processes start flushing dirty pages.
# The `dirty_ratio` is the maximum percentage of RAM that can be "dirty" before the system forces a write.
# Example:
# For a system with 64GB of RAM:
# - dirty_background_ratio=5: Around 3.2GB will start flushing.
# - dirty_ratio=10: Around 6.4GB can be dirty before a forced write.
# Adjust these values depending on system load and disk performance requirements.
#vm.dirty_background_ratio=5
#vm.dirty_ratio=10
###################################################################
# ==> Networking (Functional Parameters)
# Disable IPv6 if not required.
# Recommended for systems without IPv6 dependencies for security and performance reasons.
net.ipv6.conf.all.disable_ipv6=1
net.ipv6.conf.default.disable_ipv6=1
net.ipv6.conf.lo.disable_ipv6=1
# Prevent sending ICMP redirects.
# Improves security for non-router devices to avoid man-in-the-middle attacks.
net.ipv4.conf.all.send_redirects=0
net.ipv4.conf.default.send_redirects=0
# Disable IP forwarding to prevent the system from routing packets, increasing security.
net.ipv4.ip_forward=0
net.ipv4.conf.all.forwarding=0
net.ipv4.conf.default.forwarding=0
net.ipv6.conf.all.forwarding=0
net.ipv6.conf.default.forwarding=0
# Disable source routing to protect against spoofing attacks, which can be used to bypass security mechanisms.
net.ipv4.conf.all.accept_source_route=0
net.ipv4.conf.default.accept_source_route=0
net.ipv6.conf.all.accept_source_route=0
net.ipv6.conf.default.accept_source_route=0
# Prevent acceptance of ICMP redirects, mitigating spoofing attacks.
net.ipv4.conf.all.accept_redirects=0
net.ipv4.conf.default.accept_redirects=0
net.ipv6.conf.all.accept_redirects=0
net.ipv6.conf.default.accept_redirects=0
# Do not accept ICMP redirects only for gateways listed in our default.
net.ipv4.conf.all.secure_redirects=0
net.ipv4.conf.default.secure_redirects=0
# Log Martian Packets (better to have enabled for security, but can cause log spam).
net.ipv4.conf.all.log_martians=0
net.ipv4.conf.default.log_martians=0
# Ignore broadcast ICMP pings and erroneous error responses to enhance security.
net.ipv4.icmp_echo_ignore_broadcasts=1
net.ipv4.icmp_ignore_bogus_error_responses=1
net.ipv4.icmp_echo_ignore_all=0
# Enable source address validation to prevent spoofing.
net.ipv4.conf.all.rp_filter=1
net.ipv4.conf.default.rp_filter=1
net.ipv4.conf.lo.rp_filter=0
# Enable TCP SYN cookies to mitigate SYN flood attacks.
net.ipv4.tcp_syncookies=1
# Enable TCP Selective Acknowledgements (SACK), improving throughput and robustness.
net.ipv4.tcp_sack=1
# Disable Path MTU Discovery to reduce the risk of attackers manipulating MTU values.
net.ipv4.ip_no_pmtu_disc=1
# Disable TCP timestamps to improve security against timing-based attacks. (RFC1323/RFC7323)
net.ipv4.tcp_timestamps=0
# Protect Against TCP Time-Wait to mitigate DoS attack attempts.
net.ipv4.tcp_rfc1337=1
# Enable temporary IPv6 addresses for better privacy (anonymizing address information).
net.ipv6.conf.all.use_tempaddr=2
net.ipv6.conf.default.use_tempaddr=2
# Enable source address verification for IPv6.
# This makes it more difficult for an attacker to spoof their IP address
net.ipv6.conf.all.accept_ra=0
net.ipv6.conf.default.accept_ra=0
# Clear routing cache to ensure routing decisions are made based on up-to-date information.
net.ipv4.route.flush=1
net.ipv6.route.flush=1
# Enable TCP Fast Open for faster connections, enhancing performance for both clients and servers.
# - 0: Disable TCP Fast Open (default if not explicitly set).
# - 1: Enable TCP Fast Open for outgoing connections (clients).
# - 2: Enable TCP Fast Open for incoming connections (servers).
# - 3: Enable TCP Fast Open for both outgoing and incoming connections.
net.ipv4.tcp_fastopen=3
# Set congestion control algorithm for better throughput and latency.
net.ipv4.tcp_congestion_control=bbr
#net.ipv4.tcp_congestion_control=htcp
#net.ipv4.tcp_congestion_control=cubic
# Default queuing discipline (reduces latency under load).
net.core.default_qdisc=fq_codel
#net.core.default_qdisc=fq
# Enable TCP window scaling for larger buffers, useful in high-bandwidth or high-latency networks.
# Increase Linux autotuning TCP buffer limit to 128MB (64MB).
net.ipv4.tcp_window_scaling=3
# Enable MTU Probing (recommended for hosts with jumbo frames enabled).
#net.ipv4.tcp_mtu_probing=1
# Enable auto-tuning of the receive buffer size for better performance in high-throughput networks.
net.ipv4.tcp_moderate_rcvbuf=1
# Don't cache the slow start threshold from previous connections for more consistent performance.
net.ipv4.tcp_no_metrics_save=1
# Enable low-latency TCP connections for time-sensitive applications.
net.ipv4.tcp_low_latency=1
# Disable netfilter on bridge devices for improved performance in virtualized environments.
net.bridge.bridge-nf-call-iptables=0
net.bridge.bridge-nf-call-arptables=0
net.bridge.bridge-nf-call-ip6tables=0
# IPv6 Privacy Extensions (RFC 4941)
# ---
# IPv6 typically uses a device's MAC address when choosing an IPv6 address
# to use in autoconfiguration. Privacy extensions allow using a randomly
# generated IPv6 address, which increases privacy.
#
# Acceptable values:
# 0 - don’t use privacy extensions.
# 1 - generate privacy addresses
# 2 - prefer privacy addresses and use them over the normal addresses.
net.ipv6.conf.all.use_tempaddr=2
net.ipv6.conf.default.use_tempaddr=2
# Set preferred lifetime to 1 hour (time before a new address is preferred)
# Backuped optional values: 86400 (24h)
net.ipv6.conf.all.temp_prefered_lft=3600
net.ipv6.conf.default.temp_prefered_lft=3600
# Set valid lifetime to 2 hours (time before the old address is invalidated)
# Backuped optional values: 604800 (168h)
net.ipv6.conf.all.temp_valid_lft=7200
net.ipv6.conf.default.temp_valid_lft=7200
###################################################################
# ==> Networking (Performance Parameters)
# Increase maximum receive/send socket buffer sizes for handling large data streams.
# Backuped optional values: 212992 | 67108864 | 134217728
net.core.rmem_max=134217728
net.core.wmem_max=134217728
# Increase input queue size for better handling of high traffic volumes.
# Backuped optional values: 1000 | 3000
#net.core.netdev_max_backlog=1000
# Increase maximum number of pending connections to support high traffic loads.
# Backuped optional values: 4096 | 65535
#net.core.somaxconn=4096
# Number of flow entries for Receive Packet Steering (RPS).
# Backuped optional values: 0 | 32768
#net.core.rps_sock_flow_entries=32768
# Optimize TCP buffers for high throughput connections (low, pressure, high).
# Backuped optional values: 4096 131072 6291456 | 4096 87380 67108864 | 4096 87380 134217728
#net.ipv4.tcp_rmem=4096 87380 134217728
# Backuped optional values: 4096 16384 4194304 | 4096 87380 67108864 | 4096 87380 134217728
#net.ipv4.tcp_wmem=4096 87380 134217728
# Maximum number of queued SYN requests, higher values prevent SYN flood attacks.
# Backuped optional values: 512 | 4096
#net.ipv4.tcp_max_syn_backlog=4096
# Define memory pressure thresholds for TCP memory management (low, pressure, high).
# Backuped optional values: 93222 124299 186444 | 4194304 4194304 4194304 | 8388608 12582912 16777216
#net.ipv4.tcp_mem=8388608 12582912 16777216
###################################################################
# ==> Filesystem Parameters
# Increase maximum number of open file descriptors system-wide, supporting applications with many files open.
# Backuped optional values: 2097152 | 262144 | 4194304 | 9223372036854775807
fs.file-max=9223372036854775807
# Increase maximum virtual memory map count for applications using large amounts of virtual memory.
vm.max_map_count=1048576
###################################################################
# Notes and Optional Settings
# ###################################################################
# Enabling TCP Timestamps and PMTU Discovery can improve certain network performance metrics
# but may expose systems to specific types of attacks:
#net.ipv4.tcp_timestamps=1
#net.ipv4.ip_no_pmtu_disc=0
#net.ipv4.tcp_fastopen=1
# ###################################################################
# If IPv6 is required, enable it with the following settings
#net.ipv6.conf.all.disable_ipv6=0
#net.ipv6.conf.default.disable_ipv6=0
#net.ipv6.conf.lo.disable_ipv6=0
EOF
sudo sysctl --system
|
Create a Kiosk User
1
2
3
4
| sudo adduser --disabled-password --comment "" kiosk
sudo usermod -aG video,render,audio,input,tty kiosk
sudo loginctl enable-linger kiosk
sudo systemctl restart systemd-logind
|
Auto-Login & Auto-Start Kiosk
Enable autologin for the kiosk user:
1
2
3
4
5
6
7
| sudo mkdir -p /etc/systemd/system/getty@tty1.service.d/
sudo tee /etc/systemd/system/getty@tty1.service.d/override.conf <<'EOF'
[Service]
ExecStart=
ExecStart=-/sbin/agetty --autologin kiosk --noclear %I $TERM
EOF
|
Systemd User Service for Sway
1
| sudo apt install -y sway swayidle wayland-utils libva-drm2
|
1
2
3
4
5
6
7
8
9
10
| sudo tee /etc/udev/rules.d/70-kiosk-tty.rules <<'EOF'
# Kiosk user TTY access
SUBSYSTEM=="tty", KERNEL=="tty[0-9]*", GROUP="tty", MODE="0660"
SUBSYSTEM=="vc", KERNEL=="vcs[0-9]*", GROUP="tty", MODE="0660"
SUBSYSTEM=="drm", KERNEL=="card[0-9]*", GROUP="video", MODE="0660"
EOF
sudo udevadm control --reload-rules
sudo udevadm trigger
|
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
| sudo -u kiosk mkdir -p /home/kiosk/.config/systemd/user
sudo -u kiosk tee /home/kiosk/.config/systemd/user/sway.service <<'EOF'
[Unit]
Description=Sway - Wayland compositor
Documentation=man:sway(5) man:sway(1)
PartOf=graphical.target
After=graphical.target
[Service]
Type=simple
ExecStartPre=systemctl --user unset-environment WAYLAND_DISPLAY DISPLAY
ExecStart=/usr/bin/sway
Restart=always
RestartSec=3
Environment=XDG_RUNTIME_DIR=%t
[Install]
WantedBy=default.target
EOF
# Enable + start service
sudo -u kiosk XDG_RUNTIME_DIR=/run/user/$(id -u kiosk) systemctl --user daemon-reload
sudo -u kiosk XDG_RUNTIME_DIR=/run/user/$(id -u kiosk) systemctl --user --now enable sway.service
|
Create Sway config:
Replace placeholders:
HDMI-A-2: your HDMI output - check possible option with:1
2
3
4
| sudo -u kiosk XDG_RUNTIME_DIR=/run/user/$(id -u kiosk) \
SWAYSOCK=$(sudo -u kiosk find /run/user/$(id -u kiosk) \
-type s -name 'sway-ipc.*.sock' 2>/dev/null | head -n1) \
swaymsg -t get_outputs | jq '.[] | .name'
|
input "8746:1:ILITEK_ILITEK-TP": your touch input - check possible option with:1
2
3
4
| sudo -u kiosk XDG_RUNTIME_DIR=/run/user/$(id -u kiosk) \
SWAYSOCK=$(sudo -u kiosk find /run/user/$(id -u kiosk) \
-type s -name 'sway-ipc.*.sock' 2>/dev/null | head -n1) \
swaymsg -t get_inputs | jq '.[] | select(.type == "touch") | .identifier'
|
1280x800: your screen resolution - check possible option with:1
2
3
4
| sudo -u kiosk XDG_RUNTIME_DIR=/run/user/$(id -u kiosk) \
SWAYSOCK=$(sudo -u kiosk find /run/user/$(id -u kiosk) \
-type s -name 'sway-ipc.*.sock' 2>/dev/null | head -n1) \
swaymsg -t get_outputs
|
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
| sudo -u kiosk mkdir -p /home/kiosk/.config/sway
sudo -u kiosk tee /home/kiosk/.config/sway/config <<'EOF'
xwayland disable
set $mod Mod4
set $term foot
set $menu dmenu_path | wmenu | xargs swaymsg exec --
#output * bg /usr/share/backgrounds/sway/Sway_Wallpaper_Blue_1920x1080.png fill
### Idle handling with dpms + touch fix ###
exec swayidle -w \
timeout 60 'swaymsg "output * dpms off"' \
resume 'swaymsg "output * dpms on"'
### Screen & input mapping ###
output "HDMI-A-2" {
resolution 1280x800
transform 90
}
input "8746:1:ILITEK_ILITEK-TP" {
map_to_output "HDMI-A-2"
tap enabled
natural_scroll enabled
middle_emulation enabled
}
### Cursor hiding ###
seat * hide_cursor 1000
include /etc/sway/config.d/*
EOF
sudo -u kiosk XDG_RUNTIME_DIR=/run/user/$(id -u kiosk) systemctl --user restart sway.service
|
Systemd User Service for Browser [firefox]
Install Firefox
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| sudo install -d -m 0755 /etc/apt/keyrings
curl -fsSL https://packages.mozilla.org/apt/repo-signing-key.gpg | \
sudo tee /etc/apt/keyrings/packages.mozilla.org.asc > /dev/null
gpg -n -q --import --import-options import-show /etc/apt/keyrings/packages.mozilla.org.asc | \
awk '/pub/{getline; gsub(/^ +| +$/,""); if($0 == "35BAA0B33E9EB396F59CA838C0BA5CE6DC6315A3") print "\nThe key fingerprint matches ("$0").\n"; else print "\nVerification failed: the fingerprint ("$0") does not match the expected one.\n"}' # pragma: allowlist secret
sudo tee /etc/apt/sources.list.d/mozilla.sources <<'EOF'
Types: deb
URIs: https://packages.mozilla.org/apt
Suites: mozilla
Components: main
Signed-By: /etc/apt/keyrings/packages.mozilla.org.asc
EOF
sudo tee /etc/apt/preferences.d/mozilla <<'EOF'
Package: *
Pin: origin packages.mozilla.org
Pin-Priority: 1000
EOF
sudo apt update && sudo apt install -y firefox
|
Setup Firefox
1
2
3
4
5
6
7
8
9
10
11
12
| sudo -u kiosk XDG_RUNTIME_DIR=/run/user/$(id -u kiosk) firefox --headless --createprofile kiosk
FIREFOX_PROFILE="$(sudo -iu kiosk find /home/kiosk/.config/mozilla/firefox/ -name *.kiosk)"
curl -fsSL https://github.com/MVladislav/ansible-install-client/raw/refs/heads/main/templates/firefox/user.js.j2 | sudo -u kiosk tee "$FIREFOX_PROFILE/user.js"
curl -fsSL https://github.com/MVladislav/ansible-install-client/raw/refs/heads/main/templates/firefox/user-overrides.js.j2 | sudo -u kiosk tee -a "$FIREFOX_PROFILE/user.js"
sudo -u kiosk tee -a "$FIREFOX_PROFILE/user.js" <<'EOF'
user_pref("dom.w3c_touch_events.enabled", 1);
user_pref("dom.w3c_pointer_events.enabled", 1);
user_pref("general.smoothScroll", true);
EOF
|
Setup Systemd
Replace placeholders:
<URL-TO-DASHBOARD>: e.g. https://home.local:8123/lovelace/kioskwayland-1: find correct DISPLAY index - check possible option with:1
| sudo -u kiosk ls "/run/user/$(sudo -u kiosk id -u)" | grep wayland
|
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
| sudo -u kiosk mkdir -p /home/kiosk/.config/systemd/user
sudo -u kiosk tee /home/kiosk/.config/systemd/user/browser-kiosk.service <<'EOF'
[Unit]
Description=Browser in kiosk mode
After=graphical.target sway.service
Requires=sway.service
[Service]
Type=simple
ExecStart=/usr/bin/firefox \
-P kiosk \
--kiosk \
--new-instance \
<URL-TO-DASHBOARD>
Restart=always
RestartSec=2
StartLimitBurst=5
Environment=XDG_RUNTIME_DIR=%t
Environment=DISPLAY=
Environment=WAYLAND_DISPLAY=wayland-1
Environment=MOZ_ENABLE_WAYLAND=1
Environment=MOZ_USE_XINPUT2=1
Environment=MOZ_ACCELERATED=1
[Install]
WantedBy=default.target
EOF
|
Enable and start
1
2
| sudo -u kiosk XDG_RUNTIME_DIR=/run/user/$(id -u kiosk) systemctl --user daemon-reload
sudo -u kiosk XDG_RUNTIME_DIR=/run/user/$(id -u kiosk) systemctl --user --now enable browser-kiosk.service
|
Alternative: Systemd User Service for Browser [chromium]
Install Chromium
1
| sudo apt install chromium-browser
|
Setup Systemd
Replace placeholders:
<URL-TO-DASHBOARD>: e.g. https://home.local:8123/lovelace/kioskwayland-1: find correct DISPLAY index - check possible option with:1
| sudo -u kiosk ls "/run/user/$(sudo -u kiosk id -u)" | grep wayland
|
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
| sudo -u kiosk mkdir -p /home/kiosk/.config/systemd/user
sudo -u kiosk tee /home/kiosk/.config/systemd/user/browser-kiosk.service <<'EOF'
[Unit]
Description=Browser in kiosk mode
After=graphical.target sway.service
Requires=sway.service
[Service]
Type=simple
ExecStart=/snap/bin/chromium --kiosk \
--app=<URL-TO-DASHBOARD> \
--disable-infobars \
--enable-features=VaapiVideoDecoder,VaapiVideoEncoder,CanvasOopRasterization,UseOzonePlatform,WaylandWindowDecorations \
--ozone-platform=wayland \
--use-gl=egl \
--enable-gpu-rasterization \
--ignore-gpu-blocklist \
--overscroll-history-navigation=0 \
--disable-translate \
--disable-session-crashed-bubble \
--noerrdialogs \
--no-first-run \
--load-extension=/home/kiosk/reload-on-touch \
--enable-zero-copy \
--disable-gpu-sandbox \
--no-sandbox \
--disable-dev-shm-usage
Restart=always
RestartSec=2
StartLimitBurst=5
Environment=XDG_RUNTIME_DIR=%t
Environment=DISPLAY=
Environment=WAYLAND_DISPLAY=wayland-1
[Install]
WantedBy=default.target
EOF
|
1
2
3
4
5
6
7
8
9
| sudo -u kiosk tee /home/kiosk/.config/systemd/user/browser-kiosk-restart.service <<'EOF'
[Unit]
Description=Restart the browser kiosk service
[Service]
Type=oneshot
ExecStart=/usr/bin/systemctl --user restart browser-kiosk.service
EOF
|
1
2
3
4
5
6
7
8
9
10
11
12
| sudo -u kiosk tee /home/kiosk/.config/systemd/user/browser-kiosk-restart.timer <<'EOF'
[Unit]
Description=Restart browser kiosk daily at 03:00
[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true
[Install]
WantedBy=timers.target
EOF
|
Add Reload-on-Touch Extension
Chromium extension for three-finger reload:
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
| sudo -u kiosk mkdir -p /home/kiosk/reload-on-touch
sudo -u kiosk tee /home/kiosk/reload-on-touch/manifest.json <<'EOF'
{
"manifest_version": 3,
"name": "Reload on Touch",
"version": "1.0",
"permissions": ["activeTab"],
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["touch.js"]
}
]
}
EOF
sudo -u kiosk tee /home/kiosk/reload-on-touch/touch.js <<'EOF'
// Reload page on three-finger tap
document.addEventListener("touchstart", (e) => {
if (e.touches.length === 3) {
location.reload();
}
});
EOF
|
Enable and start
1
2
3
| sudo -u kiosk XDG_RUNTIME_DIR=/run/user/$(id -u kiosk) systemctl --user daemon-reload
sudo -u kiosk XDG_RUNTIME_DIR=/run/user/$(id -u kiosk) systemctl --user --now enable browser-kiosk.service
sudo -u kiosk XDG_RUNTIME_DIR=/run/user/$(id -u kiosk) systemctl --user --now enable browser-kiosk-restart.timer
|
Disable Cloud-init (Optional)
You can also just leave it enabled; after first boot it won’t change much unless re-provisioned.
1
| sudo touch /etc/cloud/cloud-init.disabled
|
🔧 Troubleshooting & Tips
- Wi-Fi drops: verify
dmesg | grep brcmfmac for errors, adjust options in /etc/modprobe.d/brcmfmac.conf. - Slow Snap apps: prefer apt-installed Chromium/Firefox when possible.
- Check service logs on problems:
sudo -u kiosk XDG_RUNTIME_DIR=/run/user/$(id -u kiosk) journalctl --user -u sway.service -fsudo -u kiosk XDG_RUNTIME_DIR=/run/user/$(id -u kiosk) journalctl --user -u browser-kiosk.service -f
- Kiosk stuck or blank?
- Switch to another TTY with
Ctrl+Alt+F2 to get a shell for troubleshooting. - You can disable kiosk mode by editing/removing the systemd user services for debugging.
- If cloud-init doesn’t pick up changes, reapply configs manually:
1
2
| sudo rm /var/lib/cloud/data/instance-id
sudo cloud-init init --local
|