diff --git a/build_debian.sh b/build_debian.sh index 9600b887c389..8cdbe6a90a63 100755 --- a/build_debian.sh +++ b/build_debian.sh @@ -320,6 +320,7 @@ fi ## Note: ca-certificates is needed for easy_install ## Note: don't install python-apt by pip, older than Debian repo one ## Note: fdisk and gpg are needed by fwutil +## Note: whois is needed for mkpasswd by sonic-utilities sudo LANG=C DEBIAN_FRONTEND=noninteractive chroot $FILESYSTEM_ROOT apt-get -y install \ file \ ifmetric \ @@ -386,7 +387,8 @@ sudo LANG=C DEBIAN_FRONTEND=noninteractive chroot $FILESYSTEM_ROOT apt-get -y in wireless-regdb \ ethtool \ zstd \ - nvme-cli + nvme-cli \ + whois sudo cp files/initramfs-tools/pzstd $FILESYSTEM_ROOT/etc/initramfs-tools/hooks/pzstd sudo chmod +x $FILESYSTEM_ROOT/etc/initramfs-tools/hooks/pzstd diff --git a/files/build_templates/sonic_debian_extension.j2 b/files/build_templates/sonic_debian_extension.j2 index d5f77ac8ee2a..48f0fc2c0eba 100644 --- a/files/build_templates/sonic_debian_extension.j2 +++ b/files/build_templates/sonic_debian_extension.j2 @@ -346,6 +346,8 @@ install_deb_package {{deb}} # Install sonic-db-cli install_deb_package $debs_path/sonic-db-cli_*.deb +# Install SONiC host userd package (requires swss-common packages) +install_deb_package $debs_path/sonic-host-userd_*.deb {% if include_system_eventd == "y" and build_reduce_image_size != "y" %} # Install sonic-rsyslog-plugin diff --git a/platform/vs/docker-sonic-vs.mk b/platform/vs/docker-sonic-vs.mk index 8371030f8ac3..80bb50bc81d1 100644 --- a/platform/vs/docker-sonic-vs.mk +++ b/platform/vs/docker-sonic-vs.mk @@ -12,6 +12,7 @@ $(DOCKER_SONIC_VS)_DEPENDS += $(SYNCD_VS) \ $(LIBYANG_PY3) \ $(SONIC_UTILITIES_DATA) \ $(SONIC_HOST_SERVICES_DATA) \ + $(SONIC_HOST_USERD) \ $(SYSMGR) $(DOCKER_SONIC_VS)_PYTHON_WHEELS += $(SONIC_PY_COMMON_PY3) \ diff --git a/platform/vs/docker-sonic-vs/Dockerfile.j2 b/platform/vs/docker-sonic-vs/Dockerfile.j2 index 15ff85d808dc..317b656c71eb 100644 --- a/platform/vs/docker-sonic-vs/Dockerfile.j2 +++ b/platform/vs/docker-sonic-vs/Dockerfile.j2 @@ -34,6 +34,8 @@ RUN apt-get install -y net-tools \ iptables \ jq \ uuid-dev \ + # For using mkpasswd by sonic-utilities + whois \ # For installing Python m2crypto package # (these can be uninstalled after installation) build-essential \ diff --git a/rules/sonic-host-services.dep b/rules/sonic-host-services.dep index 905ec3c3cacc..54803a9f12f9 100644 --- a/rules/sonic-host-services.dep +++ b/rules/sonic-host-services.dep @@ -1,7 +1,7 @@ SPATH := $($(SONIC_HOST_SERVICES_PY3)_SRC_PATH) DEP_FILES := $(SONIC_COMMON_FILES_LIST) rules/sonic-host-services.mk rules/sonic-host-services.dep DEP_FILES += $(SONIC_COMMON_BASE_FILES_LIST) -SMDEP_FILES := $(addprefix $(SPATH)/,$(shell git -C $(SPATH) ls-files | grep -v ^data)) +SMDEP_FILES := $(addprefix $(SPATH)/,$(shell git -C $(SPATH) ls-files | grep -v ^data | grep -v ^userd)) $(SONIC_HOST_SERVICES_PY3)_CACHE_MODE := GIT_CONTENT_SHA $(SONIC_HOST_SERVICES_PY3)_DEP_FLAGS := $(SONIC_COMMON_FLAGS_LIST) diff --git a/rules/sonic-host-userd.dep b/rules/sonic-host-userd.dep new file mode 100644 index 000000000000..e06c0ad69c28 --- /dev/null +++ b/rules/sonic-host-userd.dep @@ -0,0 +1,8 @@ +SPATH := $($(SONIC_HOST_USERD)_SRC_PATH) +DEP_FILES := $(SONIC_COMMON_FILES_LIST) rules/sonic-host-userd.mk rules/sonic-host-userd.dep +DEP_FILES += $(SONIC_COMMON_BASE_FILES_LIST) +DEP_FILES += $(addprefix $(SPATH)/,$(shell git -C $(SPATH) ls-files)) + +$(SONIC_HOST_USERD)_CACHE_MODE := GIT_CONTENT_SHA +$(SONIC_HOST_USERD)_DEP_FLAGS := $(SONIC_COMMON_FLAGS_LIST) +$(SONIC_HOST_USERD)_DEP_FILES := $(DEP_FILES) diff --git a/rules/sonic-host-userd.mk b/rules/sonic-host-userd.mk new file mode 100644 index 000000000000..ec9c00d3beb9 --- /dev/null +++ b/rules/sonic-host-userd.mk @@ -0,0 +1,16 @@ +# SONiC host userd binary package + +SONIC_HOST_USERD = sonic-host-userd_1.0-1_$(CONFIGURED_ARCH).deb +$(SONIC_HOST_USERD)_SRC_PATH = $(SRC_PATH)/sonic-host-services/userd +$(SONIC_HOST_USERD)_DEPENDS += $(LIBSWSSCOMMON_DEV) +$(SONIC_HOST_USERD)_RDEPENDS += $(SONIC_HOST_SERVICES_DATA) $(LIBSWSSCOMMON) +SONIC_DPKG_DEBS += $(SONIC_HOST_USERD) + +# Debug symbols package +SONIC_HOST_USERD_DBG = sonic-host-userd-dbgsym_1.0-1_$(CONFIGURED_ARCH).deb +$(SONIC_HOST_USERD_DBG)_DEPENDS += $(SONIC_HOST_USERD) +$(SONIC_HOST_USERD_DBG)_RDEPENDS += $(SONIC_HOST_USERD) +$(eval $(call add_derived_package,$(SONIC_HOST_USERD),$(SONIC_HOST_USERD_DBG))) + +# Add to debug source archive for debugging +DBG_SRC_ARCHIVE += sonic-host-services/userd diff --git a/slave.mk b/slave.mk index 7ea76264d59b..81227e67e735 100644 --- a/slave.mk +++ b/slave.mk @@ -1398,6 +1398,7 @@ $(addprefix $(TARGET_PATH)/, $(SONIC_INSTALLERS)) : $(TARGET_PATH)/% : \ $(SONIC_UTILITIES_DATA) \ $(SONIC_CTRMGRD_RS) \ $(SONIC_HOST_SERVICES_DATA) \ + $(SONIC_HOST_USERD) \ $(BASH) \ $(BASH_TACPLUS) \ $(AUDISP_TACPLUS) \ diff --git a/src/sonic-yang-models/setup.py b/src/sonic-yang-models/setup.py index ff43f475bd59..532c68d62aec 100644 --- a/src/sonic-yang-models/setup.py +++ b/src/sonic-yang-models/setup.py @@ -147,6 +147,7 @@ 'sonic-smart-switch.yang', 'sonic-spanning-tree.yang', 'sonic-srv6.yang', + 'sonic-user.yang', ] class my_build_py(build_py): diff --git a/src/sonic-yang-models/tests/files/sample_config_db.json b/src/sonic-yang-models/tests/files/sample_config_db.json index 10d49083037d..0c4842a37720 100644 --- a/src/sonic-yang-models/tests/files/sample_config_db.json +++ b/src/sonic-yang-models/tests/files/sample_config_db.json @@ -3111,6 +3111,13 @@ "DEBUG_4|DIP_LINK_LOCAL": {}, "DEBUG_4|SIP_LINK_LOCAL": {} }, + "LOCAL_USER": { + "admin": { + "role": "administrator", + "password_hash": "$y$j9T$mYVqMrB/TF29nNg.IK63H1$4k1Th1bqfrLXo50z3.KJvsKpffgD7NPMExVmDTrnUc8", + "enabled": "true" + } + }, "HIGH_FREQUENCY_TELEMETRY_PROFILE": { "dummy": { "stream_state": "disabled", diff --git a/src/sonic-yang-models/yang-models/sonic-device_metadata.yang b/src/sonic-yang-models/yang-models/sonic-device_metadata.yang index 372894aa1846..10ed19a15652 100644 --- a/src/sonic-yang-models/yang-models/sonic-device_metadata.yang +++ b/src/sonic-yang-models/yang-models/sonic-device_metadata.yang @@ -116,6 +116,16 @@ module sonic-device_metadata { } } + leaf local_user_management { + type enumeration { + enum enabled; + enum disabled; + } + default disabled; + description "Enable or disable local user management feature. + When enabled, the system will manage local users through the LOCAL_USER table."; + } + leaf frr_mgmt_framework_config { type boolean; description "FRR configurations are handled by sonic-frr-mgmt-framework module when set to true, diff --git a/src/sonic-yang-models/yang-models/sonic-user.yang b/src/sonic-yang-models/yang-models/sonic-user.yang new file mode 100644 index 000000000000..af0cf7dda011 --- /dev/null +++ b/src/sonic-yang-models/yang-models/sonic-user.yang @@ -0,0 +1,102 @@ +module sonic-user { + yang-version 1.1; + namespace "http://github.com/sonic-net/sonic-user"; + prefix "sonic-user"; + + description "SONIC User Management YANG Module"; + + revision 2025-09-12 { + description "Initial revision for declarative user management"; + } + + // Common typedef for user roles + typedef user-role { + type enumeration { + enum "administrator" { + description "Grants administrative privileges (e.g., member of sudo, docker, admin, redis groups)."; + } + enum "operator" { + description "Grants operator-level (read-only or limited) privileges."; + } + } + description "User role that determines group memberships, privileges, and applicable security policies."; + } + + // Top-level container for the User feature + container sonic-user { + description "Top-level container for local user management configuration"; + + container LOCAL_USER { + description "LOCAL_USER part of config_db.json"; + + list LOCAL_USER_LIST { + key "username"; + description "List of declaratively managed local users."; + + must "count(../LOCAL_USER_LIST[role='administrator' and (not(enabled) or enabled='true')]) >= 1" { + error-message "At least one administrator user must remain enabled."; + } + + leaf username { + type string { + pattern '[a-z_][a-z0-9_-]*[$]?' { + error-message "Invalid username. Must start with a lowercase letter or underscore, followed by lowercase letters, numbers, underscores, or hyphens."; + } + length 1..32; + } + must ". != 'root'" { + error-message "Username cannot be 'root'."; + } + description "The username for the local account."; + } + + leaf role { + type user-role; + mandatory true; + description "The role assigned to the user, which determines their group memberships and privileges."; + } + + leaf password_hash { + type string; + mandatory true; + must "not(starts-with(., '!'))" { + error-message "Password hash cannot start with '!'. Use the 'enabled' attribute to disable user accounts."; + } + description "The hashed password string for the user, as found in /etc/shadow. Password hashes can be generated using 'mkpasswd' utility or programmatically using libraries like 'passlib'. To disable an account, use the 'enabled' attribute instead of prepending '!' to the password hash."; + } + + leaf-list ssh_keys { + type string; + description "A list of full public SSH key strings."; + } + + leaf enabled { + type boolean; + default true; + description "Whether the user account is enabled. When false, the password is disabled by prepending '!' to prevent password-based login while preserving SSH key access."; + } + } + } + + container LOCAL_ROLE_SECURITY_POLICY { + description "LOCAL_ROLE_SECURITY_POLICY part of config_db.json"; + + list LOCAL_ROLE_SECURITY_POLICY_LIST { + key "role"; + description "Global security policies applied to users based on their role."; + + leaf role { + type user-role; + description "The role for which this security policy applies."; + } + + leaf max_login_attempts { + type uint32 { + range "1..1000"; + } + description "Maximum number of failed login attempts before accounts with this role are locked. If not set, system defaults apply."; + } + } + } + } +}