Add blacklisted usernames

This commit is contained in:
Kevin Veen-Birkenbach 2025-07-02 15:57:14 +02:00
parent 9d1b44319c
commit 821275ce70
No known key found for this signature in database
GPG Key ID: 44D8F11FD62F878E
5 changed files with 250 additions and 19 deletions

View File

@ -6,6 +6,12 @@ USERS_SCRIPT := ./cli/generate_users.py
INCLUDES_OUT := ./tasks/utils/docker-roles.yml INCLUDES_OUT := ./tasks/utils/docker-roles.yml
INCLUDES_SCRIPT := ./cli/generate_playbook.py INCLUDES_SCRIPT := ./cli/generate_playbook.py
EXTRA_USERS := $(shell \
find $(ROLES_DIR) -maxdepth 1 -type d -name 'docker*' -printf '%f\n' \
| sed -E 's/^docker[_-]?//' \
| paste -sd, - \
)
.PHONY: build install test .PHONY: build install test
build: build:
@ -15,7 +21,7 @@ build:
@echo "✅ Applications defaults written to $(APPLICATIONS_OUT)\n" @echo "✅ Applications defaults written to $(APPLICATIONS_OUT)\n"
@echo "🔧 Generating users defaults → $(USERS_OUT) from roles in $(ROLES_DIR)" @echo "🔧 Generating users defaults → $(USERS_OUT) from roles in $(ROLES_DIR)"
@mkdir -p $(dir $(USERS_OUT)) @mkdir -p $(dir $(USERS_OUT))
python3 $(USERS_SCRIPT) --roles-dir $(ROLES_DIR) --output $(USERS_OUT) python3 $(USERS_SCRIPT) --roles-dir $(ROLES_DIR) --output $(USERS_OUT) --extra-users "$(EXTRA_USERS)"
@echo "✅ Users defaults written to $(USERS_OUT)\n" @echo "✅ Users defaults written to $(USERS_OUT)\n"
@echo "🔧 Generating Docker role includes → $(INCLUDES_OUT)" @echo "🔧 Generating Docker role includes → $(INCLUDES_OUT)"
@mkdir -p $(dir $(INCLUDES_OUT)) @mkdir -p $(dir $(INCLUDES_OUT))

View File

@ -25,8 +25,7 @@ def build_users(defs, primary_domain, start_id, become_pwd):
""" """
users = OrderedDict() users = OrderedDict()
used_uids = set() used_uids = set()
used_gids = set()
# Pre-collect any provided uids/gids and check for duplicates # Pre-collect any provided uids/gids and check for duplicates
for key, overrides in defs.items(): for key, overrides in defs.items():
if 'uid' in overrides: if 'uid' in overrides:
@ -34,11 +33,6 @@ def build_users(defs, primary_domain, start_id, become_pwd):
if uid in used_uids: if uid in used_uids:
raise ValueError(f"Duplicate uid {uid} for user '{key}'") raise ValueError(f"Duplicate uid {uid} for user '{key}'")
used_uids.add(uid) used_uids.add(uid)
if 'gid' in overrides:
gid = overrides['gid']
if gid in used_gids:
raise ValueError(f"Duplicate gid {gid} for user '{key}'")
used_gids.add(gid)
next_free = start_id next_free = start_id
def allocate_free_id(): def allocate_free_id():
@ -48,7 +42,6 @@ def build_users(defs, primary_domain, start_id, become_pwd):
next_free += 1 next_free += 1
free = next_free free = next_free
used_uids.add(free) used_uids.add(free)
used_gids.add(free)
next_free += 1 next_free += 1
return free return free
@ -68,13 +61,8 @@ def build_users(defs, primary_domain, start_id, become_pwd):
if 'gid' in overrides: if 'gid' in overrides:
gid = overrides['gid'] gid = overrides['gid']
else: else:
# if gid not provided, default to uid (and ensure uniqueness) # default GID to UID
if uid in used_gids: gid = uid
# already added in allocate_free_id or pre-collect
gid = uid
else:
gid = uid
used_gids.add(gid)
entry = { entry = {
'username': username, 'username': username,
@ -102,8 +90,6 @@ def build_users(defs, primary_domain, start_id, become_pwd):
raise ValueError(f"Duplicate username '{un}' in merged users") raise ValueError(f"Duplicate username '{un}' in merged users")
if em in seen_emails: if em in seen_emails:
raise ValueError(f"Duplicate email '{em}' in merged users") raise ValueError(f"Duplicate email '{em}' in merged users")
if gd in seen_gids:
raise ValueError(f"Duplicate gid '{gd}' in merged users")
seen_usernames.add(un) seen_usernames.add(un)
seen_emails.add(em) seen_emails.add(em)
seen_gids.add(gd) seen_gids.add(gd)
@ -185,6 +171,11 @@ def parse_args():
'--start-id', '-s', type=int, default=1001, '--start-id', '-s', type=int, default=1001,
help='Starting uid/gid number (default: 1001).' help='Starting uid/gid number (default: 1001).'
) )
parser.add_argument(
'--extra-users', '-e',
help='Comma-separated list of additional usernames to include.',
default=None
)
return parser.parse_args() return parser.parse_args()
@ -199,6 +190,17 @@ def main():
print(f"Error merging user definitions: {e}", file=sys.stderr) print(f"Error merging user definitions: {e}", file=sys.stderr)
sys.exit(1) sys.exit(1)
# Add extra users if any
if args.extra_users:
for name in args.extra_users.split(','):
user = name.strip()
if not user:
continue
if user in user_defs:
print(f"Warning: extra user '{user}' already defined; skipping.", file=sys.stderr)
else:
user_defs[user] = {}
try: try:
users = build_users( users = build_users(
defs=user_defs, defs=user_defs,

View File

@ -3,7 +3,7 @@ images:
users: users:
administrator: administrator:
username: "administrator" username: "administrator"
crm: contact:
description: "General contact account" description: "General contact account"
username: "contact" username: "contact"
features: features:

View File

@ -0,0 +1,126 @@
# Reserved usernames
users:
root:
username: root
uid: 0
gid: 0
description: "System superuser"
daemon:
username: daemon
description: "Daemon processes owner"
bin:
username: bin
description: "Owner of essential binaries"
sys:
username: sys
description: "System files owner"
sync:
username: sync
description: "Sync user for filesystem synchronization"
games:
username: games
description: "Games and educational software owner"
man:
username: man
description: "Manual pages viewer"
lp:
username: lp
description: "Printer spooler"
mail:
username: mail
description: "Mail system"
news:
username: news
description: "Network news system"
uucp:
username: uucp
description: "UUCP system"
proxy:
username: proxy
description: "Proxy user"
www-data:
username: www-data
description: "Web server user"
backup:
username: backup
description: "Backup operator"
list:
username: list
description: "Mailing list manager"
irc:
username: irc
description: "IRC services user"
gnats:
username: gnats
description: "GNATS bug-reporting system"
nobody:
username: nobody
description: "Unprivileged user"
messagebus:
username: messagebus
description: "D-Bus message bus system"
sshd:
username: sshd
description: "SSH daemon"
rpc:
username: rpc
description: "Rpcbind daemon"
ftp:
username: ftp
description: "FTP server"
postfix:
username: postfix
description: "Postfix mail transfer agent"
mysql:
username: mysql
description: "MySQL database server"
mongodb:
username: mongodb
description: "MongoDB database server"
admin:
username: admin
description: "Generic reserved username"
administrator:
username: administrator
user:
username: user
description: "Generic reserved username"
test:
username: test
description: "Generic reserved username"
guest:
username: guest
description: "Generic reserved username"
demo:
username: demo
description: "Generic reserved username"
info:
username: info
description: "Generic reserved username"
support:
username: support
description: "Generic reserved username"
helpdesk:
username: helpdesk
description: "Generic reserved username"
operator:
username: operator
description: "Generic reserved username"
staff:
username: staff
description: "Generic reserved username"
smtp:
username: smtp
description: "Generic reserved username"
imap:
username: imap
description: "Generic reserved username"
pop:
username: pop
description: "Generic reserved username"
webmaster:
username: webmaster
description: "Generic reserved username"
mailman:
username: mailman
description: "Generic reserved username"

View File

@ -0,0 +1,97 @@
import os
import sys
import unittest
import tempfile
import shutil
import yaml
from collections import OrderedDict
# Add cli/ to import path
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../../..", "cli")))
import generate_users
class TestGenerateUsers(unittest.TestCase):
def test_build_users_auto_increment_and_overrides(self):
defs = {
'alice': {},
'bob': {'uid': 2000, 'email': 'bob@custom.com', 'description': 'Custom user'},
'carol': {}
}
users = generate_users.build_users(
defs=defs,
primary_domain='example.com',
start_id=1001,
become_pwd='pw'
)
# alice should get uid/gid 1001
self.assertEqual(users['alice']['uid'], 1001)
self.assertEqual(users['alice']['gid'], 1001)
self.assertEqual(users['alice']['email'], 'alice@example.com')
# bob overrides
self.assertEqual(users['bob']['uid'], 2000)
self.assertEqual(users['bob']['gid'], 2000)
self.assertEqual(users['bob']['email'], 'bob@custom.com')
self.assertIn('description', users['bob'])
# carol should get next free id = 1002
self.assertEqual(users['carol']['uid'], 1002)
self.assertEqual(users['carol']['gid'], 1002)
def test_build_users_duplicate_override_uid(self):
defs = {
'u1': {'uid': 1001},
'u2': {'uid': 1001}
}
with self.assertRaises(ValueError):
generate_users.build_users(defs, 'ex.com', 1001, 'pw')
def test_build_users_shared_gid_allowed(self):
# Allow two users to share the same GID when one overrides gid and the other uses that as uid
defs = {
'a': {'uid': 1500},
'b': {'gid': 1500}
}
users = generate_users.build_users(defs, 'ex.com', 1500, 'pw')
# Both should have gid 1500
self.assertEqual(users['a']['gid'], 1500)
self.assertEqual(users['b']['gid'], 1500)
def test_build_users_duplicate_username_email(self):
defs = {
'u1': {'username': 'same', 'email': 'same@ex.com'},
'u2': {'username': 'same'}
}
# second user with same username should raise
with self.assertRaises(ValueError):
generate_users.build_users(defs, 'ex.com', 1001, 'pw')
def test_dictify_converts_ordereddict(self):
od = generate_users.OrderedDict([('a', 1), ('b', {'c': 2})])
result = generate_users.dictify(OrderedDict(od))
self.assertIsInstance(result, dict)
self.assertEqual(result, {'a': 1, 'b': {'c': 2}})
def test_load_user_defs_and_conflict(self):
# create temp roles structure
tmp = tempfile.mkdtemp()
try:
os.makedirs(os.path.join(tmp, 'role1/vars'))
os.makedirs(os.path.join(tmp, 'role2/vars'))
# role1 defines user x
with open(os.path.join(tmp, 'role1/vars/configuration.yml'), 'w') as f:
yaml.safe_dump({'users': {'x': {'email': 'x@a'}}}, f)
# role2 defines same user x with same value
with open(os.path.join(tmp, 'role2/vars/configuration.yml'), 'w') as f:
yaml.safe_dump({'users': {'x': {'email': 'x@a'}}}, f)
defs = generate_users.load_user_defs(tmp)
self.assertIn('x', defs)
# now conflict definition
with open(os.path.join(tmp, 'role2/vars/configuration.yml'), 'w') as f:
yaml.safe_dump({'users': {'x': {'email': 'x@b'}}}, f)
with self.assertRaises(ValueError):
generate_users.load_user_defs(tmp)
finally:
shutil.rmtree(tmp)
if __name__ == '__main__':
unittest.main()