Files
netbird-iac/ansible/netbird/setup-users.yml
2026-02-15 18:37:15 +02:00

282 lines
11 KiB
YAML

---
# =============================================================================
# NetBird v1.6 - User Provisioning
# =============================================================================
# Creates users with embedded IdP and stores generated passwords.
# Requires: service user PAT in vault.yml (see setup-bootstrap.yml)
#
# Run:
# ansible-playbook -i inventory.yml setup-users.yml --ask-vault-pass
#
# Optional variables:
# -e "dry_run=true" Preview changes without creating users
# =============================================================================
- name: Provision NetBird Users
hosts: netbird_servers
become: false
gather_facts: true
vars_files:
- group_vars/netbird_servers.yml
- group_vars/vault.yml
vars:
# For SSL-IP mode, use server IP; for domain mode, use netbird_domain
netbird_api_host: "{{ hostvars[inventory_hostname].ansible_host | default(netbird_domain) }}"
netbird_api_url: "https://{{ netbird_api_host }}/api"
dry_run: false
pre_tasks:
# =========================================================================
# Validate Prerequisites
# =========================================================================
- name: Validate service PAT is provided
ansible.builtin.assert:
that:
- vault_netbird_service_pat is defined
- vault_netbird_service_pat | length > 0
fail_msg: |
Service PAT not configured!
Run setup-bootstrap.yml first, then add PAT to vault.yml
- name: Verify API connectivity with PAT
ansible.builtin.uri:
url: "{{ netbird_api_url }}/users"
method: GET
headers:
Authorization: "Token {{ vault_netbird_service_pat }}"
Accept: "application/json"
validate_certs: false
status_code: [200]
register: api_check
delegate_to: localhost
- name: Display connection status
ansible.builtin.debug:
msg: "API connection successful. Found {{ api_check.json | length }} existing users."
tasks:
# =========================================================================
# Fetch Existing State
# =========================================================================
- name: Get existing users
ansible.builtin.uri:
url: "{{ netbird_api_url }}/users"
method: GET
headers:
Authorization: "Token {{ vault_netbird_service_pat }}"
Accept: "application/json"
validate_certs: false
status_code: [200]
register: existing_users_response
delegate_to: localhost
- name: Extract existing user emails
ansible.builtin.set_fact:
existing_user_emails: "{{ existing_users_response.json | map(attribute='email') | list }}"
- name: Get existing groups
ansible.builtin.uri:
url: "{{ netbird_api_url }}/groups"
method: GET
headers:
Authorization: "Token {{ vault_netbird_service_pat }}"
Accept: "application/json"
validate_certs: false
status_code: [200]
register: existing_groups_response
delegate_to: localhost
- name: Build group ID mapping
ansible.builtin.set_fact:
group_id_map: "{{ group_id_map | default({}) | combine({item.name: item.id}) }}"
loop: "{{ existing_groups_response.json }}"
# =========================================================================
# Resolve Auto-Groups for Users
# =========================================================================
- name: Resolve auto-groups for users
ansible.builtin.set_fact:
resolved_users: "{{ resolved_users | default([]) + [user_with_groups] }}"
vars:
battalion_group: >-
{%- if item.battalion is defined and item.battalion -%}
{%- if item.type | default('pilot') == 'pilot' -%}
{{ item.battalion }}-pilots
{%- else -%}
{{ item.battalion }}-ground-stations
{%- endif -%}
{%- endif -%}
final_auto_groups: >-
{{ item.auto_groups | default([]) + ([battalion_group | trim] if battalion_group | trim else []) }}
resolved_group_ids: >-
{{ final_auto_groups | map('extract', group_id_map) | select('defined') | list }}
user_with_groups:
email: "{{ item.email }}"
name: "{{ item.name }}"
role: "{{ item.role | default('user') }}"
auto_groups: "{{ resolved_group_ids }}"
auto_group_names: "{{ final_auto_groups }}"
battalion: "{{ item.battalion | default(none) }}"
skip: "{{ item.email in existing_user_emails }}"
loop: "{{ netbird_users | default([]) }}"
# =========================================================================
# Display Plan
# =========================================================================
- name: Count users to process
ansible.builtin.set_fact:
users_to_create: "{{ resolved_users | default([]) | rejectattr('skip') | list }}"
users_to_skip: "{{ resolved_users | default([]) | selectattr('skip') | list }}"
- name: Display provisioning plan
ansible.builtin.debug:
msg: |
============================================
User Provisioning Plan
============================================
Mode: {{ 'DRY RUN' if dry_run else 'EXECUTE' }}
Users to CREATE ({{ users_to_create | length }}):
{% for user in users_to_create %}
- {{ user.email }}
Name: {{ user.name }}
Role: {{ user.role }}
Groups: {{ user.auto_group_names | join(', ') or 'None' }}
{% endfor %}
{% if users_to_create | length == 0 %}
(none)
{% endif %}
Users to SKIP - already exist ({{ users_to_skip | length }}):
{% for user in users_to_skip %}
- {{ user.email }}
{% endfor %}
{% if users_to_skip | length == 0 %}
(none)
{% endif %}
============================================
- name: End play in dry run mode
ansible.builtin.meta: end_play
when: dry_run | bool
- name: End play if no users to create
ansible.builtin.meta: end_play
when: users_to_create | length == 0
# =========================================================================
# Create Users
# =========================================================================
- name: Create credentials directory
ansible.builtin.file:
path: "{{ playbook_dir }}/files/credentials"
state: directory
mode: "0700"
delegate_to: localhost
- name: Create new users
ansible.builtin.uri:
url: "{{ netbird_api_url }}/users"
method: POST
headers:
Authorization: "Token {{ vault_netbird_service_pat }}"
Content-Type: "application/json"
Accept: "application/json"
body_format: json
body:
email: "{{ item.email }}"
name: "{{ item.name }}"
role: "{{ item.role }}"
auto_groups: "{{ item.auto_groups }}"
is_service_user: false
validate_certs: false
status_code: [200, 201]
loop: "{{ users_to_create }}"
register: created_users
delegate_to: localhost
# =========================================================================
# Store Credentials
# =========================================================================
- name: Build credentials list
ansible.builtin.set_fact:
user_credentials: "{{ user_credentials | default([]) + [credential] }}"
vars:
matching_user: "{{ resolved_users | selectattr('email', 'equalto', item.json.email) | first }}"
credential:
email: "{{ item.json.email }}"
name: "{{ item.json.name }}"
password: "{{ item.json.password | default('N/A') }}"
user_id: "{{ item.json.id }}"
role: "{{ item.json.role }}"
created_at: "{{ ansible_date_time.iso8601 }}"
groups: "{{ matching_user.auto_group_names | default([]) }}"
loop: "{{ created_users.results }}"
when: item.json is defined
- name: Save credentials to file
ansible.builtin.copy:
content: |
---
# =============================================================================
# NetBird User Credentials
# =============================================================================
# Generated: {{ ansible_date_time.iso8601 }}
# Instance: {{ netbird_domain }}
#
# WARNING: Store securely! Passwords cannot be retrieved again.
# =============================================================================
users:
{% for user in user_credentials %}
- email: "{{ user.email }}"
name: "{{ user.name }}"
password: "{{ user.password }}"
user_id: "{{ user.user_id }}"
role: "{{ user.role }}"
groups:
{% for group in user.groups %}
- "{{ group }}"
{% endfor %}
{% if user.groups | length == 0 %}
[]
{% endif %}
{% endfor %}
dest: "{{ playbook_dir }}/files/credentials/users-{{ ansible_date_time.date }}.yml"
mode: "0600"
delegate_to: localhost
when:
- user_credentials is defined
- user_credentials | length > 0
# =========================================================================
# Display Summary
# =========================================================================
- name: Display provisioning summary
ansible.builtin.debug:
msg: |
============================================
User Provisioning Complete!
============================================
Created Users ({{ user_credentials | default([]) | length }}):
{% for user in user_credentials | default([]) %}
{{ user.email }}
Password: {{ user.password }}
Role: {{ user.role }}
Groups: {{ user.groups | join(', ') or 'None' }}
{% endfor %}
Credentials saved to:
{{ playbook_dir }}/files/credentials/users-{{ ansible_date_time.date }}.yml
IMPORTANT:
1. Share passwords securely with users
2. Encrypt or delete credentials file after distribution:
ansible-vault encrypt files/credentials/users-{{ ansible_date_time.date }}.yml
3. Users should change passwords on first login
Login URL: https://{{ netbird_api_host }}
============================================
when: user_credentials is defined