|
| 1 | +# Declarative Local User Management |
| 2 | + |
| 3 | +## Revision |
| 4 | + |
| 5 | +| Rev | Date | Author | Change Description | |
| 6 | +|:---:|:--------------:|:-----------:|:----------------------------------------------------------------------------------------------------------------------------------------------------:| |
| 7 | +| 1.0 | June 16, 2025 | Manoharan Sundaramoorthy | Initial HLD | |
| 8 | + |
| 9 | + |
| 10 | +## 1. Scope |
| 11 | +This document describes the high-level design for a new feature that provides a declarative and persistent method for managing local user accounts on a SONiC device. This feature allows administrators to define local users, including their roles, password hashes, SSH keys, and **security policies** including **login attempt limits**. |
| 12 | + |
| 13 | +This ensures that local user accounts are consistently applied with robust security postures and persist across reboots and upgrades. |
| 14 | + |
| 15 | +## 2. Definitions/Abbreviations |
| 16 | + |
| 17 | +| Abbreviation | Definition | |
| 18 | +|:------------:|:--------------------------------------------------------------------------| |
| 19 | +| **`userd`** | The new User Daemon proposed in this design. | |
| 20 | +| **PAM** | Pluggable Authentication Modules | |
| 21 | + |
| 22 | + |
| 23 | +## 3. Overview |
| 24 | +This feature introduces a new dedicated daemon, **`userd`**, which manages the full lifecycle of local users based on definitions in `CONFIG_DB`. It simplifies management by providing a user-friendly CLI, mapping abstract roles (`administrator`, `operator`) to specific Linux groups, and enforcing security policies. This provides a solution for managing secure, persistent local user accounts. |
| 25 | + |
| 26 | +## 4. Requirements |
| 27 | +### 4.1 Functional Requirements |
| 28 | +1. The system must allow an administrator to define a local user account declaratively. |
| 29 | +2. The user definition must support: |
| 30 | + * Username and a pre-hashed password. |
| 31 | + * A role, limited to either **`administrator`** or **`operator`**. |
| 32 | + * Authorized SSH keys (statically defined). |
| 33 | +3. The system must map roles to underlying Linux groups: |
| 34 | + * `administrator`: members of `sudo`, `docker`, `redis` and `admin` groups. |
| 35 | + * `operator`: members of a standard, non-privileged and would belong to `users` group. |
| 36 | +4. The system will **auto-generate** a unique UID for each new user. |
| 37 | +5. There should be atleast one user configured in the role of `administrator` |
| 38 | +6. **Default Admin User:** |
| 39 | + * The **`admin`** user (or the user specified during compilation) must be included in the `init_cfg.json` with default settings. |
| 40 | + * This ensures the admin user is present in CONFIG_DB by default and can be managed like any other user. |
| 41 | + * The admin user can be modified or disabled or deleted through standard CONFIG_DB operations. |
| 42 | +7. **Security Policy Requirements:** |
| 43 | + * **Login Attempts:** The system must support configuring a global maximum number of failed login attempts per role before accounts are temporarily locked. |
| 44 | +8. User accounts and their configurations must persist across system reboots and upgrades. |
| 45 | +9. **System Consistency:** On startup, the system must perform a consistency check to ensure Linux users match CONFIG_DB definitions and automatically remove any users that were added directly to Linux (bypassing CONFIG_DB). |
| 46 | + |
| 47 | +## 5. Architecture Design |
| 48 | +The architecture centers on the new `userd` daemon. This daemon will now interact with several core Linux subsystems to enforce the configured security policies. |
| 49 | + |
| 50 | +**`userd`'s Points of Interaction:** |
| 51 | +1. **CONFIG_DB:** Single source of truth for user configuration and global security policies. |
| 52 | +2. **Core User Files:** `/etc/passwd`, `/etc/shadow`, `/etc/group` for basic user identity. |
| 53 | +3. **PAM Configuration (`/etc/security/faillock.conf`):** To manage global failed login attempt policies per role via `pam_faillock`. |
| 54 | + |
| 55 | +## 6. High-Level Design |
| 56 | + |
| 57 | +### 6.1 `userd` Daemon |
| 58 | +The `userd` daemon's logic will be expanded to manage security configurations idempotently. |
| 59 | + |
| 60 | +**Startup Consistency Check:** |
| 61 | +* **System Reconciliation:** On startup, `userd` will perform a consistency check to ensure that all local users in the Linux system (`/etc/passwd`, `/etc/shadow`, `/etc/group`) match the definitions in CONFIG_DB. |
| 62 | +* **Cleanup of Unmanaged Users:** Any users found in the Linux system that are not defined in CONFIG_DB (except for system users like `root`, `admin`, `daemon`, etc.) will be automatically removed to maintain consistency. |
| 63 | +* **CONFIG_DB as Source of Truth:** This ensures that CONFIG_DB remains the single source of truth for user management and prevents configuration drift. |
| 64 | + |
| 65 | +**Default Admin User:** |
| 66 | +* **Init Config Integration:** The `admin` user (or the user specified during compilation) is included in `init_cfg.json` with default settings, ensuring it's always present in CONFIG_DB. |
| 67 | +* **Standard Management:** The admin user is managed through the same CONFIG_DB interface as other users, with no special exception handling required. |
| 68 | +* **Default Configuration:** The admin user in `init_cfg.json` will use the username and password hash from the `USERNAME` and `PASSWORD` environment variables during build, with fallback defaults if not specified. |
| 69 | + |
| 70 | +**Configuration File Hierarchy:** |
| 71 | +* **`init_cfg.json`:** Contains base system defaults including the default admin user. This file is generated at build-time from a Jinja2 template and is always present. It ensures every SONiC system has essential user management configuration. |
| 72 | +* **`golden_config_db.json`:** Optional site-specific configuration file that can override defaults when used with `config load_minigraph --override_config`. This allows deployments to customize user configurations without modifying the base system defaults. |
| 73 | +* **Configuration Precedence:** When both files are present, `golden_config_db.json` takes precedence over `init_cfg.json` during minigraph reload operations with override enabled. |
| 74 | + |
| 75 | +**User Account Management:** |
| 76 | +* **Account Status Control:** `userd` will monitor the `enabled` attribute for each user. When `enabled` is `false`, it will prepend `!` to the password hash in `/etc/shadow` to disable password-based login. When `enabled` is `true`, it will restore the original password hash. |
| 77 | +* **Password Hash Preservation:** When disabling a user, `userd` will store the original password hash and restore it when the user is re-enabled, ensuring seamless account management. |
| 78 | + |
| 79 | +**New Logic for Security Policies:** |
| 80 | + |
| 81 | +* **To enforce Login Attempts:** |
| 82 | + * `userd` will manage the PAM configuration file at `/etc/security/faillock.conf`. |
| 83 | + * For global role-based limits (e.g., administrators with limit of 5), it will ensure appropriate configuration in the global section. |
| 84 | + * `userd` will configure PAM to apply different limits based on user group membership (administrator vs operator roles). |
| 85 | + * `userd` will be responsible for ensuring the PAM stack is configured to use `pam_faillock` with role-based policies. |
| 86 | + |
| 87 | +## 7. SAI API |
| 88 | +No SAI API changes are required. |
| 89 | + |
| 90 | +## 8. Configuration and Management |
| 91 | +### 8.1 Config DB Enhancements |
| 92 | +The `USER` table schema is defined for individual user accounts, and a new `USER_SECURITY_POLICY` table is added for global role-based security policies. |
| 93 | + |
| 94 | +**Schema:** |
| 95 | +```json |
| 96 | +// Example for CONFIG_DB |
| 97 | +{ |
| 98 | + "USER": { |
| 99 | + "admin": { |
| 100 | + "role": "administrator", |
| 101 | + "password_hash": "$6$salt$hash_of_YourPaSsWoRd", |
| 102 | + "ssh_keys": ["ssh-rsa AAA..."], |
| 103 | + "enabled": true |
| 104 | + }, |
| 105 | + "newadmin": { |
| 106 | + "role": "administrator", |
| 107 | + "password_hash": "hashed_password", |
| 108 | + "ssh_keys": ["ssh-rsa BBB..."], |
| 109 | + "enabled": true |
| 110 | + }, |
| 111 | + "showuser": { |
| 112 | + "role": "operator", |
| 113 | + "password_hash": "hashed_password", |
| 114 | + "ssh_keys": ["ssh-rsa CCC..."], |
| 115 | + "enabled": false |
| 116 | + } |
| 117 | + }, |
| 118 | + "USER_SECURITY_POLICY": { |
| 119 | + "administrator": { |
| 120 | + "max_login_attempts": 5 |
| 121 | + }, |
| 122 | + "operator": { |
| 123 | + "max_login_attempts": 3 |
| 124 | + } |
| 125 | + } |
| 126 | +} |
| 127 | +``` |
| 128 | + |
| 129 | +**USER Table:** |
| 130 | +* `role` (string, required): `administrator` or `operator`. |
| 131 | +* `password_hash` (string, required): The hashed password. |
| 132 | +* `ssh_keys` (optional): List of SSH public keys for the user. |
| 133 | +* `enabled` (boolean, optional): Whether the user account is enabled. Defaults to `true` if not specified. |
| 134 | + |
| 135 | +**USER_SECURITY_POLICY Table:** |
| 136 | +* **`max_login_attempts`** (integer, optional): Number of failed login attempts before accounts with this role are locked. |
| 137 | + |
| 138 | +**Notes:** |
| 139 | +* Session timeouts are managed by the system's default timeout policy and are not configurable per user. |
| 140 | +* Password hashes can be generated using `mkpasswd` command-line utility or programmatically using libraries like `passlib` in Python for NETCONF/RESTCONF implementations. |
| 141 | +* The CLI provides both `--password-hash` (for pre-hashed passwords) and `--password-prompt` (for interactive secure password entry) options for improved security and usability. |
| 142 | +* **CONFIG_DB as Source of Truth:** On startup, `userd` performs a consistency check and removes any users that were added directly to Linux (bypassing CONFIG_DB) to ensure CONFIG_DB remains the authoritative source for user management. |
| 143 | +* **User Account Status:** When `enabled` is set to `false`, `userd` will prepend `!` to the password hash in `/etc/shadow` to disable password-based login while preserving SSH key access. When `enabled` is `true` (default), the original password hash is restored. |
| 144 | +* **Password Hash Format:** Password hashes in CONFIG_DB cannot start with `!` as this is managed automatically by the `enabled` attribute. Use `enabled: false` instead of manually prepending `!` to disable accounts. |
| 145 | +* **Administrator Availability:** At least one administrator user must remain enabled at all times to prevent complete loss of administrative access. This constraint is enforced by the Yang model. |
| 146 | +* **Default Admin User:** The `admin` user (or the user specified during compilation) is included in `init_cfg.json` with default settings, ensuring it's always available and manageable through standard CONFIG_DB operations. |
| 147 | + |
| 148 | +### 8.2 Yang Model Enhancements |
| 149 | +``` |
| 150 | +module sonic-user { |
| 151 | + yang-version 1.1; |
| 152 | + namespace "http://sonicproject.com/sonic-user"; |
| 153 | + prefix "sonic-user"; |
| 154 | +
|
| 155 | + import sonic-ext { |
| 156 | + prefix "sonic-ext"; |
| 157 | + } |
| 158 | +
|
| 159 | + // Common typedef for user roles |
| 160 | + typedef user-role { |
| 161 | + type enumeration { |
| 162 | + enum "administrator" { |
| 163 | + description "Grants administrative privileges (e.g., member of sudo, docker groups)."; |
| 164 | + } |
| 165 | + enum "operator" { |
| 166 | + description "Grants operator-level (read-only or limited) privileges."; |
| 167 | + } |
| 168 | + } |
| 169 | + description "User role that determines group memberships, privileges, and applicable security policies."; |
| 170 | + } |
| 171 | +
|
| 172 | + // Top-level container for the User feature |
| 173 | + container sonic-user { |
| 174 | + description "Top-level container for local user management configuration"; |
| 175 | +
|
| 176 | + list USER_LIST { |
| 177 | + key "username"; |
| 178 | + description "List of declaratively managed local users."; |
| 179 | +
|
| 180 | + must "count(../USER_LIST[role='administrator' and (not(enabled) or enabled='true')]) >= 1" { |
| 181 | + error-message "At least one administrator user must remain enabled."; |
| 182 | + } |
| 183 | +
|
| 184 | + leaf username { |
| 185 | + type string { |
| 186 | + pattern '[a-z_][a-z0-9_-]*[$]?' { |
| 187 | + error-message "Invalid username. Must start with a lowercase letter or underscore, followed by lowercase letters, numbers, underscores, or hyphens."; |
| 188 | + } |
| 189 | + must ". != 'root'" { |
| 190 | + error-message "Username cannot be 'root'."; |
| 191 | + }; |
| 192 | + length 1..32; |
| 193 | + } |
| 194 | + description "The username for the local account."; |
| 195 | + } |
| 196 | +
|
| 197 | + leaf role { |
| 198 | + type user-role; |
| 199 | + mandatory true; |
| 200 | + description "The role assigned to the user, which determines their group memberships and privileges."; |
| 201 | + } |
| 202 | +
|
| 203 | + leaf password_hash { |
| 204 | + type string; |
| 205 | + mandatory true; |
| 206 | + must "not(starts-with(., '!'))" { |
| 207 | + error-message "Password hash cannot start with '!'. Use the 'enabled' attribute to disable user accounts."; |
| 208 | + } |
| 209 | + 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."; |
| 210 | + } |
| 211 | +
|
| 212 | + leaf-list ssh_keys { |
| 213 | + type string; |
| 214 | + description "A list of full public SSH key strings."; |
| 215 | + } |
| 216 | +
|
| 217 | + leaf enabled { |
| 218 | + type boolean; |
| 219 | + default true; |
| 220 | + 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."; |
| 221 | + } |
| 222 | + } |
| 223 | +
|
| 224 | + list USER_SECURITY_POLICY_LIST { |
| 225 | + key "role"; |
| 226 | + description "Global security policies applied to users based on their role."; |
| 227 | +
|
| 228 | + leaf role { |
| 229 | + type user-role; |
| 230 | + description "The role for which this security policy applies."; |
| 231 | + } |
| 232 | +
|
| 233 | + leaf max_login_attempts { |
| 234 | + type uint32 { |
| 235 | + range "1..1000"; |
| 236 | + } |
| 237 | + description "Maximum number of failed login attempts before accounts with this role are locked. If not set, system defaults apply."; |
| 238 | + } |
| 239 | + } |
| 240 | + } |
| 241 | +} |
| 242 | +``` |
| 243 | + |
| 244 | +### 8.3 CLI Enhancements |
| 245 | +The CLI is enhanced with user management and global security policy commands. |
| 246 | + |
| 247 | +**User Add Command:** |
| 248 | +``` |
| 249 | +config user add <username> --role <role> |
| 250 | + [--password-hash <hash> | --password-prompt] |
| 251 | + [--ssh-key <key>] |
| 252 | + [--disabled] |
| 253 | +``` |
| 254 | +* Multiple `--ssh-key` flags can be provided to build a list. |
| 255 | +* Use `--password-hash` to provide a pre-hashed password directly. |
| 256 | +* Use `--password-prompt` to enter the password securely through an interactive prompt (password will be hashed automatically). |
| 257 | +* Use `--disabled` to create the account in disabled state. Accounts are enabled by default. |
| 258 | +* If neither password option is provided, the user account will be created with password login disabled (password set to `!`). |
| 259 | + |
| 260 | +**User Delete Command:** |
| 261 | +``` |
| 262 | +config user del <username> |
| 263 | +``` |
| 264 | + |
| 265 | +**User Modify Command:** |
| 266 | +``` |
| 267 | +config user modify <username> [--password-hash <hash> | --password-prompt] |
| 268 | + [--ssh-key <key>] |
| 269 | + [--enabled | --disabled] |
| 270 | +``` |
| 271 | + |
| 272 | +* Use `--password-hash` to provide a pre-hashed password directly. |
| 273 | +* Use `--password-prompt` to enter the password securely through an interactive prompt (password will be hashed automatically). |
| 274 | +* Use `--enabled` or `--disabled` to change the account status. |
| 275 | + |
| 276 | +**Security Policy Commands:** |
| 277 | +``` |
| 278 | +config user security-policy <role> --max-login-attempts <count> |
| 279 | +``` |
| 280 | +* Configure global login attempt limits for a specific role (`administrator` or `operator`). |
| 281 | + |
| 282 | +``` |
| 283 | +show user security-policy [<role>] |
| 284 | +``` |
| 285 | +* Display current security policies for all roles or a specific role. |
| 286 | + |
| 287 | +**Password Prompt Implementation:** |
| 288 | +When `--password-prompt` is used, the CLI will: |
| 289 | +1. Display a secure password prompt (e.g., "Enter password for user <username>:") |
| 290 | +2. Hide password input (no echo to terminal) |
| 291 | +3. Prompt for password confirmation (e.g., "Confirm password:") |
| 292 | +4. Validate that both entries match |
| 293 | +5. Hash the password using the same algorithm as `mkpasswd` (e.g., SHA-512) |
| 294 | +6. Store only the hashed password in CONFIG_DB |
| 295 | +7. Clear the plaintext password from memory immediately after hashing |
| 296 | + |
| 297 | +**Other commands (e.g., `show user`) remain as previously defined.** |
| 298 | + |
| 299 | +## 9. Testing Requirements |
| 300 | +### New System Test cases |
| 301 | +* **Global Login Attempts Policy:** |
| 302 | + * Configure `administrator` role with `max_login_attempts` of 5 and `operator` role with 3. |
| 303 | + * Create users with both roles and verify that login attempt limits are enforced according to their role. |
| 304 | + * Attempt to log in with incorrect passwords and verify accounts are locked based on their role's policy. |
| 305 | + * Verify `faillock --user <username>` shows the correct state for users of different roles. |
| 306 | + |
| 307 | +* **Password Management:** |
| 308 | + * Test user creation with `--password-hash` option and verify the hash is stored correctly. |
| 309 | + * Test user creation with `--password-prompt` option and verify the password is hashed and stored securely. |
| 310 | + * Verify that passwords entered via prompt are not logged in command history. |
| 311 | + * Test password modification using both hash and prompt methods. |
| 312 | + * Attempt to create a user with a password hash starting with `!` and verify it fails with an appropriate error message. |
| 313 | + |
| 314 | +* **Account Status Management:** |
| 315 | + * Create a user with `--disabled` and verify the password hash is prepended with `!` in `/etc/shadow`. |
| 316 | + * Enable a disabled user and verify the original password hash is restored. |
| 317 | + * Test that disabled users cannot login with password but can still use SSH keys. |
| 318 | + * Verify that the `enabled` attribute defaults to `true` when not specified. |
| 319 | + |
| 320 | +* **Administrator Availability Constraint:** |
| 321 | + * Attempt to disable the last remaining enabled administrator user and verify it fails with an appropriate error. |
| 322 | + * Verify that at least one administrator user can always be enabled when multiple administrators exist. |
| 323 | + * Test that the constraint is enforced during user deletion of administrator accounts. |
| 324 | + |
| 325 | +* **Default Admin User:** |
| 326 | + * Verify the `admin` user exists by default in `init_cfg.json` and is present in CONFIG_DB after system initialization. |
| 327 | + * Verify the `admin` user can be modified, disabled, or deleted through standard CONFIG_DB operations. |
| 328 | + * Test that the `admin` user has the configured username/password from build-time environment variables and administrator role when first created from init config. |
| 329 | + |
| 330 | +* **Startup Consistency Check:** |
| 331 | + * Create a user directly in Linux using `useradd` (bypassing CONFIG_DB). |
| 332 | + * Restart the `userd` daemon and verify the manually created user is automatically removed. |
| 333 | + * Verify that users defined in CONFIG_DB are preserved and properly configured. |
| 334 | + * Ensure system users (root, daemon, etc.) are not affected by the cleanup process. |
| 335 | + |
| 336 | +* **User Management:** Create users with different roles and verify all security policies are enforced correctly based on role-based policies. |
| 337 | + |
| 338 | +## 10. Future Enhancements |
| 339 | + |
| 340 | +### 10.1 Remote SSH Key Management |
| 341 | +Support for dynamically fetching SSH keys from remote URLs could be added in future versions to enable centralized SSH key management. |
| 342 | + |
| 343 | +## 11. Open/Resolved Issues |
| 344 | + |
| 345 | +None |
0 commit comments