Skip to content

Commit 4f6c0fa

Browse files
[SONiC] Implement comprehensive local user management system
This implementation addresses the User Management HLD requirements for centralized user administration in SONiC. sonic-net/SONiC#2018 **1. YANG Model & Configuration Schema:** - Added sonic-user.yang model defining LOCAL_USER and LOCAL_ROLE_SECURITY_POLICY tables - Integrated user management into CONFIG_DB schema with role-based configuration - Added DEVICE_METADATA.local_user_management feature flag **2. User Management Daemon (userd):** - Implemented C++ daemon using SWSS framework for CONFIG_DB integration - Added user lifecycle management (create/update/delete/enable/disable) - Implemented role-based group assignment (administrator, operator roles) - Added SSH key management with proper file permissions - Integrated PAM faillock configuration using Jinja2 templates **3. CLI Interface:** - Extended sonic-utilities with 'config user' and 'show user' commands - Added user import functionality to migrate existing system users - Added role-based user management with proper validation **4. Build System Integration:** - Added sonic-host-services package with userd daemon and systemd service - Integrated user management into SONiC image build process - Added template-based configuration generation for init_cfg.json - Added build dependencies for JSON processing and password hashing
1 parent 59a84f5 commit 4f6c0fa

File tree

7 files changed

+140
-0
lines changed

7 files changed

+140
-0
lines changed

files/build_templates/init_cfg.json.j2

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
{%- if include_p4rt == "y" %}"synchronous_mode":"enable",{% endif %}
66
"default_bgp_status": {% if shutdown_bgp_on_start == "y" %}"down"{% else %}"up"{% endif %},
77
"default_pfcwd_status": {% if enable_pfcwd_on_start == "y" %}"enable"{% else %}"disable"{% endif %},
8+
"local_user_management": {%- if username and password %}"enabled"{% else %}"disabled"{% endif %},
89
"timezone": "UTC"
910
}
1011
},
@@ -212,5 +213,14 @@
212213
"admin_state": "enabled",
213214
"vrf": "default"
214215
}
216+
},
217+
"LOCAL_USER": {
218+
{%- if username and password %}
219+
"{{ username }}": {
220+
"role": "administrator",
221+
"password_hash": "{{ password }}",
222+
"enabled": true
223+
}
224+
{%- endif %}
215225
}
216226
}

files/build_templates/sonic_debian_extension.j2

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,9 @@ install_pip_package {{sonic_host_services_py3_wheel_path}}
268268
# Install SONiC host services data files (and any dependencies via 'apt-get -y install -f')
269269
install_deb_package $debs_path/sonic-host-services-data_*.deb
270270

271+
# Install SONiC host services binary package (C++ userd daemon)
272+
install_deb_package $debs_path/sonic-host-services_*.deb
273+
271274
{% if enable_ztp == "y" %}
272275
# Install ZTP (and its dependencies via 'apt-get -y install -f')
273276
install_deb_package $debs_path/sonic-ztp_*.deb

rules/sonic-host-services.dep

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,13 @@ $(SONIC_HOST_SERVICES_PY3)_DEP_FLAGS := $(SONIC_COMMON_FLAGS_LIST)
88
$(SONIC_HOST_SERVICES_PY3)_DEP_FILES := $(DEP_FILES)
99
$(SONIC_HOST_SERVICES_PY3)_SMDEP_FILES := $(SMDEP_FILES)
1010
$(SONIC_HOST_SERVICES_PY3)_SMDEP_PATHS := $(SPATH)
11+
12+
# Binary package dependencies
13+
SPATH_BIN := $($(SONIC_HOST_SERVICES)_SRC_PATH)
14+
DEP_FILES_BIN := $(SONIC_COMMON_FILES_LIST) rules/sonic-host-services.mk rules/sonic-host-services.dep
15+
DEP_FILES_BIN += $(SONIC_COMMON_BASE_FILES_LIST)
16+
DEP_FILES_BIN += $(addprefix $(SPATH_BIN)/,$(shell git -C $(SPATH_BIN) ls-files | grep -v ^data))
17+
18+
$(SONIC_HOST_SERVICES)_CACHE_MODE := GIT_CONTENT_SHA
19+
$(SONIC_HOST_SERVICES)_DEP_FLAGS := $(SONIC_COMMON_FLAGS_LIST)
20+
$(SONIC_HOST_SERVICES)_DEP_FILES := $(DEP_FILES_BIN)

rules/sonic-host-services.mk

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,10 @@ $(SONIC_HOST_SERVICES_PY3)_DEPENDS += $(SONIC_PY_COMMON_PY3) \
88
$(SONIC_HOST_SERVICES_PY3)_DEBS_DEPENDS = $(LIBSWSSCOMMON) \
99
$(PYTHON3_SWSSCOMMON)
1010
SONIC_PYTHON_WHEELS += $(SONIC_HOST_SERVICES_PY3)
11+
12+
# SONiC host services binary package (C++ userd daemon)
13+
SONIC_HOST_SERVICES = sonic-host-services_1.0-1_$(CONFIGURED_ARCH).deb
14+
$(SONIC_HOST_SERVICES)_SRC_PATH = $(SRC_PATH)/sonic-host-services
15+
$(SONIC_HOST_SERVICES)_DEPENDS += $(LIBSWSSCOMMON_DEV)
16+
$(SONIC_HOST_SERVICES)_RDEPENDS += $(SONIC_HOST_SERVICES_DATA) $(LIBSWSSCOMMON)
17+
SONIC_DPKG_DEBS += $(SONIC_HOST_SERVICES)

src/sonic-yang-models/setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@
146146
'sonic-smart-switch.yang',
147147
'sonic-spanning-tree.yang',
148148
'sonic-srv6.yang',
149+
'sonic-user.yang',
149150
]
150151

151152
class my_build_py(build_py):

src/sonic-yang-models/tests/files/sample_config_db.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3110,6 +3110,13 @@
31103110
"DEBUG_COUNTER_DROP_REASON": {
31113111
"DEBUG_4|DIP_LINK_LOCAL": {},
31123112
"DEBUG_4|SIP_LINK_LOCAL": {}
3113+
},
3114+
"LOCAL_USER": {
3115+
"admin": {
3116+
"role": "administrator",
3117+
"password_hash": "$6$rounds=656000$YI/0Vi2X$YI/0Vi2X.FGpk0yZ0DSbMVyp0/ozeWVnJMSUVJsvZQR2ZjbDxw0jkEQBB2trWecfmJpSg.k2VjbdJVv.JZlnM.",
3118+
"enabled": "true"
3119+
}
31133120
}
31143121
},
31153122
"SAMPLE_CONFIG_DB_UNKNOWN": {
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
module sonic-user {
2+
yang-version 1.1;
3+
namespace "http://github.com/sonic-net/sonic-user";
4+
prefix "sonic-user";
5+
6+
description "SONIC User Management YANG Module";
7+
8+
revision 2025-06-19 {
9+
description "Initial revision for declarative user management";
10+
}
11+
12+
// Common typedef for user roles
13+
typedef user-role {
14+
type enumeration {
15+
enum "administrator" {
16+
description "Grants administrative privileges (e.g., member of sudo, docker groups).";
17+
}
18+
enum "operator" {
19+
description "Grants operator-level (read-only or limited) privileges.";
20+
}
21+
}
22+
description "User role that determines group memberships, privileges, and applicable security policies.";
23+
}
24+
25+
// Top-level container for the User feature
26+
container sonic-user {
27+
description "Top-level container for local user management configuration";
28+
29+
container LOCAL_USER {
30+
description "LOCAL_USER part of config_db.json";
31+
32+
list LOCAL_USER_LIST {
33+
key "username";
34+
description "List of declaratively managed local users.";
35+
36+
must "count(../LOCAL_USER_LIST[role='administrator' and (not(enabled) or enabled='true')]) >= 1" {
37+
error-message "At least one administrator user must remain enabled.";
38+
}
39+
40+
leaf username {
41+
type string {
42+
pattern '[a-z_][a-z0-9_-]*[$]?' {
43+
error-message "Invalid username. Must start with a lowercase letter or underscore, followed by lowercase letters, numbers, underscores, or hyphens.";
44+
}
45+
length 1..32;
46+
}
47+
must ". != 'root'" {
48+
error-message "Username cannot be 'root'.";
49+
}
50+
description "The username for the local account.";
51+
}
52+
53+
leaf role {
54+
type user-role;
55+
mandatory true;
56+
description "The role assigned to the user, which determines their group memberships and privileges.";
57+
}
58+
59+
leaf password_hash {
60+
type string;
61+
mandatory true;
62+
must "not(starts-with(., '!'))" {
63+
error-message "Password hash cannot start with '!'. Use the 'enabled' attribute to disable user accounts.";
64+
}
65+
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.";
66+
}
67+
68+
leaf-list ssh_keys {
69+
type string;
70+
description "A list of full public SSH key strings.";
71+
}
72+
73+
leaf enabled {
74+
type boolean;
75+
default true;
76+
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.";
77+
}
78+
}
79+
}
80+
81+
container LOCAL_ROLE_SECURITY_POLICY {
82+
description "LOCAL_ROLE_SECURITY_POLICY part of config_db.json";
83+
84+
list LOCAL_ROLE_SECURITY_POLICY_LIST {
85+
key "role";
86+
description "Global security policies applied to users based on their role.";
87+
88+
leaf role {
89+
type user-role;
90+
description "The role for which this security policy applies.";
91+
}
92+
93+
leaf max_login_attempts {
94+
type uint32 {
95+
range "1..1000";
96+
}
97+
description "Maximum number of failed login attempts before accounts with this role are locked. If not set, system defaults apply.";
98+
}
99+
}
100+
}
101+
}
102+
}

0 commit comments

Comments
 (0)