feat(samba_server): new role for SMB/CIFS shares
Mirrors the nfs_server design: standalone tdbsam server, per-share access control (valid_users, write_list, force_user/group), optional guest fallback (map to guest = Bad User), UFW rules for ports 445/139, testparm-validated config, idempotent smbpasswd user creation.
This commit is contained in:
@@ -0,0 +1,72 @@
|
|||||||
|
# Samba Server
|
||||||
|
|
||||||
|
Minimal SMB/CIFS file sharing, mirroring the design of the `nfs_server` role.
|
||||||
|
|
||||||
|
Security is assumed to come from the network (firewall + VPN). No Active
|
||||||
|
Directory, no Kerberos, no winbind. Standalone server, `tdbsam` backend.
|
||||||
|
|
||||||
|
## In a nutshell
|
||||||
|
|
||||||
|
**Supports:**
|
||||||
|
|
||||||
|
- SMB2/SMB3 over TCP (port 445) and legacy NetBIOS (port 139)
|
||||||
|
- Per-share access control (`valid_users`, `write_list`, `force_user/group`)
|
||||||
|
- Optional guest fallback (`map to guest = Bad User`)
|
||||||
|
- UFW firewall configuration
|
||||||
|
- `testparm`-validated config before reload
|
||||||
|
- Idempotent user creation via `smbpasswd`
|
||||||
|
|
||||||
|
**Limitations:**
|
||||||
|
|
||||||
|
- No Active Directory / Kerberos integration
|
||||||
|
- Samba user accounts are only **created**, never updated. To rotate a
|
||||||
|
password, run `pdbedit -x <username>` first, then rerun the playbook.
|
||||||
|
- The matching system user (`/etc/passwd`) must already exist; this role
|
||||||
|
does not create UNIX accounts.
|
||||||
|
|
||||||
|
## Inventory
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# Bind only to private interfaces
|
||||||
|
samba_bind_interfaces_only: true
|
||||||
|
samba_interfaces:
|
||||||
|
- lo
|
||||||
|
- lan0
|
||||||
|
- 192.168.1.161
|
||||||
|
|
||||||
|
# UNIX users must exist beforehand (e.g. via the `users` role
|
||||||
|
# or manual `useradd`). This role only manages the SMB password.
|
||||||
|
samba_users:
|
||||||
|
- username: alice
|
||||||
|
password: "{{ vault_alice_smb_password }}"
|
||||||
|
- username: bob
|
||||||
|
password: "{{ vault_bob_smb_password }}"
|
||||||
|
|
||||||
|
samba_shares:
|
||||||
|
- name: photos
|
||||||
|
path: /mnt/andromeda/family-photos
|
||||||
|
comment: "Family photos"
|
||||||
|
read_only: false
|
||||||
|
valid_users: ["alice", "bob"]
|
||||||
|
write_list: ["alice"]
|
||||||
|
force_user: alice
|
||||||
|
force_group: users
|
||||||
|
|
||||||
|
- name: public
|
||||||
|
path: /mnt/andromeda/public
|
||||||
|
comment: "Read-only public share"
|
||||||
|
guest_ok: true
|
||||||
|
read_only: true
|
||||||
|
|
||||||
|
samba_server_firewall_allowed_sources:
|
||||||
|
- 192.168.1.0/24
|
||||||
|
- 192.168.27.0/27
|
||||||
|
```
|
||||||
|
|
||||||
|
See [`defaults/main.yml`](./defaults/main.yml) for all variables and defaults.
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
- https://wiki.archlinux.org/title/Samba
|
||||||
|
- https://wiki.samba.org/index.php/Setting_up_Samba_as_a_Standalone_Server
|
||||||
|
- `man smb.conf`, `man smbpasswd`, `man pdbedit`
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
---
|
||||||
|
# Global server identity
|
||||||
|
samba_workgroup: "WORKGROUP"
|
||||||
|
samba_server_string: "Samba Server"
|
||||||
|
samba_netbios_name: "{{ inventory_hostname | upper }}"
|
||||||
|
|
||||||
|
# Map unknown users to guest (similar to NFS all_squash).
|
||||||
|
# "Never" disables guest fallback, "Bad User" maps unknown users to guest.
|
||||||
|
samba_map_to_guest: "Bad User"
|
||||||
|
samba_guest_account: "nobody"
|
||||||
|
|
||||||
|
# Interfaces to bind samba listeners to.
|
||||||
|
# `bind interfaces only` is always enabled. If samba_interfaces is empty,
|
||||||
|
# samba binds to no interface and is effectively isolated.
|
||||||
|
samba_interfaces: []
|
||||||
|
# Example:
|
||||||
|
# samba_interfaces:
|
||||||
|
# - lo
|
||||||
|
# - lan0
|
||||||
|
# - 192.168.1.161
|
||||||
|
|
||||||
|
# Samba user accounts. The matching system user MUST already exist
|
||||||
|
# (created by another role or manually). The role only manages the
|
||||||
|
# samba password (smbpasswd) and is idempotent: existing users are
|
||||||
|
# not touched. To rotate a password, delete it first with
|
||||||
|
# `pdbedit -x <username>` then rerun the playbook.
|
||||||
|
samba_users: []
|
||||||
|
# Example:
|
||||||
|
# samba_users:
|
||||||
|
# - username: alice
|
||||||
|
# password: "secret"
|
||||||
|
|
||||||
|
# Shares
|
||||||
|
samba_shares: []
|
||||||
|
# Example:
|
||||||
|
# samba_shares:
|
||||||
|
# - name: photos
|
||||||
|
# path: /mnt/andromeda/family-photos
|
||||||
|
# comment: "Family photos"
|
||||||
|
# browseable: true # default: true
|
||||||
|
# read_only: false # default: true
|
||||||
|
# guest_ok: false # default: false
|
||||||
|
# valid_users: ["alice"] # optional
|
||||||
|
# write_list: ["alice"] # optional
|
||||||
|
# force_user: alice # optional
|
||||||
|
# force_group: users # optional
|
||||||
|
# create_mask: "0664" # default: 0664
|
||||||
|
# directory_mask: "0775" # default: 0775
|
||||||
|
# manage_directory: false # default: false (do not create/chown the dir)
|
||||||
|
# extra_options: # optional, raw smb.conf key/values
|
||||||
|
# "veto files": "/.DS_Store/"
|
||||||
|
|
||||||
|
samba_config_file: "/etc/samba/smb.conf"
|
||||||
|
|
||||||
|
# smbd defaults to 445 (SMB) and 139 (NetBIOS Session)
|
||||||
|
samba_port_smb: 445
|
||||||
|
samba_port_netbios: 139
|
||||||
|
|
||||||
|
samba_server_firewall_allowed_sources:
|
||||||
|
- 127.0.0.0/8
|
||||||
|
|
||||||
|
# OS-dependent service name
|
||||||
|
samba_service_name: >-
|
||||||
|
{{ (ansible_facts['os_family'] == 'Archlinux') | ternary('smb', 'smbd') }}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
- name: Restart samba
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
name: "{{ samba_service_name }}"
|
||||||
|
state: restarted
|
||||||
|
daemon_reload: true
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
---
|
||||||
|
- name: Validate samba users have a password set
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- item.username is defined and item.username | length > 0
|
||||||
|
- item.password is defined and item.password | length >= 8
|
||||||
|
fail_msg: |
|
||||||
|
Each samba_users entry must define `username` and `password` (>=8 chars).
|
||||||
|
See roles/samba_server/defaults/main.yml for the expected schema.
|
||||||
|
loop: "{{ samba_users }}"
|
||||||
|
loop_control:
|
||||||
|
label: "{{ item.username | default('<unnamed>') }}"
|
||||||
|
no_log: true
|
||||||
|
|
||||||
|
- name: Install samba
|
||||||
|
ansible.builtin.package:
|
||||||
|
name: samba
|
||||||
|
state: present
|
||||||
|
|
||||||
|
- name: Configure samba
|
||||||
|
ansible.builtin.template:
|
||||||
|
src: smb.conf.j2
|
||||||
|
dest: "{{ samba_config_file }}"
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: "0644"
|
||||||
|
validate: "testparm -s %s"
|
||||||
|
notify: Restart samba
|
||||||
|
|
||||||
|
- name: Ensure share directories exist
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "{{ item.path }}"
|
||||||
|
state: directory
|
||||||
|
owner: "{{ item.force_user | default('root') }}"
|
||||||
|
group: "{{ item.force_group | default('root') }}"
|
||||||
|
mode: "{{ item.directory_mask | default('0775') }}"
|
||||||
|
loop: "{{ samba_shares }}"
|
||||||
|
loop_control:
|
||||||
|
label: "{{ item.name }}"
|
||||||
|
when: item.manage_directory | default(false)
|
||||||
|
|
||||||
|
- name: Verify system users exist for samba accounts
|
||||||
|
ansible.builtin.getent:
|
||||||
|
database: passwd
|
||||||
|
key: "{{ item.username }}"
|
||||||
|
loop: "{{ samba_users }}"
|
||||||
|
loop_control:
|
||||||
|
label: "{{ item.username }}"
|
||||||
|
|
||||||
|
- name: Check existing samba users
|
||||||
|
ansible.builtin.command: pdbedit -L
|
||||||
|
register: samba_existing_users
|
||||||
|
changed_when: false
|
||||||
|
failed_when: false
|
||||||
|
|
||||||
|
- name: Add samba users
|
||||||
|
ansible.builtin.shell: |
|
||||||
|
set -o pipefail
|
||||||
|
(echo "{{ item.password }}"; echo "{{ item.password }}") | smbpasswd -s -a "{{ item.username }}"
|
||||||
|
args:
|
||||||
|
executable: /bin/bash
|
||||||
|
loop: "{{ samba_users }}"
|
||||||
|
loop_control:
|
||||||
|
label: "{{ item.username }}"
|
||||||
|
when: item.username not in (samba_existing_users.stdout | default(''))
|
||||||
|
changed_when: true
|
||||||
|
no_log: true
|
||||||
|
|
||||||
|
- name: Systemd service for samba is started and enabled
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
name: "{{ samba_service_name }}"
|
||||||
|
state: started
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
- name: Setup firewall rules for samba
|
||||||
|
community.general.ufw:
|
||||||
|
rule: allow
|
||||||
|
src: "{{ item.0 }}"
|
||||||
|
port: "{{ item.1 }}"
|
||||||
|
proto: tcp
|
||||||
|
direction: in
|
||||||
|
comment: "Samba (SMB)"
|
||||||
|
loop: "{{ samba_server_firewall_allowed_sources | product([samba_port_smb, samba_port_netbios]) | list }}"
|
||||||
|
retries: 5
|
||||||
|
delay: 2
|
||||||
|
register: ufw_result
|
||||||
|
until: ufw_result is succeeded
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
# {{ ansible_managed }}
|
||||||
|
[global]
|
||||||
|
workgroup = {{ samba_workgroup }}
|
||||||
|
server string = {{ samba_server_string }}
|
||||||
|
netbios name = {{ samba_netbios_name }}
|
||||||
|
server role = standalone server
|
||||||
|
security = user
|
||||||
|
passdb backend = tdbsam
|
||||||
|
map to guest = {{ samba_map_to_guest }}
|
||||||
|
guest account = {{ samba_guest_account }}
|
||||||
|
bind interfaces only = yes
|
||||||
|
interfaces = {{ samba_interfaces | join(' ') }}
|
||||||
|
log file = /var/log/samba/log.%m
|
||||||
|
max log size = 1000
|
||||||
|
logging = file
|
||||||
|
disable netbios = no
|
||||||
|
dns proxy = no
|
||||||
|
|
||||||
|
{% for share in samba_shares %}
|
||||||
|
[{{ share.name }}]
|
||||||
|
path = {{ share.path }}
|
||||||
|
{% if share.comment is defined %}
|
||||||
|
comment = {{ share.comment }}
|
||||||
|
{% endif %}
|
||||||
|
browseable = {{ share.browseable | default(true) | ternary('yes', 'no') }}
|
||||||
|
read only = {{ share.read_only | default(true) | ternary('yes', 'no') }}
|
||||||
|
guest ok = {{ share.guest_ok | default(false) | ternary('yes', 'no') }}
|
||||||
|
{% if share.valid_users is defined %}
|
||||||
|
valid users = {{ share.valid_users | join(' ') }}
|
||||||
|
{% endif %}
|
||||||
|
{% if share.write_list is defined %}
|
||||||
|
write list = {{ share.write_list | join(' ') }}
|
||||||
|
{% endif %}
|
||||||
|
{% if share.force_user is defined %}
|
||||||
|
force user = {{ share.force_user }}
|
||||||
|
{% endif %}
|
||||||
|
{% if share.force_group is defined %}
|
||||||
|
force group = {{ share.force_group }}
|
||||||
|
{% endif %}
|
||||||
|
create mask = {{ share.create_mask | default('0664') }}
|
||||||
|
directory mask = {{ share.directory_mask | default('0775') }}
|
||||||
|
{% if share.extra_options is defined %}
|
||||||
|
{% for k, v in share.extra_options.items() %}
|
||||||
|
{{ k }} = {{ v }}
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endfor %}
|
||||||
Reference in New Issue
Block a user