adguardhome: run as an unprivileged user
authorGeorge Sapkin <[email protected]>
Wed, 26 Mar 2025 00:04:22 +0000 (02:04 +0200)
committerTianling Shen <[email protected]>
Wed, 6 Aug 2025 13:49:54 +0000 (21:49 +0800)
Run AdGuard Home without superuser privileges, by granting the binary
capabilities through ujail.

AdGuard Home writes new config files, so it must have r/w access to the
directory where these files live. Which means existing configs must be
migrated to a new directory, /etc/adguardhome, by default.

CAP_NET_BIND_SERVICE and CAP_NET_RAW capabilities are based on the
official documentation linked below.

Link: https://github.com/AdguardTeam/AdGuardHome/wiki/Getting-Started#running-without-superuser-linux-only
Signed-off-by: George Sapkin <[email protected]>
net/adguardhome/Makefile
net/adguardhome/files/adguardhome.config
net/adguardhome/files/adguardhome.defaults [new file with mode: 0644]
net/adguardhome/files/adguardhome.init
net/adguardhome/files/adguardhome.json [new file with mode: 0644]

index 6ee8313a6244fba38ff257846fb790f5ad782ffc..7634cde7866350801b60b9cf4659c140fb125e58 100644 (file)
@@ -7,7 +7,7 @@ include $(TOPDIR)/rules.mk
 
 PKG_NAME:=adguardhome
 PKG_VERSION:=0.107.64
-PKG_RELEASE:=1
+PKG_RELEASE:=2
 
 PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
 PKG_SOURCE_URL:=https://codeload.github.com/AdguardTeam/AdGuardHome/tar.gz/v$(PKG_VERSION)?
@@ -46,10 +46,11 @@ define Package/adguardhome
        TITLE:=Network-wide ads and trackers blocking DNS server
        URL:=https://github.com/AdguardTeam/AdGuardHome
        DEPENDS:=$(GO_ARCH_DEPENDS) +ca-bundle
+       USERID:=adguardhome=853:adguardhome=853
 endef
 
 define Package/adguardhome/conffiles
-/etc/adguardhome.yaml
+/etc/adguardhome/adguardhome.yaml
 /etc/config/adguardhome
 endef
 
@@ -72,14 +73,20 @@ endef
 
 define Package/adguardhome/install
        $(call GoPackage/Package/Install/Bin,$(1))
-       $(INSTALL_DIR) $(1)/etc/init.d
-       $(INSTALL_BIN) ./files/adguardhome.init $(1)/etc/init.d/adguardhome
+       $(INSTALL_DIR) $(1)/etc/capabilities
+       $(INSTALL_CONF) ./files/adguardhome.json $(1)/etc/capabilities/adguardhome.json
 
        $(INSTALL_DIR) $(1)/etc/config
-       $(INSTALL_DATA) ./files/adguardhome.config $(1)/etc/config/adguardhome
+       $(INSTALL_CONF) ./files/adguardhome.config $(1)/etc/config/adguardhome
+
+       $(INSTALL_DIR) $(1)/etc/init.d
+       $(INSTALL_BIN) ./files/adguardhome.init $(1)/etc/init.d/adguardhome
 
        $(INSTALL_DIR) $(1)/etc/sysctl.d
        $(INSTALL_CONF) ./files/adguardhome.sysctl $(1)/etc/sysctl.d/50-adguardhome.conf
+
+       $(INSTALL_DIR) $(1)/etc/uci-defaults
+       $(INSTALL_BIN) ./files/adguardhome.defaults $(1)/etc/uci-defaults/adguardhome
 endef
 
 $(eval $(call Download,adguardhome-frontend))
index 7a8a417d04afb8da0cec5c5402f6114bd7a79fe4..293ce6fd4052473054415b32c40aa110f74aff7b 100644 (file)
@@ -1,5 +1,12 @@
-config adguardhome config
-       option config /etc/adguardhome.yaml
+config adguardhome 'config'
+       # All paths except for PID must be readable by the configured user
+       option config '/etc/adguardhome/adguardhome.yaml'
        # Where to store persistent data by AdGuard Home
-       option workdir /var/lib/adguardhome
-       option pidfile /run/adguardhome.pid
+       option workdir '/var/lib/adguardhome'
+       option pidfile '/run/adguardhome.pid'
+       option user 'adguardhome'
+       option group 'adguardhome'
+       option verbose '0'
+       # Files and directories that AdGuard Home has read-only access to
+       # list jail_mount '/etc/ssl/adguardhome.crt'
+       # list jail_mount '/etc/ssl/adguardhome.key'
diff --git a/net/adguardhome/files/adguardhome.defaults b/net/adguardhome/files/adguardhome.defaults
new file mode 100644 (file)
index 0000000..92d8895
--- /dev/null
@@ -0,0 +1,93 @@
+#!/bin/sh
+
+OLD_CONFIG_FILE=$(uci -q get adguardhome.config.config)
+OLD_CONFIG_FILE=${OLD_CONFIG_FILE:-/etc/adguardhome.yaml}
+NEW_CONFIG_DIR=/etc/adguardhome
+NEW_CONFIG_FILE="$NEW_CONFIG_DIR/adguardhome.yaml"
+
+start_service() {
+       if ! /etc/init.d/adguardhome running; then
+               /etc/init.d/adguardhome start
+       fi
+}
+
+stop_service() {
+       if /etc/init.d/adguardhome running; then
+               /etc/init.d/adguardhome stop
+       fi
+}
+
+if [ -f "$OLD_CONFIG_FILE" ] && [ "$OLD_CONFIG_FILE" != "$NEW_CONFIG_FILE" ]; then
+       echo "Old AdGuard Home config found in '$OLD_CONFIG_FILE'"
+       OLD_CONFIG_DIR=$(dirname "$OLD_CONFIG_FILE")
+
+       USER=$(uci -q get adguardhome.config.user)
+       USER=${USER:-adguardhome}
+       GROUP=$(uci -q get adguardhome.config.group)
+       GROUP=${GROUP:-adguardhome}
+
+       echo "Using $USER:$GROUP for file ownership."
+
+       CUR_CONFIG_FILE="$OLD_CONFIG_FILE"
+       if [ "$OLD_CONFIG_DIR" = "/etc" ]; then
+               echo "AdGuard Home config must be stored in its own directory. Migrating..."
+               stop_service
+
+               [ -d "$NEW_CONFIG_DIR" ] || mkdir -m 0700 -p "$NEW_CONFIG_DIR"
+               mv "$OLD_CONFIG_FILE" "$NEW_CONFIG_FILE"
+               chown -R "$USER":"$GROUP" "$NEW_CONFIG_DIR"
+               CUR_CONFIG_FILE="$NEW_CONFIG_FILE"
+               uci set adguardhome.config.config="$NEW_CONFIG_FILE"
+
+               echo "Config migrated to '$NEW_CONFIG_FILE'"
+
+       elif [ "$OLD_CONFIG_DIR" != "$NEW_CONFIG_DIR" ]; then
+               echo "AdGuard Home config is stored in a non-default path. " \
+                       + "Ensure configured service user '$USER' can access it."
+       fi
+
+       # Use awk to split match on :, remove double quotes and trim leading and
+       # trailing spaces
+       cert_path=$(grep certificate_path: "$CUR_CONFIG_FILE" \
+               | awk -F':' '{gsub(/"/, "", $2); gsub(/^ +| +$/, "", $2); print $2}')
+       if [ -n "$cert_path" ]; then
+               echo "Found custom 'certificate_path' pointing to '$cert_path'." \
+                       + "Ensure configured service user '$USER' can access it."
+
+               stop_service
+
+               if ! uci -q show adguardhome.config.jail_mount | grep -q "$cert_path"; then
+                       uci add_list adguardhome.config.jail_mount="$cert_path"
+               fi
+       fi
+
+       private_key_path=$(grep private_key_path: "$CUR_CONFIG_FILE" \
+               | awk -F':' '{gsub(/"/, "", $2); gsub(/^ +| +$/, "", $2); print $2}')
+       if [ -n "$private_key_path" ]; then
+               echo "Found custom 'private_key_path' pointing to '$private_key_path'." \
+                       + "Ensure configured service user '$USER' can access it."
+
+               stop_service
+
+               if ! uci -q show adguardhome.config.jail_mount | grep -q "$private_key_path"; then
+                       uci add_list adguardhome.config.jail_mount="$private_key_path"
+               fi
+       fi
+
+       uci commit adguardhome
+       start_service
+
+elif [ "$OLD_CONFIG_FILE" != "$NEW_CONFIG_FILE" ]; then
+       echo "Old AdGuard Home config not found in '$OLD_CONFIG_FILE'"
+       stop_service
+
+       # Service script will create the new config directory
+       uci set adguardhome.config.config="$NEW_CONFIG_FILE"
+       echo "Config path changed to '$NEW_CONFIG_FILE'"
+
+       uci commit adguardhome
+       start_service
+
+else
+       echo "AdGuard Home config is in its default path '$NEW_CONFIG_FILE'. Nothing to do."
+fi
index 328ce693d085be187ce82a8640982e0f1e64252d..dd7e96c17eb74e7eac788192d0b32e0ebf0d363f 100644 (file)
@@ -1,4 +1,5 @@
 #!/bin/sh /etc/rc.common
+# shellcheck disable=SC3043 # ash supports local
 
 PROG=/usr/bin/AdGuardHome
 
@@ -10,34 +11,80 @@ START=19
 STOP=89
 
 boot() {
-  adguardhome_boot=1
-  start "$@"
+       ADGUARDHOME_BOOT=1
+       start "$@"
 }
 
 start_service() {
-  if [ -n "$adguardhome_boot" ]; then
-    # Do not start yet, wait for triggers
-    return 0
-  fi
-
-  config_load adguardhome
-  config_get CONFIG_FILE config config "/etc/adguardhome.yaml"
-  config_get PID_FILE config pidfile "/run/adguardhome.pid"
-  config_get WORK_DIR config workdir "/var/lib/adguardhome"
-
-  [ -d "$WORK_DIR" ] || mkdir -m 0755 -p "$WORK_DIR"
-
-  procd_open_instance
-  procd_set_param command "$PROG" -c "$CONFIG_FILE" -w "$WORK_DIR" --pidfile "$PID_FILE" --no-check-update
-  procd_set_param stdout 1
-  procd_set_param stderr 1
-  procd_close_instance
+       if [ -n "$ADGUARDHOME_BOOT" ]; then
+               # Do not start yet, wait for triggers
+               return 0
+       fi
+
+       local config_file
+       local group
+       local pid_file
+       local user
+       local verbose
+       local work_dir
+
+       config_load adguardhome
+       config_get config_file config config "/etc/adguardhome/adguardhome.yaml"
+       config_get work_dir config workdir "/var/lib/adguardhome"
+       config_get pid_file config pidfile "/run/adguardhome.pid"
+       config_get_bool verbose config verbose
+
+       config_get user config user adguardhome
+       config_get group config group adguardhome
+
+       local config_dir
+       config_dir=$(dirname "$config_file")
+       if [ "$config_dir" = '/etc' ]; then
+               echo "AdGuard Home config must be stored in its own directory, and not in /etc" >&2
+               exit 1
+       fi
+       mkdir -m 0700 -p "$config_dir"
+       chown -R "$user":"$group" "$config_dir"
+
+       mkdir -m 0700 -p "$work_dir"
+       chown -R "$user":"$group" "$work_dir"
+
+       procd_open_instance
+
+       procd_set_param command "$PROG"
+       procd_append_param command --config "$config_file"
+       procd_append_param command --work-dir "$work_dir"
+       procd_append_param command --logfile syslog
+       procd_append_param command --no-check-update
+       [ "$verbose" = 1 ] && procd_append_param command --verbose
+
+       procd_set_param pidfile "$pid_file"
+       procd_set_param stdout 1
+       procd_set_param stderr 1
+       procd_set_param user "$user"
+       procd_set_param group "$group"
+       procd_set_param capabilities /etc/capabilities/adguardhome.json
+       procd_set_param no_new_privs 1
+
+       # log is needed for logging to syslog instead of stdout
+       # procfs is needed to readlink /proc/self/exe
+       procd_add_jail adguardhome log procfs
+
+       # config directory must be writable to write new config files
+       procd_add_jail_mount_rw "$config_dir"
+       procd_add_jail_mount_rw "$work_dir"
+
+       procd_add_jail_mount /etc/hosts
+       procd_add_jail_mount /etc/ssl/certs
+       config_list_foreach config jail_mount procd_add_jail_mount
+
+       procd_close_instance
 }
 
 service_triggers() {
-  if [ -n "$adguardhome_boot" ]; then
-    # Wait for interfaces to be up before starting AdGuard Home for real.
-    # Prevents issues like https://github.com/openwrt/packages/issues/21868.
-    procd_add_raw_trigger "interface.*.up" 5000 /etc/init.d/adguardhome restart
-  fi
+       if [ -n "$ADGUARDHOME_BOOT" ]; then
+               # Wait for interfaces to be up before starting AdGuard Home for real.
+               # Prevents issues like https://github.com/openwrt/packages/issues/21868.
+               procd_add_raw_trigger "interface.*.up" 5000 /etc/init.d/adguardhome restart
+       fi
 }
diff --git a/net/adguardhome/files/adguardhome.json b/net/adguardhome/files/adguardhome.json
new file mode 100644 (file)
index 0000000..3586a4a
--- /dev/null
@@ -0,0 +1,22 @@
+{
+       "bounding": [
+               "CAP_NET_BIND_SERVICE",
+               "CAP_NET_RAW"
+       ],
+       "effective": [
+               "CAP_NET_BIND_SERVICE",
+               "CAP_NET_RAW"
+       ],
+       "ambient": [
+               "CAP_NET_BIND_SERVICE",
+               "CAP_NET_RAW"
+       ],
+       "permitted": [
+               "CAP_NET_BIND_SERVICE",
+               "CAP_NET_RAW"
+       ],
+       "inheritable": [
+               "CAP_NET_BIND_SERVICE",
+               "CAP_NET_RAW"
+       ]
+}