Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 25 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ This Home Assistant add-on provides a CUPS (Common Unix Printing System) print s

- **Network Printing**: Share printers across your local network using CUPS
- **Web Interface**: Access the CUPS administration panel at `http://<your-ha-ip>:631` to add and manage printers
- **Secure Administration**: Optional authentication for the CUPS admin interface
- **Secure Administration**: Authenticated CUPS admin interface with add-on credentials
- **Printer Support**: Compatible with a wide range of network and USB printers
- **Lightweight**: Built on Alpine Linux for minimal resource usage
- **Data Persistence**: Printer settings and configurations persist across restarts and updates
Expand All @@ -40,12 +40,12 @@ If you prefer to manually install:

2. Copy the repository to your Home Assistant add-ons directory:
```bash
scp -r cups-addon/cups root@<your-ha-ip>:/addons/
scp -r cups-addon/cups root@<your-ha-ip>:/addons/local/
```

3. In Home Assistant, go to **Settings** → **Add-ons** → **Add-on Store**.
4. Click the 3-dot menu (top right) → **Repositories**.
5. Add `/addons` as a repository URL and click **Add**.
5. Add `local_addons` as a repository URL and click **Add**.
6. Refresh the add-on store to see "CUPS Print Server."
7. Install the add-on.

Expand All @@ -56,10 +56,30 @@ The add-on provides the following configuration options:
```yaml
admin_username: printadmin
admin_password: your_secure_password
force_regenerate_config: false
```

- **admin_username**: Username for the CUPS admin interface (default: printadmin)
- **admin_password**: Password for the CUPS admin interface
- **admin_username**: Username for the CUPS admin interface (required)
- **admin_password**: Password for the CUPS admin interface (required)
- **force_regenerate_config**: One-shot option to replace persisted `cupsd.conf` with the latest managed default (a backup is created)

Printing from LAN clients remains open, while CUPS administration requires the configured credentials.

## Updating cupsd.conf Safely

The add-on manages default `cupsd.conf` from a dedicated template file and tracks the applied template hash.

- If no config exists, it creates a managed default file.
- If a managed file is based on an older template hash (or managed version), it is automatically backed up and replaced.
- If your file is unmanaged (custom/legacy), it is left untouched.

To apply the latest managed defaults on an existing install, set `force_regenerate_config: true` for one restart. The old file is backed up as `cupsd.conf.bak.<timestamp>`.
After restart, set `force_regenerate_config` back to `false`.

## Networking Notes

This add-on uses host networking so mDNS/Bonjour announcements are visible on the LAN.
This is required for AirPrint discovery by iOS/macOS clients.

After configuring:

Expand Down
9 changes: 3 additions & 6 deletions cups/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ ENV CUPS_SERVERROOT=/data/cups/config

# Setup base
RUN \
echo "http://dl-cdn.alpinelinux.org/alpine/v$(cat /etc/alpine-release | cut -d'.' -f1,2)/main" > /etc/apk/repositories && \
echo "http://dl-cdn.alpinelinux.org/alpine/v$(cat /etc/alpine-release | cut -d'.' -f1,2)/community" >> /etc/apk/repositories && \
echo "https://dl-cdn.alpinelinux.org/alpine/v$(cat /etc/alpine-release | cut -d'.' -f1,2)/main" > /etc/apk/repositories && \
echo "https://dl-cdn.alpinelinux.org/alpine/v$(cat /etc/alpine-release | cut -d'.' -f1,2)/community" >> /etc/apk/repositories && \
apk update && \
apk add --no-cache \
cups \
Expand All @@ -26,8 +26,5 @@ RUN \
# Copy data
COPY rootfs /

# Expose CUPS web interface port
EXPOSE 631

HEALTHCHECK \
CMD lpstat -r || exit 1
CMD lpstat -r || exit 1
10 changes: 4 additions & 6 deletions cups/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,12 @@ map:
- data:rw
- share:rw
init: false
ports:
631/tcp: 631
ports_description:
631/tcp: CUPS web interface and printing port
options:
admin_username: "admin"
admin_password: "admin"
admin_username: "printadmin"
admin_password: "change_me_now"
force_regenerate_config: false
schema:
admin_username: str
admin_password: password
force_regenerate_config: bool
startup: services
131 changes: 79 additions & 52 deletions cups/rootfs/etc/cont-init.d/cups.sh
Original file line number Diff line number Diff line change
@@ -1,4 +1,29 @@
#!/usr/bin/with-contenv bash
set -euo pipefail

source /usr/lib/bashio/bashio.sh

ADMIN_USER="$(bashio::config 'admin_username')"
ADMIN_PASSWORD="$(bashio::config 'admin_password')"
FORCE_REGENERATE_CONFIG="$(bashio::config 'force_regenerate_config')"

CUPSD_CONF="/data/cups/config/cupsd.conf"
TEMPLATE_PATH="/etc/cups/cupsd.conf.template"

if [[ -z "${ADMIN_USER}" ]]; then
bashio::log.fatal "Configuration admin_username must not be empty"
exit 1
fi

if [[ -z "${ADMIN_PASSWORD}" ]]; then
bashio::log.fatal "Configuration admin_password must not be empty"
exit 1
fi

if [[ "${ADMIN_PASSWORD}" == "change_me_now" ]]; then
bashio::log.fatal "Set a strong admin_password in add-on configuration before starting"
exit 1
fi

# Create CUPS data directories for persistence
mkdir -p /data/cups/cache
Expand All @@ -15,60 +40,62 @@ chmod -R 775 /data/cups
# Create CUPS configuration directory if it doesn't exist
mkdir -p /etc/cups

# Basic CUPS configuration without admin authentication
cat > /data/cups/config/cupsd.conf << EOL
# Listen on all interfaces
Listen 0.0.0.0:631

# Allow access from local network
<Location />
Order allow,deny
Allow localhost
Allow 10.0.0.0/8
Allow 172.16.0.0/12
Allow 192.168.0.0/16
</Location>

# Admin access (no authentication)
<Location /admin>
Order allow,deny
Allow localhost
Allow 10.0.0.0/8
Allow 172.16.0.0/12
Allow 192.168.0.0/16
</Location>

# Job management permissions
<Location /jobs>
Order allow,deny
Allow localhost
Allow 10.0.0.0/8
Allow 172.16.0.0/12
Allow 192.168.0.0/16
</Location>

<Limit Send-Document Send-URI Hold-Job Release-Job Restart-Job Purge-Jobs Set-Job-Attributes Create-Job-Subscription Renew-Subscription Cancel-Subscription Get-Notifications Reprocess-Job Cancel-Current-Job Suspend-Current-Job Resume-Job Cancel-My-Jobs Close-Job CUPS-Move-Job CUPS-Get-Document>
Order allow,deny
Allow localhost
Allow 10.0.0.0/8
Allow 172.16.0.0/12
Allow 192.168.0.0/16
</Limit>

# Enable web interface
WebInterface Yes

# Default settings
DefaultAuthType None
JobSheets none,none
PreserveJobHistory No
EOL
if [[ ! -f "${TEMPLATE_PATH}" ]]; then
bashio::log.fatal "Managed CUPS template not found at ${TEMPLATE_PATH}"
exit 1
fi

TEMPLATE_SHA256="$(sha256sum "${TEMPLATE_PATH}" | awk '{print $1}')"

MANAGED_CONFIG_VERSION="3"

write_managed_cupsd_conf() {
{
echo "# HA_ADDON_MANAGED_VERSION=${MANAGED_CONFIG_VERSION}"
echo "# HA_ADDON_TEMPLATE_SHA256=${TEMPLATE_SHA256}"
cat "${TEMPLATE_PATH}"
} > "${CUPSD_CONF}"
}

backup_and_regenerate_cupsd_conf() {
local ts
ts="$(date +%Y%m%d%H%M%S)"
cp "${CUPSD_CONF}" "${CUPSD_CONF}.bak.${ts}"
write_managed_cupsd_conf
}

if [[ ! -f "${CUPSD_CONF}" ]]; then
bashio::log.info "No persisted cupsd.conf found, creating managed default configuration"
write_managed_cupsd_conf
else
CURRENT_MANAGED_VERSION="$(grep -E '^# HA_ADDON_MANAGED_VERSION=' "${CUPSD_CONF}" | head -n1 | cut -d'=' -f2 || true)"
CURRENT_TEMPLATE_SHA256="$(grep -E '^# HA_ADDON_TEMPLATE_SHA256=' "${CUPSD_CONF}" | head -n1 | cut -d'=' -f2 || true)"

if [[ "${FORCE_REGENERATE_CONFIG}" == "true" ]]; then
bashio::log.warning "force_regenerate_config=true: replacing existing cupsd.conf and writing backup"
backup_and_regenerate_cupsd_conf
elif [[ -n "${CURRENT_MANAGED_VERSION}" ]]; then
if [[ "${CURRENT_MANAGED_VERSION}" != "${MANAGED_CONFIG_VERSION}" ]]; then
bashio::log.info "Updating managed cupsd.conf from version ${CURRENT_MANAGED_VERSION} to ${MANAGED_CONFIG_VERSION}"
backup_and_regenerate_cupsd_conf
elif [[ "${CURRENT_TEMPLATE_SHA256}" != "${TEMPLATE_SHA256}" ]]; then
bashio::log.info "Updating managed cupsd.conf because template content changed"
backup_and_regenerate_cupsd_conf
fi
elif [[ -z "${CURRENT_MANAGED_VERSION}" ]]; then
bashio::log.warning "Existing cupsd.conf is unmanaged; keeping it unchanged. Set force_regenerate_config=true to replace it with managed defaults"
fi
fi

if ! id -u "${ADMIN_USER}" >/dev/null 2>&1; then
adduser -D -H -s /sbin/nologin "${ADMIN_USER}"
fi

addgroup "${ADMIN_USER}" lpadmin >/dev/null 2>&1 || true
printf '%s:%s\n' "${ADMIN_USER}" "${ADMIN_PASSWORD}" | chpasswd

# Create a symlink from the default config location to our persistent location
ln -sf /data/cups/config/cupsd.conf /etc/cups/cupsd.conf
ln -sf "${CUPSD_CONF}" /etc/cups/cupsd.conf
ln -sf /data/cups/config/printers.conf /etc/cups/printers.conf
ln -sf /data/cups/config/ppd /etc/cups/ppd
ln -sf /data/cups/config/ssl /etc/cups/ssl

# Start CUPS service
/usr/sbin/cupsd -f
79 changes: 79 additions & 0 deletions cups/rootfs/etc/cups/cupsd.conf.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# Listen on all interfaces
Listen 0.0.0.0:631

# Allow access from local network
<Location />
Order allow,deny
Allow localhost
Allow 10.0.0.0/8
Allow 172.16.0.0/12
Allow 192.168.0.0/16
</Location>

# Admin access requires authentication
<Location /admin>
AuthType Basic
Require user @SYSTEM
Order allow,deny
Allow localhost
Allow 10.0.0.0/8
Allow 172.16.0.0/12
Allow 192.168.0.0/16
</Location>

<Location /admin/conf>
AuthType Basic
Require user @SYSTEM
Order allow,deny
Allow localhost
Allow 10.0.0.0/8
Allow 172.16.0.0/12
Allow 192.168.0.0/16
</Location>

<Location /admin/log>
AuthType Basic
Require user @SYSTEM
Order allow,deny
Allow localhost
Allow 10.0.0.0/8
Allow 172.16.0.0/12
Allow 192.168.0.0/16
</Location>

# Job listing remains LAN-accessible
<Location /jobs>
Order allow,deny
Allow localhost
Allow 10.0.0.0/8
Allow 172.16.0.0/12
Allow 192.168.0.0/16
</Location>

# Printing remains LAN-accessible
<Limit Send-Document Send-URI Create-Job>
Order allow,deny
Allow localhost
Allow 10.0.0.0/8
Allow 172.16.0.0/12
Allow 192.168.0.0/16
</Limit>

# Admin-level operations require authenticated system user
<Limit CUPS-Add-Modify-Printer CUPS-Delete-Printer CUPS-Add-Modify-Class CUPS-Delete-Class CUPS-Set-Default CUPS-Move-Job>
AuthType Basic
Require user @SYSTEM
Order allow,deny
Allow localhost
Allow 10.0.0.0/8
Allow 172.16.0.0/12
Allow 192.168.0.0/16
</Limit>

# Enable web interface
WebInterface Yes

# Default settings
DefaultAuthType Basic
JobSheets none,none
PreserveJobHistory No
4 changes: 4 additions & 0 deletions cups/rootfs/etc/services.d/cups/run
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/usr/bin/with-contenv bash
set -e

exec /usr/sbin/cupsd -f