develop
Dave S. 2 years ago
commit 101fc6e791
  1. 68
      all.yml
  2. 13
      ansible.cfg
  3. 36
      group_vars/all.yml
  4. 126
      group_vars/infra.yml
  5. 10
      group_vars/windows.yml
  6. 36
      hosts
  7. 138
      mappings.yml
  8. 44
      roles/acme-dns/defaults/main.yml
  9. 5
      roles/acme-dns/handlers/main.yml
  10. 113
      roles/acme-dns/tasks/main.yml
  11. 26
      roles/acme-dns/templates/config.j2
  12. 18
      roles/acme-dns/templates/init.j2
  13. 8
      roles/acme-dns/templates/nginx_server.j2
  14. 2
      roles/acme/defaults/main.yml
  15. 202
      roles/acme/tasks/main.yml
  16. 49
      roles/acme/templates/renewal.j2
  17. 1
      roles/ansible/defaults/main.yml
  18. 31
      roles/ansible/tasks/main.yml
  19. 620
      roles/asterisk/defaults/main.yml
  20. 8
      roles/asterisk/handlers/main.yml
  21. 19
      roles/asterisk/tasks/asterisk_handlers.yml
  22. 194
      roles/asterisk/tasks/main.yml
  23. 85
      roles/asterisk/templates/_macros.j2
  24. 3
      roles/asterisk/templates/config.j2
  25. 75
      roles/asterisk/templates/custom_pjsip.j2
  26. 26
      roles/asterisk/templates/custom_queues.j2
  27. 58
      roles/asterisk/templates/ext_ivr.j2
  28. 88
      roles/asterisk/templates/ext_utils.j2
  29. 303
      roles/asterisk/templates/extensions.j2
  30. 5
      roles/backup/tasks/add.yml
  31. 8
      roles/backup/tasks/main.yml
  32. 31
      roles/backup/tasks/setup.yml
  33. 52
      roles/blocky/defaults/main.yml
  34. 4
      roles/blocky/handlers/main.yml
  35. 185
      roles/blocky/tasks/main.yml
  36. 7
      roles/blocky/templates/blocky.j2
  37. 19
      roles/blocky/templates/init.j2
  38. 16
      roles/blocky/templates/nginx_server.j2
  39. 4
      roles/blocky/vars/disable_ipv6.yml
  40. 7
      roles/blocky/vars/internal.yml
  41. 4
      roles/blocky/vars/tls.yml
  42. 27
      roles/ca/defaults/main.yml
  43. 227
      roles/ca/tasks/add_cert.yml
  44. 44
      roles/ca/tasks/add_root.yml
  45. 18
      roles/ca/tasks/check_acme.yml
  46. 86
      roles/ca/tasks/gen_acme.yml
  47. 7
      roles/ca/tasks/gen_acme_include.yml
  48. 74
      roles/ca/tasks/gen_dhparam.yml
  49. 154
      roles/ca/tasks/install.yml
  50. 39
      roles/ca/tasks/install_acme.yml
  51. 51
      roles/ca/tasks/main.yml
  52. 17
      roles/ca/tasks/prepare_item.yml
  53. 24
      roles/cdr/defaults/main.yml
  54. 4
      roles/cdr/handlers/main.yml
  55. 141
      roles/cdr/tasks/main.yml
  56. 3
      roles/cdr/templates/env.j2
  57. 14
      roles/cdr/templates/init.j2
  58. 11
      roles/cdr/templates/nginx_server.j2
  59. 24
      roles/certs/tasks/acme_dns.yml
  60. 46
      roles/certs/tasks/external_ns.yml
  61. 2
      roles/certs/tasks/internal_ca.yml
  62. 41
      roles/certs/tasks/main.yml
  63. 46
      roles/certs/tasks/validate.yml
  64. 72
      roles/clamav/defaults/main.yml
  65. 16
      roles/clamav/handlers/main.yml
  66. 97
      roles/clamav/tasks/main.yml
  67. 16
      roles/clamav/templates/config.j2
  68. 14
      roles/clamav/templates/milter_init.j2
  69. 1
      roles/common/defaults/main.yml
  70. 19
      roles/common/files/dropbear_init
  71. 16
      roles/common/handlers/main.yml
  72. 94
      roles/common/tasks/alpine.yml
  73. 52
      roles/common/tasks/debian.yml
  74. 147
      roles/common/tasks/main.yml
  75. 7
      roles/container/defaults/main.yml
  76. 186
      roles/container/tasks/main.yml
  77. 66
      roles/container/tasks/preconf.yml
  78. 9
      roles/coredns/defaults/main.yml
  79. 4
      roles/coredns/handlers/main.yml
  80. 119
      roles/coredns/tasks/add_record.yml
  81. 21
      roles/coredns/tasks/add_records.yml
  82. 47
      roles/coredns/tasks/increase_serial.yml
  83. 93
      roles/coredns/tasks/install.yml
  84. 28
      roles/coredns/tasks/install_tls.yml
  85. 13
      roles/coredns/tasks/main.yml
  86. 15
      roles/coredns/templates/corefile.j2
  87. 14
      roles/coredns/templates/init.j2
  88. 9
      roles/coredns/templates/tls.j2
  89. 32
      roles/coredns/templates/zone.j2
  90. 1
      roles/crl/defaults/main.yml
  91. 41
      roles/crl/tasks/main.yml
  92. 4
      roles/crl/templates/nginx_crl.j2
  93. 290
      roles/dovecot/defaults/main.yml
  94. 4
      roles/dovecot/handlers/main.yml
  95. 241
      roles/dovecot/tasks/main.yml
  96. 8
      roles/dovecot/templates/dovecot-acl.j2
  97. 22
      roles/dovecot/templates/dovecot-dict-sql.j2
  98. 47
      roles/dovecot/templates/dovecot-sql.j2
  99. 3
      roles/dovecot/templates/dovecot-trash.j2
  100. 94
      roles/dovecot/templates/dovecot.j2
  101. Some files were not shown because too many files have changed in this diff Show More

@ -0,0 +1,68 @@
- hosts: all
gather_facts: no
serial: "{{ hosts_per_batch | d(1) | int }}"
strategy: "{{ hosts_strategy | d('linear') }}"
tasks:
- name: get primary role
set_fact:
host_primary_role: "{%- if primary_role is defined -%}{{ primary_role }}\
{%- elif hostvars[inventory_hostname]['primary_role'] is defined -%}{{ hostvars[inventory_hostname]['primary_role'] }}\
{%- else -%}{{ inventory_hostname }}\
{%- endif -%}"
- name: import role mappings
import_tasks: mappings.yml
- name: fail if mappings are missing
fail:
msg: role mappings are missing or invalid
when: (common_roles is not defined) or (common_roles | type_debug != 'list')
- name: warn if current role mapping is missing
debug:
msg: "mapping for role \"{{ host_primary_role }}\" is missing - using default role mapping at stage 6"
when: (extra_roles | d({}))[host_primary_role] is not defined
- name: build role mapping
set_fact:
role_mapping: "{{ (((extra_roles | d({}))[host_primary_role] | d([{ 'stage': 6, 'role': host_primary_role }])) +
([] if host_primary_role in (no_common_roles | d([])) else common_roles) +
([{ 'stage': 1, 'role': 'container' }] if 'containers' in group_names else []) +
([{ 'stage': 3, 'role': 'postgres', 'function': 'integrate' }] if host_primary_role in (database_roles | d([])) else [])
) | sort(attribute='stage') }}"
- name: remember selected stages
set_fact:
selected_stages: "{%- if stage is defined and ((stage | string) is search(',')) -%}{{ stage | string | split(',') | list | map('int') | list }}\
{%- elif (stage is not defined) or ((stage | int) == 0) -%}{{ [1,2,3,4,5,6,7,8,9] }}\
{%- else -%}{{ [stage | int] }}\
{%- endif -%}"
no_log: yes
- name: show deployment info
debug:
msg: "deploying primary role \"{{ host_primary_role }}\" on host \"{{ inventory_hostname }}\", {{
(('stages ' if (selected_stages | length > 1) else 'stage ') ~ (selected_stages | join(', ')))
if ([1,2,3,4,5,6,7,8,9] | symmetric_difference(selected_stages) | list | length > 0) else 'all stages' }}"
- name: run pre_tasks
include_tasks: tasks/pre_tasks.yml
- name: run stages
include_tasks: tasks/includes/stage.yml
loop: "{{ selected_stages }}"
loop_control:
loop_var: this_stage
- name: show deployment info
debug:
msg: "ok: deployment completed on host \"{{ inventory_hostname }}\""

@ -0,0 +1,13 @@
[defaults]
interpreter_python = auto_silent
stdout_callback = debug
use_persistent_connections = true
forks = 6
internal_poll_interval = 0.01
jinja2_native = true
[ssh_connection]
pipelining = true
transfer_method = scp
scp_if_ssh = smart
ssh_args = -C -o ControlMaster=auto -o ControlPersist=60s -o PreferredAuthentications=publickey,password

@ -0,0 +1,36 @@
ansible_user: root
ansible_dir: /etc/ansible
ansible_key_dir: keys
alpine_version: "3.16"
mac_prefix: 02:FF
default_container_hardware:
cores: 1
cpus: 1
cpuunits: 1024
memory: 128
swap: 128
disk: 0.4
known_external_ca:
- url: letsencrypt.org
wildcard: no
validation_methods:
- dns-01
- url: ';'
wildcard: yes
bogons:
- 0.0.0.0/8
- 127.0.0.0/8
- 169.254.0.0/16
- 192.0.0.0/24
- 192.0.2.0/24
- 198.18.0.0/15
- 198.51.100.0/24
- 203.0.113.0/24
- 240.0.0.0/4
services: {}
mail_server: {}

@ -0,0 +1,126 @@
timezone: Europe/Kirov
org: Organization Name
org_localized: Название организации
tld: org.local
int_net: 10.0.0.0/8
int_tld: "corp.{{ tld }}"
maintainer_email: "admin@{{ tld }}"
timezone_win: Russian Standard Time
container_default_nameserver: 10.40.0.1
networks:
srv:
gw: 10.41.0.1/16
tag: 11
priv:
gw: 10.42.0.1/16
tag: 12
dmz:
gw: 10.43.0.1/16
tag: 13
services:
db:
hostname: postgres
vault:
hostname: vault
backup:
hostname: rest-server
port: 443
internal_ns:
hostname: ns
recursive_ns:
hostname: ns-rec
filtering_ns:
- hostname: blocky1
- hostname: blocky2
acme_dns:
hostname: acme-dns
rest_server:
hostname: rest-server
mariadb:
hostname: mariadb
smb:
hostname: smb
use_alternative_apk_repo: yes
mail_server:
tld: "{{ tld }}"
max_mail_size_bytes: 75000000
admin_email: "admin@{{ tld }}"
db_server_hostname: postgres
db_name: mail
db_user: mail
db_pass: pass
mta_hostname: postfix
mua_hostname: dovecot
rspamd_hostname: rspamd
webmail_hostname: mail
clamav_hostname: clamav
mua_lmtp_port: 11001
mua_quota_port: 11002
mua_auth_port: 11003
mua_managesieve_port: 4190
rspamd_port: 11332
mta_sts_port: 11000
clamav_port: 7357
mta_actual_hostname: smtp
mua_actual_hostname: imap
allowed_spf:
- 1.1.1.1
domains:
- "{{ tld }}"
aliases:
- { source: 'postmaster', source_domain: "{{ tld }}", target: 'admin', target_domain: "{{ tld }}" }
- { source: 'hostmaster', source_domain: "{{ tld }}", target: 'admin', target_domain: "{{ tld }}" }
- { source: 'webmaster', source_domain: "{{ tld }}", target: 'admin', target_domain: "{{ tld }}" }
- { source: 'abuse', source_domain: "{{ tld }}", target: 'admin', target_domain: "{{ tld }}" }
- { source: 'caa-report', source_domain: "{{ tld }}", target: 'admin', target_domain: "{{ tld }}" }
- { source: 'dkim-report', source_domain: "{{ tld }}", target: 'admin', target_domain: "{{ tld }}" }
- { source: 'dmarc-report', source_domain: "{{ tld }}", target: 'admin', target_domain: "{{ tld }}" }
- { source: 'smtp-tls-report', source_domain: "{{ tld }}", target: 'admin', target_domain: "{{ tld }}" }
acme_preferred_chain: ISRG Root X1
winrm_remote_user: remote-admin
winrm_bootstrap_password: bootstrap123
backup_filters:
none:
- "*"
- "!*/"
office:
- "!*.doc"
- "!*.docx"
- "!*.xls"
- "!*.xlsx"
- "!*.ppt"
- "!*.pptx"
- "!*.txt"
- "!*.ods"
- "!*.odt"
- "!*.odp"
- "!*.pdf"
images:
- "!*.jpg"
- "!*.jpeg"
- "!*.png"
- "!*.tiff"

@ -0,0 +1,10 @@
is_windows: true
ansible_connection: winrm
ansible_user: "{{ winrm_remote_user }}"
ansible_winrm_transport: credssp
ansible_winrm_scheme: http
ansible_port: 5985
primary_role: workstation

36
hosts

@ -0,0 +1,36 @@
all:
children:
containers:
hosts:
ansible:
ansible_host: 10.0.0.3
ansible_ssh_private_key_file: /etc/ansible/keys/ansible
container_password: ---
container_id: 100
container_network: srv
database: {user: 'test', name: 'test', pass: 'test'}
nodes:
hosts:
node1:
ansible_host: 10.0.0.2
ansible_password: ---
ansible_ssh_extra_args: -o StrictHostKeyChecking=no
external_ipv4: 1.1.1.1
primary_role: proxmox
container_mtu: 1390
windows:
children:
workstations:
infra:
vars:
ansible_group_priority: 1000
children:
containers:
nodes:
windows:

@ -0,0 +1,138 @@
- name: define role list
set_fact:
# common roles for all primary roles
common_roles:
- {stage: 2, role: 'common'}
- {stage: 3, role: 'ns', function: 'add_records'}
- {stage: 5, role: 'mail-user'}
- {stage: 8, role: 'iptables'}
- {stage: 9, role: 'backup', function: 'setup'}
# these primary roles do not inherit common roles
no_common_roles:
- mikrotik
- workstation
# these primary roles will always inherit postgres integration
database_roles:
- acme-dns
- asterisk
- gitea
- roundcube
- shop
- wikijs
- vault
# additional roles for specific primary roles
extra_roles:
ca:
- {stage: 2, role: 'ca', function: 'install'}
coredns:
- {stage: 2, role: 'coredns', function: 'install'}
- {stage: 4, role: 'coredns', function: 'install_tls'}
mariadb:
- {stage: 4, role: 'mariadb', function: 'install'}
mikrotik:
- {stage: 3, role: 'ns', function: 'add_records'}
- {stage: 5, role: 'mikrotik'}
nsd:
- {stage: 4, role: 'nsd', function: 'install'}
- {stage: 4, role: 'nsd', function: 'populate'}
- {stage: 5, role: 'nsd', function: 'install_dnssec'}
- {stage: 5, role: 'nsd', function: 'install_tls'}
postfix:
- {stage: 3, role: 'mail-db'}
- {stage: 4, role: 'postfix'}
postgres:
- {stage: 2, role: 'postgres', function: 'install'}
- {stage: 3, role: 'postgres', function: 'install_tls'}
powerdns:
- {stage: 2, role: 'postgres', function: 'integrate'}
- {stage: 2, role: 'powerdns', function: 'install'}
- {stage: 3, role: 'ca', function: 'certs'}
proxmox:
- {stage: 1, role: 'common'}
- {stage: 1, role: 'proxmox', function: 'install'}
- {stage: 5, role: 'mail-user'}
- {stage: 5, role: 'proxmox', function: 'tls'}
- {stage: 6, role: 'proxmox', function: 'mail'}
rest-server:
- {stage: 6, role: 'rest-server', function: 'install'}
workstation:
- {stage: 3, role: 'ns', function: 'add_records'}
- {stage: 5, role: 'workstation'}
# recommended hardware parameters for each primary role
role_hardware:
acme-dns: {cores: 2, memory: 96, swap: 64, disk: 0.15}
ansible: {cores: 4, memory: 256, swap: 384, disk: 1.5}
asterisk: {cores: 4, memory: 192, swap: 96, disk: 0.6, cpuunits: 2048}
blocky: {cores: 4, memory: 384, swap: 128, disk: 0.15}
ca: {cores: 2, memory: 128, swap: 64, disk: 0.15, cpuunits: 512}
clamav: {cores: 4, memory: 2048, swap: 256, disk: 0.75}
coredns: {cores: 4, memory: 128, swap: 64, disk: 0.15}
crl: {cores: 2, memory: 128, swap: 48, disk: 0.15}
dovecot: {cores: 4, memory: 256, swap: 64, disk: 0.15}
gitea: {cores: 4, memory: 512, swap: 256, disk: 1}
grafana: {cores: 4, memory: 512, swap: 256, disk: 0.4}
mariadb: {cores: 4, memory: 256, swap: 128, disk: 0.4}
mc: {cores: 4, memory: 2048, swap: 512, disk: 0.5}
nsd: {cores: 2, memory: 256, swap: 256, disk: 0.15}
ntp: {cores: 2, memory: 64, swap: 128, disk: 0.15}
postfix: {cores: 4, memory: 256, swap: 48, disk: 0.15}
postgres: {cores: 4, memory: 256, swap: 256, disk: 0.5}
powerdns: {cores: 2, memory: 96, swap: 64, disk: 0.15}
prometheus: {cores: 4, memory: 512, swap: 256, disk: 0.3}
rclone: {cores: 4, memory: 192, swap: 96, disk: 0.2, cpuunits: 768}
rest-server: {cores: 4, memory: 256, swap: 192, disk: 0.2, cpuunits: 512}
roundcube: {cores: 4, memory: 384, swap: 256, disk: 0.5}
rspamd: {cores: 4, memory: 768, swap: 128, disk: 0.3}
seafile: {cores: 4, memory: 1024, swap: 1024, disk: 5}
shop: {cores: 4, memory: 192, swap: 128, disk: 0.4}
smb: {cores: 2, memory: 128, swap: 64, disk: 0.15}
strongswan: {cores: 4, memory: 128, swap: 48, disk: 0.15}
unbound: {cores: 2, memory: 128, swap: 64, disk: 0.15}
uptime-kuma: {cores: 4, memory: 384, swap: 128, disk: 0.5}
vault: {cores: 4, memory: 128, swap: 64, disk: 0.3}
web: {cores: 4, memory: 128, swap: 64, disk: 0.2}
wikijs: {cores: 4, memory: 256, swap: 256, disk: 0.75}
# role dependency table
# 0 - DNS ok
# 1 - DB ok
role_dependency:
acme-dns: 0
ansible: 0
asterisk: 2
blocky: 0
ca: 0
clamav: 1
coredns: 0
crl: 1
dovecot: 2
gitea: 2
grafana: 2
mariadb: 0
mc: 3
nsd: 0
ntp: 0
postfix: 2
postgres: 0
powerdns: 1
prometheus: 1
rclone: 1
rest-server: 0
roundcube: 2
rspamd: 2
seafile: 3
shop: 2
smb: 1
strongswan: 1
unbound: 0
uptime-kuma: 3
vault: 2
web: 1
wikijs: 3
run_once: yes
no_log: yes

@ -0,0 +1,44 @@
acme_dns_user: acmedns
acme_dns_group: acmedns
acme_dns_dir: /opt/acmedns
acme_dns_tld: "acme-dns.{{ acme_tld | d(tld) }}"
acme_dns_ns: "ns.acme-dns.{{ acme_tld | d(tld) }}"
acme_dns_admin: "{{ maintainer_email | d('admin@' ~ (acme_tld | d(tld))) }}"
acme_dns_api_port: 8080
acme_dns_default_config:
general:
listen: ":53"
protocol: both4
domain: "{{ acme_dns_tld }}"
nsname: "{{ acme_dns_ns | d(acme_dns_tld) }}"
nsadmin: "{{ acme_dns_admin | replace('@', '.') }}"
records:
- "{{ acme_dns_tld ~ '. A ' ~ acme_dns_external_ipv4 }}"
- "{{ (acme_dns_ns | d(acme_dns_tld)) ~ '. A ' ~ acme_dns_external_ipv4 }}"
- "{{ acme_dns_tld ~ '. NS ' ~ (acme_dns_ns | d(acme_dns_tld)) ~ '.' }}"
database:
engine: postgres
connection: "{{ 'postgresql://' ~ database_user ~ ':' ~ database_pass ~ '@' ~ database_host ~ '/' ~ database_name ~ '?sslmode=disable' }}"
api:
ip: "0.0.0.0"
autocert_port: 80
port: "{{ acme_dns_api_port }}"
disable_registration: no
tls: none
use_header: no
notification_email: "{{ letsencrypt_email | d(maintainer_email) }}"
corsorigins:
- "*"
logconfig:
loglevel: debug
logtype: stdout
logformat: text

@ -0,0 +1,5 @@
- name: restart acme-dns
service:
name: acme-dns
state: restarted

@ -0,0 +1,113 @@
- name: set acme_dns_cfg
set_fact:
acme_dns_cfg: "{{ acme_dns_default_config | d({}) | combine(acme_dns_config | d({}), recursive=true) }}"
- name: install dependencies
include_tasks: tasks/install_packages.yml
vars:
package:
- libcap
- name: create user and group
include_tasks: tasks/create_user.yml
vars:
user:
name: "{{ acme_dns_user }}"
group: "{{ acme_dns_group }}"
dir: "{{ acme_dns_dir }}"
- name: get and extract latest version of acme-dns
include_tasks: tasks/get_lastversion.yml
vars:
package:
name: fritterhoff/acme-dns
location: github
assets: yes
asset_filter: 'Linux_amd64.tar.gz$'
file: "{{ acme_dns_dir }}/last_version"
extract: "{{ acme_dns_dir }}"
user: "{{ acme_dns_user }}"
group: "{{ acme_dns_group }}"
notify: restart acme-dns
- name: delete unnecessary files
file:
path: "{{ acme_dns_dir }}/{{ item }}"
state: absent
loop:
- CHANGELOG.md
- LICENSE
- README.md
- name: template acme-dns config
template:
src: config.j2
dest: "{{ acme_dns_dir }}/config.cfg"
force: yes
mode: 0400
owner: "{{ acme_dns_user }}"
group: "{{ acme_dns_group }}"
lstrip_blocks: yes
notify: restart acme-dns
- name: template init script
template:
src: init.j2
dest: /etc/init.d/acme-dns
force: yes
mode: "+x"
notify: restart acme-dns
- name: ensure acme-dns binary has executable bit set
file:
path: "{{ acme_dns_dir }}/acme-dns"
mode: "+x"
- name: add cap_net_bind_service to acme-dns executable
community.general.capabilities:
path: "{{ acme_dns_dir }}/acme-dns"
capability: cap_net_bind_service+ep
changed_when: no
- name: set acme server address
set_fact:
acme_server: "http://127.0.0.1:{{ acme_dns_api_port }}"
- name: install and configure nginx
include_role:
name: nginx
vars:
nginx:
servers:
- conf: nginx_server
certs: "{{ host_tls }}"
- name: flush handlers
meta: flush_handlers
- name: add directories to backup plan
include_role:
name: backup
vars:
function: add
backup_items:
- "{{ acme_dns_dir }}"
- name: enable and start acme-dns
service:
name: acme-dns
state: started
enabled: yes

@ -0,0 +1,26 @@
{% macro acme_dns_option(option) -%}
{% if option.value is boolean -%}
{{ option.key }} = {{ 'true' if option.value else 'false' }}
{% elif option.value | type_debug == 'list' -%}
{{ option.key }} = [
{%- for s in option.value -%}
"{{- s -}}",
{%- endfor -%}
]
{% elif option.value != None -%}
{{ option.key }} = "{{ option.value }}"
{% endif -%}
{% endmacro -%}
{% for section in (acme_dns_cfg | d({}) | dict2items) -%}
[{{ section.key | lower }}]
{% for option in (section.value | d({}) | dict2items) -%}
{{ acme_dns_option(option) -}}
{% endfor %}
{%- if not loop.last %}
{% endif -%}
{% endfor %}

@ -0,0 +1,18 @@
#!/sbin/openrc-run
name="$SVCNAME"
command="{{ acme_dns_dir }}/$SVCNAME"
directory="{{ acme_dns_dir }}"
command_user="{{ acme_dns_user }}:{{ acme_dns_group }}"
pidfile="/var/run/$SVCNAME.pid"
command_background=true
start_stop_daemon_args="--stdout-logger logger --stderr-logger logger"
depend() {
need net
use dns
}
start_pre() {
setcap 'cap_net_bind_service=+ep' {{ acme_dns_dir }}/$SVCNAME
}

@ -0,0 +1,8 @@
location / {
proxy_pass http://127.0.0.1:{{ acme_dns_api_port }};
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}

@ -0,0 +1,2 @@
acme_directory: /etc/letsencrypt
acme_max_log_backups: 5

@ -0,0 +1,202 @@
- name: install certbot
include_tasks: tasks/install_packages.yml
vars:
package: certbot
- name: create certbot directories
file:
path: "{{ item }}"
state: directory
loop:
- "{{ acme_directory }}"
- "{{ acme_directory }}/cron"
- name: change certbot directory permissions
file:
path: "{{ acme_directory ~ '/' ~ item }}"
state: directory
mode: "g+rx,o+rx"
loop:
- archive
- live
- name: check if acme-dns auth hook already exists
stat:
path: "{{ acme_directory }}/acme-dns-auth.py"
register: result
- name: download acme-dns auth hook
get_url:
url: "https://raw.githubusercontent.com/RangeForce/acme-dns-certbot-joohoi/master/acme-dns-auth.py"
dest: "{{ acme_directory }}/acme-dns-auth.py"
force: no
mode: "+x"
when: result.stat.exists == false
- name: update python interpreter in acme-dns-auth to python3
lineinfile:
path: "{{ acme_directory }}/acme-dns-auth.py"
regexp: '^#!\/usr\/bin\/env python\s*$'
line: '#!/usr/bin/env python3'
- name: clear acme fqdn list
set_fact:
acme_domain_list: "{{ [] }}"
- name: build acme fqdn list
set_fact:
acme_domain_list: "{{ (acme_domain_list | d([])) +
([item.fqdn | d((item.hostname | d(host_name)) ~ '.' ~ (item.tld | d(host_tld)))] if item is mapping else
[item]) }}"
loop: "{{ acme_hosts if (acme_hosts | type_debug == 'list') else [] }}"
- name: build single acme fqdn
set_fact:
acme_domain_list: "{%- if acme_fqdn is defined and acme_fqdn != None -%}\
{{ [ acme_fqdn ] }}\
{%- elif (acme_hostname is defined and acme_hostname != None) or (acme_tld is defined and acme_tld != None) -%}\
{{ [((acme_hostname is defined and acme_hostname != None) | ternary(acme_hostname, host_name)) ~ '.' ~
((acme_tld is defined and acme_tld != None) | ternary(acme_tld, host_tld))] }}\
{%- else -%}\
{{ [ host_fqdn ] }}\
{%- endif -%}"
when: (acme_hosts is not defined) or (acme_hosts | type_debug != 'list')
- name: set acme parameters
set_fact:
acme_cert_name: "{{ acme_id if (acme_id is defined) and (acme_id != None) else (host_name ~ ('-ecc' if (acme_ecc | d(false) == true) else '')) }}"
acme_target_server: "{%- if (acme_server is defined) and (acme_server != None) -%}\
{{ acme_server }}\
{%- else -%}\
{{ (services.acme_dns.protocol | d('https')) ~ '://' ~ services.acme_dns.hostname ~ '.' ~ (services.acme_dns.tld | d(int_tld)) ~
((':' ~ services.acme_dns.port) if services.acme_dns.port is defined else '') }}\
{%- endif -%}"
- name: set certbot parameters
set_fact:
acme_params: "{{ ['--manual',
'--manual-auth-hook ' ~ ((acme_directory ~ '/acme-dns-auth.py') | quote),
'--preferred-challenges dns',
'--debug-challenges',
('--key-type ecdsa' if (acme_ecc | d(false) == true) else ''),
('--staging' if (acme_staging | d(false) == true) else ''),
('--force-renewal' if (acme_force | d(false) == true) else ''),
('--must-staple' if (acme_stapling | d(false) == true) else ''),
'--cert-name ' ~ (acme_cert_name | quote),
'--non-interactive',
'--agree-tos',
'--email ' ~ ((acme_email | d(maintainer_email)) | quote),
'--no-eff-email',
(('--preferred-chain ' ~ (acme_preferred_chain | quote)) if acme_preferred_chain is defined else ''),
'--max-log-backups ' ~ (acme_max_log_backups | quote)
] | select() | list | join(' ') }}"
- block:
- name: issue cert with dns mode
shell:
cmd: "certbot certonly {{ acme_params }} -d {{ acme_domain_list | map('quote') | join(' -d ') }}"
chdir: /usr/bin
environment:
ACMEDNS_URL: "{{ acme_target_server }}"
register: result
changed_when: ('Successfully received certificate' in result.stdout)
notify: "{{ acme_notify if (acme_notify is defined) and (acme_notify != None) else omit }}"
rescue:
- name: wait for user interaction (CNAME record must be set manually)
pause:
prompt: "{{ result.stdout }}"
- name: try again to issue cert with dns mode
shell:
cmd: "certbot certonly {{ acme_params }} -d {{ acme_domain_list | map('quote') | join(' -d ') }}"
chdir: /usr/bin
environment:
ACMEDNS_URL: "{{ acme_target_server }}"
register: result
changed_when: ('Successfully received certificate' in result.stdout)
notify: "{{ acme_notify if (acme_notify is defined) and (acme_notify != None) else omit }}"
- name: create symlinks
file:
path: "{{ item.dest }}"
src: "{{ acme_directory ~ '/live/' ~ acme_cert_name ~ '/' ~ item.src }}"
state: link
force: yes
when: (item.dest is string) and (item.dest | length > 0) and (acme_use_symlinks | d(true) == true)
loop:
- { src: 'fullchain.pem', dest: "{{ acme_cert | d(None) }}" }
- { src: 'privkey.pem', dest: "{{ acme_key | d(None) }}" }
- { src: 'cert.pem', dest: "{{ acme_cert_single | d(None) }}" }
- { src: 'chain.pem', dest: "{{ acme_chain | d(None) }}" }
notify: "{{ acme_notify if (acme_notify is defined) and (acme_notify != None) else omit }}"
- name: fix ownership on archive dir
file:
path: "{{ acme_directory ~ '/archive/' ~ acme_cert_name }}"
follow: no
recurse: yes
owner: "{{ acme_owner if (acme_owner is defined) and (acme_owner != None) else omit }}"
group: "{{ acme_group if (acme_group is defined) and (acme_group != None) else omit }}"
- name: copy certs
copy:
src: "{{ acme_directory ~ '/live/' ~ acme_cert_name ~ '/' ~ item.src }}"
dest: "{{ item.dest }}"
remote_src: yes
mode: 0600
owner: "{{ acme_owner if (acme_owner is defined) and (acme_owner != None) else omit }}"
group: "{{ acme_group if (acme_group is defined) and (acme_group != None) else omit }}"
when: (item.dest is string) and (item.dest | length > 0) and (acme_use_symlinks | d(true) == false)
loop:
- { src: 'fullchain.pem', dest: "{{ acme_cert | d(None) }}" }
- { src: 'privkey.pem', dest: "{{ acme_key | d(None) }}" }
- { src: 'cert.pem', dest: "{{ acme_cert_single | d(None) }}" }
- { src: 'chain.pem', dest: "{{ acme_chain | d(None) }}" }
notify: "{{ acme_notify | d(omit) }}"
- name: edit renewal file
lineinfile:
path: "{{ acme_directory ~ '/renewal/' ~ acme_cert_name ~ '.conf' }}"
regexp: '^{{ item.name | regex_escape }}(\s+)='
line: '{{ item.name }} = {{ item.value }}'
insertafter: '^\[renewalparams\]'
create: no
firstmatch: yes
when: (item.value is string) and (item.value | length > 0) and
((item.extra_condition is not defined) or (item.extra_condition | d(true)))
loop:
- { name: 'renew_hook', value: "{{ acme_directory ~ '/cron/' ~ acme_cert_name ~ '.sh' }}" }
- name: create custom renewal hook file
template:
src: renewal.j2
dest: "{{ acme_directory ~ '/cron/' ~ acme_cert_name ~ '.sh' }}"
force: yes
mode: 0500
lstrip_blocks: yes
- name: add certbot to crontab
cron:
name: "certbot renewal ({{ acme_cert_name ~ ' on ' ~ acme_target_server }})"
job: "ACMEDNS_URL={{ acme_target_server | quote }} \
/usr/bin/certbot renew --cert-name {{ acme_cert_name | quote }} --max-log-backups {{ acme_max_log_backups | quote }}"
hour: "{{ 4 | random(start=1, seed=(host_name ~ acme_cert_name)) }}"
minute: "{{ 59 | random(seed=(host_name ~ acme_cert_name)) }}"

@ -0,0 +1,49 @@
#!/bin/sh
{% if (acme_owner is string) and (acme_group is string) and (acme_owner | length > 0) and (acme_group | length > 0) and (acme_use_symlinks | d(true) == true) -%}
chown -R {{ acme_owner ~ ':' ~ acme_group }} {{ (acme_directory ~ '/archive/' ~ acme_cert_name ~ '/') | quote }}
{% endif -%}
{{ acme_before_copy_hook | d('') }}
{% if (acme_cert is string) and (acme_cert | length > 0) and (acme_use_symlinks | d(true) == false) -%}
cp -fpT {{ (acme_directory ~ '/live/' ~ acme_cert_name ~ '/fullchain.pem') | quote }} {{ acme_cert | quote }}
{% if (acme_owner is not string) and (acme_group is string) -%}
chgrp -f {{ acme_group }} {{ acme_cert | quote }}
{% elif acme_owner is defined -%}
chown -f {{ acme_owner ~ ((':' ~ acme_group) if acme_group is string else '') }} {{ acme_cert | quote }}
{% endif -%}
{% endif -%}
{% if (acme_key is string) and (acme_key | length > 0) and (acme_use_symlinks | d(true) == false) -%}
cp -fpT {{ (acme_directory ~ '/live/' ~ acme_cert_name ~ '/privkey.pem') | quote }} {{ acme_key | quote }}
{% if (acme_owner is not string) and (acme_group is string) -%}
chgrp -f {{ acme_group }} {{ acme_key | quote }}
{% elif acme_owner is defined -%}
chown -f {{ acme_owner ~ ((':' ~ acme_group) if acme_group is string else '') }} {{ acme_key | quote }}
{% endif -%}
{% endif -%}
{% if (acme_cert_single is string) and (acme_cert_single | length > 0) and (acme_use_symlinks | d(true) == false) -%}
cp -fpT {{ (acme_directory ~ '/live/' ~ acme_cert_name ~ '/cert.pem') | quote }} {{ acme_cert_single | quote }}
{% if (acme_owner is not string) and (acme_group is string) -%}
chgrp -f {{ acme_group }} {{ acme_cert_single | quote }}
{% elif acme_owner is defined -%}
chown -f {{ acme_owner ~ ((':' ~ acme_group) if acme_group is string else '') }} {{ acme_cert_single | quote }}
{% endif -%}
{% endif -%}
{% if (acme_chain is string) and (acme_chain | length > 0) and (acme_use_symlinks | d(true) == false) -%}
cp -fpT {{ (acme_directory ~ '/live/' ~ acme_cert_name ~ '/chain.pem') | quote }} {{ acme_chain | quote }}
{% if (acme_owner is not string) and (acme_group is string) -%}
chgrp -f {{ acme_group }} {{ acme_chain | quote }}
{% elif acme_owner is defined -%}
chown -f {{ acme_owner ~ ((':' ~ acme_group) if acme_group is string else '') }} {{ acme_chain | quote }}
{% endif -%}
{% endif -%}
{{ (acme_post_hook ~ ' &>/dev/null &') if acme_post_hook is defined else '' }}

@ -0,0 +1 @@
ansible_dir: /etc/ansible

@ -0,0 +1,31 @@
- name: install ansible and dependencies
include_tasks: tasks/install_packages.yml
vars:
package:
- ansible
- py3-lxml
- py3-pip
- py3-requests
- py3-netaddr
- name: install python dependencies
pip:
name:
- pywinrm
- pywinrm[credssp]
- name: create ansible directory
file:
path: "{{ ansible_dir }}"
state: directory
- name: add directories to backup plan
include_role:
name: backup
vars:
function: add
backup_items:
- "{{ ansible_dir }}"

@ -0,0 +1,620 @@
asterisk_user: asterisk
asterisk_group: asterisk
asterisk_dir: /var/lib/asterisk
asterisk_conf_dir: /etc/asterisk
asterisk_tls_dir: "{{ asterisk_conf_dir }}/tls"
asterisk_recordings_dir: /opt/recordings
asterisk_data_dir: "{{ asterisk_dir }}"
asterisk_users: {}
asterisk_trunks: {}
asterisk_language: ru
asterisk_pjsip_ciphers:
- ECDHE-ECDSA-CHACHA20-POLY1305
- ECDHE-ECDSA-AES256-GCM-SHA384
- ECDHE-ECDSA-AES128-GCM-SHA256
- ECDHE-RSA-CHACHA20-POLY1305
- ECDHE-RSA-AES256-GCM-SHA384
- ECDHE-RSA-AES128-GCM-SHA256
- DHE-RSA-AES128-SHA256
# meta definitions:
# __template__ (bool) (section): this section is a template
# __template_from__ (string/list) (section): templates to inherit from
# __comment__ (string) (section): specify a comment before the section definition
# __inner_objects__ (boolean) (config/section): use object syntax when enumerating section members
asterisk_default_config:
acl:
acl_lan_clients:
deny:
- 0.0.0.0/0.0.0.0
permit:
- "{{ int_net | ansible.utils.ipaddr('network') }}/{{ int_net | ansible.utils.ipaddr('netmask') }}"
acl_inet_clients:
deny:
- "{{ int_net | ansible.utils.ipaddr('network') }}/{{ int_net | ansible.utils.ipaddr('netmask') }}"
permit:
- 0.0.0.0/0.0.0.0
asterisk:
directories:
__template__: yes
__inner_objects__: yes
astetcdir: "{{ asterisk_conf_dir }}"
astvarlibdir: "{{ asterisk_dir }}"
astdatadir: "{{ asterisk_data_dir }}"
astdbdir: "{{ asterisk_db_dir | d(asterisk_dir) }}"
astkeydir: "{{ asterisk_key_dir | d(asterisk_dir) }}"
astagidir: "{{ asterisk_agi_dir | d(asterisk_dir ~ '/agi-bin') }}"
astspooldir: "{{ asterisk_spool_dir | d('/var/spool/asterisk') }}"
astrundir: "{{ asterisk_run_dir | d('/var/run/asterisk') }}"
astlogdir: "{{ asterisk_log_dir | d('/var/log/asterisk') }}"
astsbindir: /usr/sbin
astmoddir: /usr/lib/asterisk/modules
options:
verbose: 0
debug: no
trace: 0
execincludes: no
highpriority: yes
initcrypto: yes
nocolor: yes
dumpcore: no
runuser: "{{ asterisk_user }}"
rungroup: "{{ asterisk_group }}"
autosystemname: yes
maxcalls: 200
maxload: "100.0"
minmemfree: 1
languageprefix: yes
transmit_silence: no
defaultlanguage: en
documentation_language: en_US
ccss:
general:
cc_max_requests: 15
cdr:
general:
enable: yes
unanswered: yes
congestion: yes
cel:
general:
enable: no
cdr_pgsql:
global:
hostname: "{{ database_host }}"
port: 5432
user: "{{ database_user | d('cdr') }}"
dbname: "{{ database_name | d('cdr') }}"
table: "{{ database_table | d('cdr') }}"
password: "{{ database_pass }}"
encoding: UNICODE
cli_aliases:
general:
template: friendly
friendly:
"hangup request": channel request hangup
"originate": channel originate
"help": core show help
"pri intense debug span": pri set debug intense span
"reload": module reload
"pjsip reload": module reload res_pjsip.so res_pjsip_authenticator_digest.so res_pjsip_endpoint_identifier_ip.so res_pjsip_mwi.so res_pjsip_notify.so res_pjsip_outbound_publish.so res_pjsip_publish_asterisk.so res_pjsip_outbound_registration.so
cli_permissions:
general:
default_perm: permit
codecs:
plc:
__inner_objects__: yes
genericplc: "true"
genericplc_on_equal_codecs: "false"
opus:
type: opus
packet_loss: 2
signal: voice
confbridge:
default_user:
type: user
dsp_drop_silence: yes
jitterbuffer: yes
default_bridge:
type: bridge
max_members: 30
language: "{{ asterisk_language }}"
features:
__inner_objects__: yes
featuremap:
blindxfer: "**"
atxfer: "*#"
applicationmap:
volume-up-tx: "#1,self/caller,Gosub(volume-up-tx,s,1)"
volume-up-rx: "#2,self/caller,Gosub(volume-up-rx,s,1)"
volume-down-tx: "#3,self/caller,Gosub(volume-down-tx,s,1)"
volume-down-rx: "#4,self/caller,Gosub(volume-down-rx,s,1)"
volume-increase-all: "#5,self/caller,Gosub(volume-increase-all,s,1)"
call-controls:
volume-up-tx: ""
volume-up-rx: ""
volume-down-tx: ""
volume-down-rx: ""
volume-increase-all: ""
followme:
__inner_objects__: yes
general:
featuredigittimeout: 3500
enable_callee_prompt: "true"
takecall: 1
declinecall: 2
call_from_prompt: followme/call-from
norecording_prompt: followme/no-recording
options_prompt: followme/options
pls_hold_prompt: followme/pls-hold-while-try
status_prompt: followme/status
sorry_prompt: followme/sorry
connecting_prompt: ""
default:
musicclass: default
context: default
enable_callee_prompt: "true"
takecall: 1
declinecall: 2
call_from_prompt: followme/call-from
norecording_prompt: followme/no-recording
options_prompt: followme/options
pls_hold_prompt: followme/pls-hold-while-try
status_prompt: followme/status
sorry_prompt: followme/sorry
connecting_prompt: ""
indications:
general:
country: ru
ru:
description: Russian Federation / ex Soviet Union
ringcadence: "1000,4000"
dial: "425"
busy: "425/350,0/350"
ring: "425/1000,0/4000"
congestion: "425/175,0/175"
callwaiting: "425/200,0/5000"
record: "1400/400,0/15000"
info: "950/330,1400/330,1800/330,0/1000"
dialrecall: "425/400,0/40"
stutter: "!425/100,!0/100,!425/100,!0/100,!425/100,!0/100,!425/100,!0/100,!425/100,!0/100,!425/100,!0/100,425"
logger:
general:
queue_log: no
logfiles:
__inner_objects__: yes
console: notice,warning,error,verbose,dtmf
"syslog.local0": "[plain]notice,warning,error"
manager:
general:
enabled: yes
webenabled: no
port: 5038
bindaddr: 0.0.0.0
debug: "off"
allowmultiplelogin: yes
displayconnects: yes
timestampevents: yes
authtimeout: 10
musiconhold:
default:
mode: files
directory: moh
pjproject:
startup:
cache_pools: yes
pjsip:
system:
type: system
threadpool_auto_increment: 3
timer_t1: 250
timer_b: 16000
global:
type: global
max_forwards: 40
keep_alive_interval: 15
user_agent: "{{ org }} Asterisk PBX"
endpoint_identifier_order: username,ip
default_from_user: pbx
default_realm: "{{ host_fqdn }}"
transport-common:
__template__: yes
type: transport
tos: cs3
cos: 3
allow_reload: no
local_net: "{{ int_net | ansible.utils.ipaddr('network') }}/{{ int_net | ansible.utils.ipaddr('netmask') }}"
transport-ext:
__template__: yes
__template_from__: transport-common
external_media_address: "{{ asterisk_external_ipv4 | d(hostvars[selected_node]['external_ipv4']) }}"
external_signaling_address: "{{ asterisk_external_ipv4 | d(hostvars[selected_node]['external_ipv4']) }}"
transport-udp:
__template__: yes
__template_from__: transport-common
protocol: udp
transport-tcp:
__template__: yes
__template_from__: transport-common
protocol: tcp
transport-lan:
__template_from__: transport-udp
bind: 0.0.0.0:5060
transport-lan-tcp:
__template_from__: transport-tcp
bind: 0.0.0.0:5060
transport-lan-tls:
__template_from__: transport-common
protocol: tls
bind: 0.0.0.0:5061
cert_file: "{{ asterisk_tls_dir }}/asterisk.crt"
priv_key_file: "{{ asterisk_tls_dir }}/asterisk.key"
cipher: "{{ asterisk_pjsip_ciphers | join(',') }}"
method: tlsv1_2
require_client_cert: no
verify_client: no
verify_server: no
endpoint-common:
__template__: yes
type: endpoint
allow: "!all,opus,g722,alaw,ulaw,g726,ilbc,gsm"
allow_overlap: no
send_connected_line: yes
trust_connected_line: yes
direct_media: no
dtmf_mode: auto_info
force_rport: yes
ice_support: no
identify_by: username
rewrite_contact: yes
rtp_symmetric: yes
send_diversion: yes
send_history_info: yes
send_pai: no
send_rpid: no
use_ptime: yes
t38_udptl: no
tone_zone: ru
language: ru
tos_audio: ef
cos_audio: 5
rtp_keepalive: 5
rtp_timeout: 360
rtp_timeout_hold: 720
rtcp_mux: yes
max_video_streams: 0
max_audio_streams: 1
bundle: no
sdp_session: "{{ org }} Asterisk PBX"
sdp_owner: PBX
suppress_q850_reason_headers: yes
endpoint-trunk:
__template__: yes
__template_from__: endpoint-common
identify_by: ip,username
trust_id_inbound: yes
acl: acl_inet_clients
contact_acl: acl_inet_clients
endpoint-lan:
__template__: yes
__template_from__: endpoint-common
identify_by: username
trust_id_inbound: no
trust_id_outbound: yes
acl: acl_lan_clients
contact_acl: acl_lan_clients
context: outbound
allow_subscribe: yes
device_state_busy_at: 1
sub_min_expiry: 15
media_encryption: sdes
media_encryption_optimistic: yes
auth-common:
__template__: yes
type: auth
auth_type: userpass
registration-common:
__template__: yes
type: registration
expiration: 1800
auth_rejection_permanent: no
max_retries: 10000
retry_interval: 20
forbidden_retry_interval: 60
fatal_retry_interval: 60
aor-common:
__template__: yes
type: aor
qualify_frequency: 30
max_contacts: 2 # https://asterisk.org/pjsip-mis-configuration-can-cause-loss-sip-registrations
__include__: custom_pjsip.conf
pjsip_notify:
__inner_objects__: yes
clear-mwi:
Event: message-summary
Content-type: application/simple-message-summary
Content:
- "Messages-Waiting: no"
- "Message-Account: sip:asterisk@127.0.0.1"
- "Voice-Message: 0/0 (0/0)"
- ""
polycom-check-cfg:
Event: check-sync
yealink-reboot:
Event: check-sync
queues:
general:
persistentmembers: no
autofill: yes
monitor-type: MixMonitor
updatecdr: yes
log_membername_as_agent: yes
shared_lastcall: yes
queue-template:
__template__: yes
musicclass: default
strategy: ringall
servicelevel: 30
maxlen: 128
timeoutpriority: conf
timeout: 300
wrapuptime: 5
announce-frequency: 0
periodic-announce-frequency: 0
announce-position: no
autopause: yes
autopausedelay: 60
autopausebusy: yes
joinempty: unavailable
leavewhenempty: unavailable
ringinuse: no
queue-single:
__template__: yes
__template_from__: queue-template
weight: 1
autopause: no
context: inbound-queued-inqueue-busy
queue-le:
__template__: yes
__template_from__: queue-template
weight: 1
autopause: no
__include__: custom_queues.conf
queuerules:
general:
rtp:
general:
rtpstart: 15000
rtpend: 19000
strictrtp: yes
icesupport: "false"
udptl:
general:
modules:
modules:
autoload: no
load:
- app_attended_transfer.so
- app_blind_transfer.so
- app_bridgeaddchan.so
- app_bridgewait.so
- app_cdr.so
- app_celgenuserevent.so
- app_chanisavail.so
- app_channelredirect.so
- app_chanspy.so
- app_confbridge.so
- app_controlplayback.so
- app_dial.so
- app_directed_pickup.so
- app_dumpchan.so
- app_echo.so
- app_exec.so
- app_followme.so
- app_forkcdr.so
- app_mixmonitor.so
- app_originate.so
- app_playback.so
- app_queue.so
- app_read.so
- app_readexten.so
- app_senddtmf.so
- app_softhangup.so
- app_stack.so
- app_stream_echo.so
- app_talkdetect.so
- app_transfer.so
- app_verbose.so
- app_waitforring.so
- app_waitforsilence.so
- app_waituntil.so
- app_while.so
- bridge_builtin_features.so
- bridge_builtin_interval_features.so
- bridge_holding.so
- bridge_native_rtp.so
- bridge_simple.so
- bridge_softmix.so
- cdr_pgsql.so
- chan_bridge_media.so
- chan_pjsip.so
- chan_rtp.so
- codec_a_mu.so
- codec_adpcm.so
- codec_alaw.so
- codec_g722.so
- codec_g726.so
- codec_gsm.so
- codec_ilbc.so
- codec_opus_open_source.so
- codec_resample.so
- codec_ulaw.so
- format_g719.so
- format_g723.so
- format_g726.so
- format_gsm.so
- format_ilbc.so
- format_pcm.so
- format_sln.so
- format_vox.so
- format_wav.so
- format_wav_gsm.so
- func_blacklist.so
- func_callcompletion.so
- func_callerid.so
- func_cdr.so
- func_channel.so
- func_config.so
- func_cut.so
- func_devstate.so
- func_dialplan.so
- func_global.so
- func_hangupcause.so
- func_holdintercept.so
- func_jitterbuffer.so
- func_logic.so
- func_module.so
- func_pjsip_aor.so
- func_pjsip_contact.so
- func_pjsip_endpoint.so
- func_rand.so
- func_sorcery.so
- func_strings.so
- func_talkdetect.so
- func_timeout.so
- func_volume.so
- pbx_config.so
- pbx_loopback.so
- pbx_realtime.so
- pbx_spool.so
- res_audiosocket.so
- res_clialiases.so
- res_clioriginate.so
- res_convert.so
- res_crypto.so
- res_format_attr_celt.so
- res_format_attr_g729.so
- res_format_attr_ilbc.so
- res_format_attr_opus.so
- res_format_attr_silk.so
- res_format_attr_siren14.so
- res_format_attr_siren7.so
- res_musiconhold.so
- res_mutestream.so
- res_pjproject.so
- res_pjsip.so
- res_pjsip_acl.so
- res_pjsip_authenticator_digest.so
- res_pjsip_caller_id.so
- res_pjsip_dialog_info_body_generator.so
- res_pjsip_diversion.so
- res_pjsip_dlg_options.so
- res_pjsip_dtmf_info.so
- res_pjsip_empty_info.so
- res_pjsip_endpoint_identifier_ip.so
- res_pjsip_endpoint_identifier_user.so
- res_pjsip_exten_state.so
- res_pjsip_header_funcs.so
- res_pjsip_history.so
- res_pjsip_logger.so
- res_pjsip_messaging.so
- res_pjsip_mwi.so
- res_pjsip_mwi_body_generator.so
- res_pjsip_nat.so
- res_pjsip_notify.so
- res_pjsip_outbound_authenticator_digest.so
- res_pjsip_outbound_publish.so
- res_pjsip_outbound_registration.so
- res_pjsip_path.so
- res_pjsip_pidf_body_generator.so
- res_pjsip_publish_asterisk.so
- res_pjsip_pubsub.so
- res_pjsip_refer.so
- res_pjsip_registrar.so
- res_pjsip_rfc3326.so
- res_pjsip_sdp_rtp.so
- res_pjsip_send_to_voicemail.so
- res_pjsip_session.so
- res_pjsip_sips_contact.so
- res_pjsip_xpidf_body_generator.so
- res_rtp_asterisk.so
- res_rtp_multicast.so
- res_security_log.so
- res_sorcery_astdb.so
- res_sorcery_config.so
- res_sorcery_memory.so
- res_sorcery_memory_cache.so
- res_srtp.so
- res_stasis.so
- res_stasis_answer.so
- res_stasis_device_state.so
- res_stasis_playback.so
- res_stasis_recording.so
- res_timing_pthread.so
- res_timing_timerfd.so
- res_pjsip_header_funcs.so
- res_pjsip_history.so
- res_pjsip_sdp_rtp.so

@ -0,0 +1,8 @@
- name: handle config change
import_tasks: asterisk_handlers.yml
- name: restart asterisk
service:
name: asterisk
state: restarted

@ -0,0 +1,19 @@
- block:
- name: restart asterisk
service:
name: asterisk
state: restarted
when: item.item.action is not defined
- name: reload dialplan
command:
cmd: 'asterisk -rx "dialplan reload"'
when: item.item.action == 'reload dialplan'
- name: reload configs
command:
cmd: 'asterisk -rx "core reload"'
when: item.item.action == 'reload configs'
when: item is defined

@ -0,0 +1,194 @@
- name: set asterisk_cfg
set_fact:
asterisk_cfg: "{{ asterisk_default_config | d({}) | combine(asterisk_config | d({}), recursive=true) }}"
- name: install dependencies
include_tasks: tasks/install_packages.yml
vars:
package:
- asterisk
- asterisk-pgsql
- asterisk-openrc
- asterisk-opus
- asterisk-srtp
- tar
- vorbis-tools
- name: create user and group
include_tasks: tasks/create_user.yml
vars:
user:
name: "{{ asterisk_user }}"
group: "{{ asterisk_group }}"
dir: "{{ asterisk_dir }}"
- name: ensure asterisk directories exist
file:
path: "{{ item }}"
state: directory
owner: "{{ asterisk_user }}"
group: "{{ asterisk_group }}"
loop:
- "{{ asterisk_dir }}"
- "{{ asterisk_conf_dir }}"
- "{{ asterisk_tls_dir }}"
- "{{ asterisk_data_dir }}"
- "{{ asterisk_data_dir }}/moh"
- "{{ asterisk_data_dir }}/sounds"
- "{{ asterisk_data_dir }}/sounds/{{ asterisk_language }}"
- "{{ asterisk_data_dir }}/sounds/{{ asterisk_language }}/custom"
- "{{ asterisk_recordings_dir }}"
- name: template custom asterisk configs
template:
src: "{{ item }}.j2"
dest: "{{ asterisk_conf_dir }}/{{ item }}.conf"
force: yes
mode: 0400
owner: "{{ asterisk_user }}"
group: "{{ asterisk_group }}"
lstrip_blocks: yes
notify: restart asterisk
loop:
- custom_pjsip
- custom_queues
- ext_ivr
- ext_utils
- extensions
- name: template asterisk configs
template:
src: "{{ 'config' if item is string else (item.config | d('config')) }}.j2"
dest: "{{ asterisk_conf_dir }}/{{ item if item is string else (item.dest | d(item.config) | d(item.name)) }}.conf"
force: yes
mode: 0400
owner: "{{ asterisk_user }}"
group: "{{ asterisk_group }}"
lstrip_blocks: yes
notify: restart asterisk
loop:
- acl
- asterisk
- ccss
- cdr
- cdr_pgsql
- cli_aliases
- cli_permissions
- codecs
- confbridge
- features
- followme
- indications
- logger
- manager
- musiconhold
- pjproject
- pjsip
- pjsip_notify
- queues
- rtp
- modules
- queuerules
- cel
- udptl
- name: edit service config
lineinfile:
path: /etc/conf.d/asterisk
regexp: "^{{ item.name | upper }}="
line: "{{ item.name | upper }}=\"{{ item.value }}\""
when: item.when | d(true)
notify: restart asterisk
loop:
- name: asterisk_opts
value: "-C {{ (asterisk_conf_dir ~ '/asterisk.conf') | quote }}"
when: "{{ asterisk_conf_dir != '/etc/asterisk' }}"
- name: asterisk_user
value: "{{ asterisk_user }}"
- name: asterisk_nice
value: "{{ asterisk_niceness | d(None) }}"
when: "{{ asterisk_niceness is defined }}"
- name: download asterisk sound pack
get_url:
url: "https://downloads.asterisk.org/pub/telephony/sounds/asterisk-core-sounds-{{ asterisk_language }}-{{ item }}-current.tar.gz"
dest: "{{ asterisk_data_dir }}/{{ asterisk_language }}-{{ item }}.tar.gz"
owner: "{{ asterisk_user }}"
group: "{{ asterisk_group }}"
register: result
loop:
- sln16
- wav
- name: extract sound pack
unarchive:
src: "{{ item }}"
dest: "{{ asterisk_data_dir }}/sounds/{{ asterisk_language }}"
remote_src: yes
owner: "{{ asterisk_user }}"
group: "{{ asterisk_group }}"
loop: "{{ result.results | d([]) | selectattr('dest', 'defined') | selectattr('changed', 'defined') | selectattr('changed', 'equalto', true) | map(attribute='dest') | list }}"
- name: deploy RSA cert for SIP TLS
include_role:
name: certs
vars:
certs:
id: ast-tls
cert: "{{ asterisk_tls_dir }}/asterisk.crt"
key: "{{ asterisk_tls_dir }}/asterisk.key"
chain: "{{ asterisk_tls_dir }}/chain.crt"
owner: "{{ asterisk_user }}"
group: "{{ asterisk_group }}"
post_hook: service asterisk restart
notify: restart asterisk
- name: install and configure cdr
include_role:
name: cdr
vars:
cdr_group: "{{ asterisk_group }}"
cdr_config:
db_host: "{{ asterisk_cfg.cdr_pgsql.global.hostname }}"
db_user: "{{ asterisk_cfg.cdr_pgsql.global.user }}"
db_pass: "{{ asterisk_cfg.cdr_pgsql.global.password }}"
db_database: "{{ asterisk_cfg.cdr_pgsql.global.dbname }}"
db_table: "{{ asterisk_cfg.cdr_pgsql.global.table }}"
record_dir: "{{ asterisk_recordings_dir }}"
ami_user: "{{ asterisk_ami_cdr_user }}"
ami_pass: "{{ asterisk_ami_cdr_secret }}"
when: asterisk_use_cdr | d(true) == true
- name: flush handlers
meta: flush_handlers
- name: add directories to backup plan
include_role:
name: backup
vars:
function: add
backup_items:
- "{{ asterisk_conf_dir }}"
- "{{ asterisk_tls_dir }}"
- "{{ asterisk_data_dir }}/moh"
- "{{ asterisk_data_dir }}/sounds/{{ asterisk_language }}/custom"
- "{{ asterisk_dir }}/astdb.sqlite3"
- name: enable and start asterisk
service:
name: asterisk
enabled: yes
state: started

@ -0,0 +1,85 @@
{% macro config_template(config_name, asterisk_cfg) -%}
{% set ns = namespace(objects=false) -%}
{% if config_name is string and asterisk_cfg[config_name] is mapping -%}
{% for section in (asterisk_cfg[config_name] | dict2items) -%}
{% if section.value is mapping -%}
{% set template_parts = [] -%}
{% if (section.value['__template__'] is boolean) and (section.value['__template__'] == true) -%}
{% set template_parts = template_parts + ['!'] -%}
{% endif -%}
{% if section.value['__template_from__'] is string -%}
{% set template_parts = template_parts + [section.value['__template_from__']] -%}
{% elif section.value['__template_from__'] | type_debug == 'list' -%}
{% set template_parts = template_parts + section.value['__template_from__'] -%}
{% endif -%}
{% if section.value['__comment__'] is string -%}
; {{ section.value['__comment__'] }}
{% endif -%}
{% if template_parts | length == 0 -%}
[{{ section.key }}]
{% else -%}
[{{ section.key }}]({{ template_parts | join(',') }})
{% endif -%}
{% set ns.objects = (section.value['__inner_objects__'] | d(asterisk_cfg[config_name]['__inner_objects__'] | d(false))) -%}
{% for option in (section.value | dict2items) -%}
{% if not option.key.startswith('__') and not option.key.endswith('__') -%}
{% if option.value | type_debug == 'list' -%}
{% if option.value | length == 0 -%}
{{ option.key }} =>
{% else -%}
{% for option_element in option.value -%}
{% set option_value = 'yes' if (option_element is boolean and option_element == true) else ('no' if (option_element is boolean and option_element == false) else option_element ) -%}
{{ option.key }} => {{ option_value }}
{% endfor -%}
{% endif -%}
{% elif option.value is mapping -%}
{% set option_is_object = option.value['__inner_objects__'] | d(ns.objects) -%}
{% if option.value['__comment__'] is string -%}
; {{ option.value['__comment__'] }}
{% endif -%}
{% if option.value['__include_before__'] is string -%}
#include {{ option.value['__include_before__'] }}
{% endif -%}
{% if option.value['__try_include_before__'] is string -%}
#tryinclude {{ option.value['__try_include_before__'] }}
{% endif -%}
{% set option_value = 'yes' if (option.value['__value__'] is boolean and option.value['__value__'] == true) else ('no' if (option.value['__value__'] is boolean and option.value['__value__'] == false) else option.value['__value__'] ) -%}
{{ option.key }} {{ '=>' if option_is_object else '=' }} {{ option_value }}
{% if option.value['__include_after__'] is string -%}
#include {{ option.value['__include_after__'] }}
{% endif -%}
{% if option.value['__try_include_after__'] is string -%}
#tryinclude {{ option.value['__try_include_after__'] }}
{% endif -%}
{% else -%}
{% set option_value = 'yes' if (option.value is boolean and option.value == true) else ('no' if (option.value is boolean and option.value == false) else option.value ) -%}
{{ option.key }} {{ '=>' if ns.objects else '=' }} {{ option_value }}
{% endif -%}
{% endif -%}
{% endfor -%}
{% if not loop.last %}
{% endif -%}
{% elif (section.key == '__include__') and (section.value is string) -%}
#include {{ section.value }}
{% elif (section.key == '__try_include__') and (section.value is string) -%}
#tryinclude {{ section.value }}
{% endif -%}
{% endfor -%}
{% endif -%}
{% endmacro -%}

@ -0,0 +1,3 @@
{%- from '_macros.j2' import config_template -%}
{{- config_template(item if (item is string) else (item.config | d(item.name)), asterisk_cfg) -}}

@ -0,0 +1,75 @@
{% macro trunk_options(opts) -%}
{% for opt in (opts | d({}) | dict2items) -%}
{{ opt.key }} = {{ 'yes' if (opt.value is boolean and opt.value == true) else ('no' if (opt.value is boolean and opt.value == false) else opt.value ) }}
{% endfor -%}
{% endmacro -%}
{% for user in asterisk_users | d({}) | dict2items -%}
{% if user.value is mapping -%}
{% if user.value['__comment__'] is string -%}
; {{ user.value['__comment__'] }}
{% endif -%}
[auth-{{ user.key }}](auth-common)
username = {{ user.value['login'] | d(user.key) }}
password = {{ user.value['password'] }}
[{{ user.key }}](aor-common)
[{{ user.key }}](endpoint-lan)
auth = auth-{{ user.key }}
aors = {{ user.key }}
callerid = {{ user.value['callerid'] | d(user.key) }} <{{ user.key }}>
{% endif -%}
{% endfor %}
{% for trunk in asterisk_trunks | d({}) | dict2items -%}
{% if trunk.value is mapping -%}
{% if trunk.value['__comment__'] is string -%}
; {{ trunk.value['__comment__'] }}
{% endif -%}
[transport-{{ trunk.key }}](transport-udp,transport-ext)
{{ trunk_options(trunk.value['transport']) }}
{# #}
[registration-{{ trunk.key }}](registration-common)
outbound_auth = auth-{{ trunk.key }}
endpoint = endpoint-{{ trunk.key }}
transport = transport-{{ trunk.key }}
{{ trunk_options(trunk.value['registration']) }}
{# #}
[auth-{{ trunk.key }}](auth-common)
{{ trunk_options(trunk.value['auth']) }}
{# #}
[aor-{{ trunk.key }}](aor-common)
{{ trunk_options(trunk.value['aor']) }}
{# #}
[endpoint-{{ trunk.key }}](endpoint-trunk)
transport = transport-{{ trunk.key }}
context = inbound-{{ trunk.key }}
outbound_auth = auth-{{ trunk.key }}
aors = aor-{{ trunk.key }}
{{ trunk_options(trunk.value['endpoint']) }}
{# #}
[identify-{{ trunk.key }}]
type = identify
endpoint = endpoint-{{ trunk.key }}
{{ trunk_options(trunk.value['identify']) }}
{%- if not loop.last %}
{% endif -%}
{% endif -%}
{% endfor %}
[reslist-all]
type=resource_list
event=presence
list_item={{ asterisk_users | d({}) | dict2items | map(attribute='key') | list | join(',') }}

@ -0,0 +1,26 @@
{% for user in asterisk_users | d({}) | dict2items -%}
{% if user.value is mapping -%}
[queue-{{ user.key }}]({{ user.value['self_queue_type'] | d('queue-single') }})
member => PJSIP/{{ user.key }},0,{{ user.value['callerid'] | d(user.key) }}
{% endif -%}
{% endfor %}
{% set defined_queues = (asterisk_users | d({}) | dict2items | map(attribute='value') | list | selectattr('queues', 'defined') | map(attribute='queues') | list | flatten | unique | list) -%}
{% set auto_queues = (asterisk_users | d({}) | dict2items | rejectattr('value.queues', 'defined') | map(attribute='key') | list) -%}
{% set all_queues = ((defined_queues | d([])) + (auto_queues | d([])) | unique | list) -%}
{% for queue in defined_queues -%}
{% if asterisk_users[queue] is not defined -%}
{% set queue_users = (asterisk_users | d({}) | dict2items | selectattr('value.queues', 'defined') | selectattr('value.queues', 'contains', queue) | list) -%}
{% if queue_users | length > 1 -%}
[queue-{{ queue }}](queue-template)
{% for user in queue_users -%}
member => PJSIP/{{ user.key }},0,{{ user.value['callerid'] | d(user.key) }}
{% endfor -%}
{%- if not loop.last %}
{% endif -%}
{% endif -%}
{% endif -%}
{% endfor -%}

@ -0,0 +1,58 @@
; IVR
; 1 - went to IVR
; 2 - pressed a button
; 3 - did not press anything
[ivr-dial]
exten => s,1,Set(CDR(ivr)=2)
same => n,Gosub(inbound-queued,s,1(${ARG1}))
same => n,Hangup()
[ivr-dial-all]
exten => s,1,Set(CDR(ivr)=3)
same => n,Queue(queue-all,inrt,,,,,,pre-call)
[ivr-select]
exten => 1,1,Gosub(ivr-dial,s,1(1))
exten => 2,1,Gosub(ivr-dial,s,1(3))
exten => 3,1,Gosub(ivr-dial,s,1(2))
exten => 4,1,Gosub(ivr-dial,s,1(11))
exten => 5,1,Gosub(ivr-dial,s,1(9))
[ivr]
exten => s,1,Answer(250)
same => n,Set(CDR(ivr)=1)
same => n,Set(TIMEOUT(digit)=3)
same => n,Set(TIMEOUT(response)=3)
same => n,Background(custom/ivr-intro-12-2021,m,,ivr-select)
same => n,WaitExten(3)
same => n,Gosub(ivr-dial-all,s,1)
same => n,Hangup()
[ivr-select-spb]
exten => 1,1,Gosub(ivr-dial,s,1(6))
exten => 2,1,Gosub(ivr-dial,s,1(8))
[ivr-dial-all-spb]
exten => s,1,Set(CDR(ivr)=3)
same => n,Queue(queue-spb,inrt,,,,,,pre-call)
[ivr-spb]
exten => s,1,Answer(250)
same => n,Set(CDR(ivr)=1)
same => n,Set(TIMEOUT(digit)=3)
same => n,Set(TIMEOUT(response)=3)
same => n,Background(custom/ivr-intro-spb,m,,ivr-select-spb)
same => n,WaitExten(3)
same => n,Gosub(ivr-dial-all-spb,s,1)
same => n,Hangup()

@ -0,0 +1,88 @@
; Extension utilities
[record-start]
exten => s,1,ExecIf($["${IS_RECORDING}"="1"]?Return())
same => n,Set(UID=${UNIQUEID}.${RAND(1,100000)})
same => n,Set(CDR(actualuniqueid)=${UID})
same => n,MixMonitor({{ asterisk_recordings_dir }}/${UID}.wav,b,oggenc -q 5 -o {{ asterisk_recordings_dir }}/${UID}.ogg {{ asterisk_recordings_dir }}/${UID}.wav && rm {{ asterisk_recordings_dir }}/${UID}.wav)
same => n,Set(__IS_RECORDING=1)
same => n,Return()
[record-stop]
exten => s,1,StopMixMonitor()
same => n,Return()
; Filtering CallerID
[clear-callerid]
exten => s,1,Verbose(Filtering CallerID)
same => n,Set(CALLERID(num)=${FILTER(0-9,${CALLERID(num)})})
same => n,Set(CALLERID(name)=)
same => n,Return()
; Setting up volume control
[volume-setup]
exten => s,1,Set(CURRENT_VOLUME_TX=1)
same => n,Set(CURRENT_VOLUME_RX=1)
same => n,Set(__DYNAMIC_FEATURES=call-controls)
same => n,Return()
[volume-up-tx]
exten => s,1,Set(CURRENT_VOLUME_TX=$[${CURRENT_VOLUME_TX}*1.25])
same => n,Set(VOLUME(TX)=${CURRENT_VOLUME_TX})
same => n,Return()
[volume-up-rx]
exten => s,1,Set(CURRENT_VOLUME_RX=$[${CURRENT_VOLUME_RX}*1.25])
same => n,Set(VOLUME(RX)=${CURRENT_VOLUME_RX})
same => n,Return()
[volume-down-tx]
exten => s,1,Set(CURRENT_VOLUME_TX=$[${CURRENT_VOLUME_TX}*0.75])
same => n,Set(VOLUME(TX)=${CURRENT_VOLUME_TX})
same => n,Return()
[volume-down-rx]
exten => s,1,Set(CURRENT_VOLUME_RX=$[${CURRENT_VOLUME_RX}*0.75])
same => n,Set(VOLUME(RX)=${CURRENT_VOLUME_RX})
same => n,Return()
[volume-increase-all]
exten => s,1,Set(CURRENT_VOLUME_RX=2)
same => n,Set(CURRENT_VOLUME_TX=2)
same => n,Set(VOLUME(RX)=2)
same => n,Set(VOLUME(TX)=2)
same => n,Return()
; An invalid extension has been dialed
[invalid-ext]
exten => s,1,Answer(250)
same => n,Playback(custom/invalid-ext)
same => n,Wait(0.5)
same => n,Hangup()
; An extension has been dialed, but it is currently offline
[offline-ext]
exten => s,1,Answer(250)
same => n,Playback(custom/this-offline)
same => n,Wait(0.5)
same => n,Hangup()
; Output "Busy" signal
[busy]
exten => s,1,Busy(10)
same => n,Wait(1)
same => n,Hangup()
; Output "Congestion" signal
[congestion]
exten => s,1,Congestion(10)
same => n,Wait(1)
same => n,Hangup()

@ -0,0 +1,303 @@
[general]
static=yes ; never rewrite this file
writeprotect=yes
autofallthrough=yes ; hang up if end of dialplan is reached
clearglobalvars=yes ; clear global vars on dialplan reload
[globals]
#include ext_utils.conf ; include utilities
#include ext_ivr.conf ; include IVR
TRANSFER_CONTEXT=transfer
[transfer]
exten => 0,1,Verbose(TRANSFER IVR)
same => n,Set(__IS_RECORDING=0)
same => n,StopMixMonitor()
same => n,ForkCDR(erv)
same => n,Gosub(pre-any,s,1(IVR,TRANSFER))
same => n,Gosub(ivr,s,1)
same => n,Wait(0.5)
same => n,Hangup()
exten => _Z,1,Verbose(TRANSFER)
same => n,Set(__IS_RECORDING=0)
same => n,StopMixMonitor()
same => n,ForkCDR(erv)
same => n,Gosub(pre-any,s,1(${EXTEN},TRANSFER))
same => n,Gosub(inbound-queued,s,1(${EXTEN}))
same => n,Wait(0.5)
same => n,Hangup()
include => catchall
[pre-any]
exten => s,1,Gosub(clear-callerid,s,1)
same => n,Set(__CALLER=${CALLERID(num)})
same => n,Set(__CALLEE=${ARG1})
same => n,Set(__CALL_OPERATION=${ARG2})
same => n,Set(CDR(actualsrc)=${CALLER})
same => n,Set(CDR(actualdst)=${CALLEE})
same => n,Set(CDR(realcall)=1)
same => n,Verbose(${CALL_OPERATION}: ${CALLER} -> ${CALLEE})
same => n,Set(LIMIT_PLAYAUDIO_CALLER=no,LIMIT_PLAYAUDIO_CALLEE=yes)
same => n,Set(LIMIT_TIMEOUT_FILE=custom/call-expired)
same => n,Set(LIMIT_WARNING_FILE=custom/call-expiring-soon)
same => n,Return()
[pre-call]
exten => s,1,Gosub(volume-setup,s,1)
same => n,Gosub(record-start,s,1)
same => n,Set(CDR(realcall)=2)
same => n,Set(CDR(startedat)=${EPOCH})
same => n,Set(CDR(actualdisposition)=ANSWERED)
same => n,Set(CDR(actualdst2)=${CALLERID(num)})
same => n,Return()
[pre-out-call]
exten => s,1,Gosub(volume-setup,s,1)
same => n,Gosub(record-start,s,1)
same => n,Set(CDR(realcall)=2)
same => n,Set(CDR(startedat)=${EPOCH})
same => n,Return()
; 1.1. Place an inbound call into a single queue
[inbound-queued]
exten => s,1,Gosub(pre-any,s,1(${ARG1},INBOUND-QUEUED))
same => n,Verbose(DS ${DEVICE_STATE(PJSIP/${CALLEE})})
same => n,GosubIf($["${DEVICE_STATE(PJSIP/${CALLEE})}" = "BUSY"]?inbound-queued-busy,s,1)
same => n,GosubIf($["${DEVICE_STATE(PJSIP/${CALLEE})}" = "INUSE"]?inbound-queued-busy,s,1)
same => n,GosubIf($["${DEVICE_STATE(PJSIP/${CALLEE})}" = "RINGINUSE"]?inbound-queued-busy,s,1)
same => n,GosubIf($["${DEVICE_STATE(PJSIP/${CALLEE})}" = "RINGING"]?inbound-queued-busy,s,1)
same => n,GosubIf($["${DEVICE_STATE(PJSIP/${CALLEE})}" = "UNAVAILABLE"]?inbound-queued-unavail,s,1)
same => n,Queue(queue-${CALLEE},inrt,,,,,,pre-call)
same => n,Wait(0.5)
same => n,Hangup()
; 1.2. Callee is busy, play a message (if appropriate) and place it into a single queue
[inbound-queued-busy]
exten => s,1,Verbose(QUEUED BUSY)
same => n,GotoIf($["${CALLEE}"="9"]?busy-le)
same => n,GotoIf($[ $["${CALLEE}"="6"] | $["${CALLEE}"="8"] | $["${CALLEE}"="10"] | $["${CALLEE}"="12"] ]?busy-spb)
same => n,Background(custom/this-busy-ask-redirect,m,,inbound-queued-select-busy))
same => n,Queue(queue-${CALLEE},inrt,,,,,,pre-call)
same => n,Wait(0.5)
same => n,Hangup()
same => n(busy-le),Playback(custom/this-busy-le)
same => n,Queue(queue-${CALLEE},inrt,,,,,,pre-call)
same => n,Wait(0.5)
same => n,Hangup()
same => n(busy-spb),Playback(custom/this-busy-spb)
same => n,Queue(queue-${CALLEE},inrt,,,,,,pre-call)
same => n,Wait(0.5)
same => n,Hangup()
; 1.25. Callee is not available, play a message and place it into a single queue
;same => n,GosubIf($[ $["${CALLEE}"="6"] | $["${CALLEE}"="8"] | $["${CALLEE}"="10"] | $["${CALLEE}"="12"] ]?inbound-queued-unavail-spb,s,1)
[inbound-queued-unavail]
exten => s,1,Verbose(QUEUED UNAVAIL)
same => n,GosubIf($["${CALLEE}"="9"]?inbound-queued-unavail-le,s,1)
same => n,Playback(custom/this-unavail-will-redirect)
same => n,Set(CDR(ivr)=3)
same => n,Queue(queue-some-${CALLEE},inrt,,,,,,pre-call)
same => n,Wait(0.5)
same => n,Hangup()
; 1.25.1 LE callee is not available, play a message and hang up
[inbound-queued-unavail-le]
exten => s,1,Playback(custom/this-unavail-le)
same => n,Wait(0.5)
same => n,Hangup()
; 1.25.2 SPB callee is not available, play a message and hang up
;[inbound-queued-unavail-spb]
;exten => s,1,Playback(custom/this-unavail-spb)
; same => n,Wait(0.5)
; same => n,Hangup()
; 1.3. Caller has requested to join a "some" queue, place it there
[inbound-queued-to-some]
exten => s,1,Verbose(QUEUE TO SOME)
same => n,Set(CDR(ivr)=3)
same => n,Queue(queue-some-${CALLEE},inrt,,,,,,pre-call)
same => n,Wait(0.5)
same => n,Hangup()
; 1.1.1. Allow callers to exit from a background playback to dial some
[inbound-queued-select-busy]
exten => 1,1,Gosub(inbound-queued-to-some,s,1)
; 1.2.1. Allow callers to exit from a queue to dial some
; Invalid DTMF keypresses get redirected back to inbound queue
[inbound-queued-inqueue-busy]
exten => 1,1,Gosub(inbound-queued-to-some,s,1)
exten => i,1,Gosub(inbound-queued,s,1(${CALLEE}))
; Inbound calls from Multifon trunk to LE endpoint (9)
[inbound-multifon]
exten => _Z.,1,Gosub(inbound-queued,s,1(9))
same => n,Hangup()
; Inbound calls from Dom.ru 222003 endpoint directly to ext 1
[inbound-domru-3]
exten => _Z.,1,Gosub(inbound-queued,s,1(1))
same => n,Hangup()
; Inbound calls from Dom.ru 222004 endpoint to IVR
[inbound-domru-4]
exten => _Z.,1,Gosub(pre-any,s,1(IVR,INBOUND))
same => n,Gosub(ivr,s,1)
same => n,Wait(0.5)
same => n,Hangup()
; Inbound calls from Smart SPB trunk to SPB IVR
[inbound-smart-spb]
exten => _Z.,1,Gosub(pre-any,s,1(IVR,INBOUND))
same => n,Gosub(ivr-spb,s,1)
same => n,Wait(0.5)
same => n,Hangup()
; Outbound calls from all local endpoints
[outbound]
exten => _Z,hint,PJSIP/${EXTEN}
exten => _ZX,hint,PJSIP/${EXTEN}
exten => _Z,1,Gosub(outbound-internal,s,1(${EXTEN}))
exten => _ZX,1,Gosub(outbound-internal,s,1(${EXTEN}))
exten => _7XXXXXXXXXX,1,GosubIf($[ $["${CALLERID(num)}"="6"] | $["${CALLERID(num)}"="8"] | $["${CALLERID(num)}"="10"] | $["${CALLERID(num)}"="12"] ]?outbound-external,s,1(8${EXTEN:1}):outbound-external,s,1(+${EXTEN}))
exten => _8XXXXXXXXXX,1,GosubIf($[ $["${CALLERID(num)}"="6"] | $["${CALLERID(num)}"="8"] | $["${CALLERID(num)}"="10"] | $["${CALLERID(num)}"="12"] ]?outbound-external,s,1(${EXTEN}):outbound-external,s,1(+7${EXTEN:1}))
exten => _+7XXXXXXXXXX,1,GosubIf($[ $["${CALLERID(num)}"="6"] | $["${CALLERID(num)}"="8"] | $["${CALLERID(num)}"="10"] | $["${CALLERID(num)}"="12"] ]?outbound-external,s,1(8${EXTEN:2}):outbound-external,s,1(${EXTEN}))
exten => _9XXXXXXXXX,1,GosubIf($[ $["${CALLERID(num)}"="6"] | $["${CALLERID(num)}"="8"] | $["${CALLERID(num)}"="10"] | $["${CALLERID(num)}"="12"] ]?outbound-external,s,1(8${EXTEN}):outbound-external,s,1(+7${EXTEN}))
exten => _XXXXXX,1,Gosub(outbound-external,s,1(+78332${EXTEN}))
exten => _XXXXXXX,1,GosubIf($[ $["${CALLERID(num)}"="6"] | $["${CALLERID(num)}"="8"] | $["${CALLERID(num)}"="10"] | $["${CALLERID(num)}"="12"] ]?outbound-external,s,1(${EXTEN}))
include => service
include => catchall
; Internal calls
[outbound-internal]
exten => s,1,Gosub(pre-any,s,1(${ARG1},INTERNAL))
same => n,GotoIf($["${CALLERID(number)}" = "${ARG1}"]?busy,s,1) ; dialing the same extension as caller
same => n,GotoIf($["${DEVICE_STATE(PJSIP/${ARG1})}" = "INVALID"]?invalid-ext,s,1) ; extension is invalid
same => n,GotoIf($["${DEVICE_STATE(PJSIP/${ARG1})}" = "UNAVAILABLE"]?offline-ext,s,1) ; extension is valid but offline
same => n,Dial(PJSIP/${ARG1},900,girtTL(3600000:60000)U(pre-out-call))
same => n,Wait(0.5)
same => n,Hangup()
[outbound-external]
exten => s,1,Gosub(pre-any,s,1(${ARG1},OUTBOUND))
same => n,GosubIf($[${CALLERID(num)} = 9]?outbound-multifon,s,1(${ARG1}))
same => n,GosubIf($[${CALLERID(num)} = 6]?outbound-smart-spb,s,1(${ARG1}))
same => n,GosubIf($[${CALLERID(num)} = 8]?outbound-smart-spb,s,1(${ARG1}))
same => n,GosubIf($[${CALLERID(num)} = 10]?outbound-smart-spb,s,1(${ARG1}))
same => n,GosubIf($[${CALLERID(num)} = 12]?outbound-smart-spb,s,1(${ARG1}))
same => n,Gosub(outbound-domru,s,1(${ARG1}))
same => n,Wait(0.5)
same => n,Hangup()
[outbound-multifon]
exten => s,1,Dial(PJSIP/${ARG1}@endpoint-multifon,900,irTL(3600000:60000)U(pre-out-call))
same => n,Wait(0.5)
same => n,Hangup()
[outbound-domru]
exten => s,1,Dial(PJSIP/${ARG1}@endpoint-domru-4,900,irTL(3600000:60000)U(pre-out-call))
same => n,Wait(0.5)
same => n,Hangup()
[outbound-smart-spb]
exten => s,1,Dial(PJSIP/${ARG1}@endpoint-smart-spb,900,irTL(3600000:60000)U(pre-out-call))
same => n,Wait(0.5)
same => n,Hangup()
[service]
; Simple ring test
exten => 001,1,Ringing()
same => n,Wait(20)
same => n,Hangup()
; Hello World playback
exten => 002,1,Answer(250)
same => n,Playback(hello-world)
same => n,Wait(0.5)
same => n,Hangup()
; Echo test
exten => 003,1,Answer(250)
same => n,Playback(demo-echotest)
same => n,Echo
same => n,Playback(demo-echodone)
same => n,Wait(0.5)
same => n,Hangup()
; Internal IVR
exten => 004,1,Answer(250)
same => n,Gosub(ivr,s,1)
same => n,Wait(0.5)
same => n,Hangup()
; Congestion test
exten => 005,1,Congestion()
same => n,Wait(20)
same => n,Hangup()
[catchall]
exten => _X.,1,Gosub(invalid-ext,s,1) ; go to invalid extension macro on all extensions
exten => i,1,Gosub(invalid-ext,s,1) ; same, but with invalid extensions

@ -0,0 +1,5 @@
- name: add backup dirs to collected backup dirs
set_fact:
collected_backup_dirs: "{{ (collected_backup_dirs | d([])) +
([backup_items] if backup_items is string else backup_items) }}"
when: backup_items is defined and ((backup_items | type_debug == 'list') or backup_items is string)

@ -0,0 +1,8 @@
- name: add to backup plan
include_tasks: add.yml
when: function is defined and function == 'add'
- name: setup backups
include_tasks: setup.yml
when: function is defined and function == 'setup'

@ -0,0 +1,31 @@
- name: notify that backups are not supported
debug:
msg: backup host is missing, will not set up backups
when: services.backup is not mapping
- name: install restic with custom configuration
block:
- include_role:
name: restic
vars:
backup: "{{ backup_cfg }}"
when: services.backup is mapping and backup_cfg is mapping
- name: install restic with default configuration
block:
- include_role:
name: restic
vars:
backup:
dirs: "{{ collected_backup_dirs }}"
password: "{{ backup_password }}"
tags: automated
filter:
- "*.log"
- "node_modules"
- ".npm"
when: services.backup is mapping and backup_cfg is not defined and backup_password is defined

@ -0,0 +1,52 @@
blocky_user: blocky
blocky_group: blocky
blocky_dir: /opt/blocky
blocky_conf_dir: /etc/blocky
blocky_conf_file: "{{ blocky_conf_dir }}/blocky.yml"
blocky_tls_ecc384_cert: "{{ blocky_conf_dir }}/ecc384.crt"
blocky_tls_ecc384_key: "{{ blocky_conf_dir }}/ecc384.key"
blocky_port: 9000
blocky_enable_dot: yes
blocky_default_groups:
- selector: default
groups:
- all
blocky_default_config:
port: 53
bootstrapDns: 1.1.1.1
logLevel: warn
logTimestamp: no
upstreamTimeout: 4s
httpPort: "127.0.0.1:{{ blocky_port }}"
prometheus:
enable: "{{ host_metrics }}"
caching:
maxTime: 8h
maxItemsCount: 15000
prefetchMaxItemsCount: 1000
upstream:
default:
- tcp-tls:anycast.censurfridns.dk:853
- tcp-tls:dns.quad9.net:853
- tcp-tls:one.one.one.one:853
- tcp-tls:dns.digitale-gesellschaft.ch:853
blocking:
blackLists:
all:
- https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts
- https://block.energized.pro/extensions/regional/formats/hosts
- https://block.energized.pro/bluGo/formats/hosts
whiteLists:
all:
- https://raw.githubusercontent.com/anudeepND/whitelist/master/domains/whitelist.txt
refreshPeriod: 8h
blockTTL: 5m

@ -0,0 +1,4 @@
- name: restart blocky
service:
name: blocky
state: restarted

@ -0,0 +1,185 @@
- name: import internal tld resolver vars if internal nameserver is present
include_vars:
file: internal.yml
when: services.internal_ns is defined
- name: import ipv6 disable snippet
include_vars:
file: disable_ipv6.yml
hash_behaviour: merge
when: blocky_disable_ipv6 | d(false) == true
- name: import tls support
include_vars:
file: tls.yml
hash_behaviour: merge
when: host_tls and blocky_enable_dot
- name: set blocky_cfg
set_fact:
blocky_cfg: "{{ blocky_default_config | d({}) | combine(blocky_config | d({}), recursive=true) }}"
- name: install dependencies
include_tasks: tasks/install_packages.yml
vars:
package:
- libcap
- libc6-compat
- name: create user and group
include_tasks: tasks/create_user.yml
vars:
user:
name: "{{ blocky_user }}"
group: "{{ blocky_group }}"
dir: "{{ blocky_dir }}"
notify: restart blocky
- name: create directories
file:
path: "{{ item }}"
state: directory
mode: 0755
owner: "{{ blocky_user }}"
group: "{{ blocky_group }}"
notify: restart blocky
loop:
- "{{ blocky_conf_dir }}"
- "{{ blocky_dir }}"
- name: get and extract latest version of blocky
include_tasks: tasks/get_lastversion.yml
vars:
package:
name: 0xERR0R/blocky
location: github
assets: yes
asset_filter: 'Linux_x86_64.tar.gz$'
file: "{{ blocky_dir }}/last_version"
extract: "{{ blocky_dir }}"
user: "{{ blocky_user }}"
group: "{{ blocky_group }}"
notify: restart blocky
- name: template config file
template:
src: blocky.j2
dest: "{{ blocky_conf_file }}"
force: yes
mode: 0400
owner: "{{ blocky_user }}"
group: "{{ blocky_group }}"
lstrip_blocks: yes
notify: restart blocky
- name: template init script
template:
src: init.j2
dest: /etc/init.d/blocky
force: yes
mode: "+x"
notify: restart blocky
- name: ensure blocky binary has executable bit set
file:
path: "{{ blocky_dir }}/blocky"
mode: "+x"
- name: add cap_net_bind_service to blocky executable
community.general.capabilities:
path: "{{ blocky_dir }}/blocky"
capability: cap_net_bind_service+ep
changed_when: no
- name: install and configure nginx
include_role:
name: nginx
vars:
nginx:
servers:
- conf: nginx_server
certs: "{{ host_tls }}"
external_tld: "{{ host_tld }}"
- block:
- name: get certificate file type
stat:
path: /etc/nginx/tls/ecc384.crt
register: stat
- name: copy nginx ecc384 certificate to blocky dir
copy:
src: "/etc/nginx/tls/{{ item.src }}"
dest: "{{ item.dest }}"
force: yes
mode: 0400
owner: "{{ blocky_user }}"
group: "{{ blocky_group }}"
remote_src: yes
loop:
- src: ecc384.crt
dest: "{{ blocky_tls_ecc384_cert }}"
- src: ecc384.key
dest: "{{ blocky_tls_ecc384_key }}"
when: not (stat.stat.islnk is defined and stat.stat.islnk)
- name: create symlinks
file:
path: "{{ item.dest }}"
src: "/etc/nginx/tls/{{ item.src }}"
state: link
force: yes
loop:
- src: ecc384.crt
dest: "{{ blocky_tls_ecc384_cert }}"
- src: ecc384.key
dest: "{{ blocky_tls_ecc384_key }}"
when: stat.stat.islnk is defined and stat.stat.islnk
when: host_tls and blocky_enable_dot
- name: add directories to backup plan
include_role:
name: backup
vars:
function: add
backup_items:
- "{{ blocky_conf_dir }}"
- name: add prometheus metric target
include_role:
name: prometheus
vars:
function: add_target
target:
name: blocky
scheme: "{{ host_protocol }}"
when: host_metrics
- name: flush handlers
meta: flush_handlers
- name: enable and start blocky
service:
name: blocky
enabled: yes
state: started

@ -0,0 +1,7 @@
{%- set mappings = blocky_default_mappings | items2dict(key_name='tld', value_name='resolver') -%}
{%- set conditional = { 'conditional': { 'mapping': mappings }} -%}
{%- set groups = blocky_default_groups | items2dict(key_name='selector', value_name='groups') -%}
{%- set clientGroupsBlock = { 'blocking': { 'clientGroupsBlock': groups }} -%}
{{- blocky_cfg | combine(clientGroupsBlock, recursive=true) | combine(conditional, recursive=true) | to_nice_yaml(indent=2, width=512) }}

@ -0,0 +1,19 @@
#!/sbin/openrc-run
name="blocky"
command="{{ blocky_dir }}/blocky"
command_args="--config {{ blocky_conf_file | quote }}"
directory="{{ blocky_dir }}"
command_user="{{ blocky_user }}:{{ blocky_group }}"
pidfile="/var/run/blocky.pid"
command_background=true
start_stop_daemon_args="--stdout-logger logger --stderr-logger logger"
depend() {
need net
use dns
}
start_pre() {
setcap 'cap_net_bind_service=+ep' {{ (blocky_dir ~ '/blocky') | quote }}
}

@ -0,0 +1,16 @@
location / {
return 404;
}
location /dns-query {
proxy_pass http://127.0.0.1:{{ blocky_port }};
proxy_set_header Connection "";
}
{% if host_metrics -%}
location /metrics {
proxy_pass http://127.0.0.1:{{ blocky_port }};
allow {{ int_net }};
deny all;
}
{%- endif %}

@ -0,0 +1,4 @@
blocky_default_config:
filtering:
queryTypes:
- AAAA

@ -0,0 +1,7 @@
blocky_default_mappings:
- tld: "{{ int_tld }}"
resolver: "{%- if services.internal_ns is mapping -%}\
{{- hostvars[services.internal_ns.hostname]['ansible_host'] -}}\
{%- else -%}\
{{- hostvars | dict2items | selectattr('key', 'in', services.internal_ns | map(attribute='hostname')) | map(attribute='value') | list | map(attribute='ansible_host') | list | join(',') -}}\
{%- endif -%}"

@ -0,0 +1,4 @@
blocky_default_config:
tlsPort: 853
certFile: "{{ blocky_tls_ecc384_cert }}"
keyFile: "{{ blocky_tls_ecc384_key }}"

@ -0,0 +1,27 @@
ca_key_types:
- { name: rsa2048, type: RSA, size: 2048 }
- { name: ecc384, type: ECC, curve: secp384r1, digest: sha384 }
ca_key_names: "{{ ca_key_types | map(attribute='name') | list }}"
ca_default_items:
- { type: ecc384 }
- { type: rsa2048 }
ca_dir: /etc/ca
ca_rp: root-
ca_ip: inter-
ca_crt_ext: crt
ca_key_ext: key
ca_csr_ext: csr
ca_pfx_ext: pfx
# when to start to reissue certs
ca_reissue_period: 8w
ca_options: {}
crl_last_update_time: +8w
crl_next_update_time: +24w
crl_dir: /opt/crl

@ -0,0 +1,227 @@
- include_tasks: prepare_item.yml
- name: define combined options
set_fact:
ca_combined: "{{ ca_options | d({}) | combine(item) }}"
- name: define cert parameters
set_fact:
key_path: "{%- if item.key is defined -%}{{ item.key }}\
{%- else -%}{{ ca_combined.path ~ '/' ~ kt.name ~ '.' ~ ca_key_ext }}\
{%- endif -%}"
cert_path: "{%- if item.cert is defined -%}{{ item.cert }}\
{%- else -%}{{ ca_combined.path ~ '/' ~ kt.name ~ '.' ~ ca_crt_ext }}\
{%- endif -%}"
use_acme: "{{ ca_combined.acme | d(has_acme | d(false)) }}"
- name: define tld and presets
set_fact:
ca_tld: "{{ ca_combined.tld | d(host_tld) }}"
ca_presets:
web:
cn: FQDN
eku: ['clientAuth', 'serverAuth']
ku: ['digitalSignature', 'keyEncipherment', 'keyAgreement']
san: FQDN
psh:
cn: FQDN
eku: ['serverAuth']
ku: ['digitalSignature', 'keyEncipherment', 'keyAgreement']
san: FQDN
- name: select a preset
set_fact:
ca_preset: >
{% if item.preset is defined -%}{{ ca_presets[item.preset] }}
{%- elif ca_options.preset is defined -%}{{ ca_presets[ca_options.preset] }}
{%- else -%}{{ None }}
{%- endif %}
- name: generate private key
community.crypto.openssl_privatekey:
path: "{{ key_path }}"
size: "{{ kt.size | d(omit) }}"
curve: "{{ kt.curve | d(omit) }}"
type: "{{ kt.type }}"
backup: yes
force: no
format: pkcs8
format_mismatch: convert
regenerate: never
mode: "{{ k_mode | d(omit) }}"
owner: "{{ k_owner | d(omit) }}"
group: "{{ k_group | d(omit) }}"
notify: "{{ ca_options.notify | d(omit) }}"
- name: generate in-memory csr request for private key
community.crypto.openssl_csr_pipe:
basic_constraints:
- 'CA:FALSE'
basic_constraints_critical: yes
digest: "{{ kt.digest | d(omit) }}"
key_usage_critical: yes
privatekey_path: "{{ key_path }}"
common_name: "{%- if item.cn is defined -%}{{ item.cn }}\
{%- elif ca_options.cn is defined -%}{{ ca_options.cn }}\
{%- elif ca_preset.cn == 'FQDN' -%}{{ host_name ~ '.' ~ ca_tld }}\
{%- elif ca_preset.cn is defined -%}{{ ca_preset.cn }}\
{%- endif -%}"
extended_key_usage: "{%- if item.eku is defined -%}{{ item.eku }}\
{%- elif ca_options.eku is defined -%}{{ ca_options.eku }}\
{%- elif ca_preset.eku is defined -%}{{ ca_preset.eku }}\
{%- endif -%}"
key_usage: "{%- if item.ku is defined -%}{{ item.ku }}\
{%- elif ca_options.ku is defined -%}{{ ca_options.ku }}\
{%- elif ca_preset.ku is defined -%}{{ ca_preset.ku }}\
{%- else -%}{{ ['digitalSignature', 'keyEncipherment', 'keyAgreement'] }}\
{%- endif -%}"
subject_alt_name: "{%- if item.san is defined -%}{{ item.san }}\
{%- elif ca_options.san is defined -%}{{ ca_options.san }}\
{%- elif item.cn is defined -%}{{ ['DNS:' ~ item.cn] }}\
{%- elif ca_options.cn is defined -%}{{ ['DNS:' ~ ca_options.cn] }}\
{%- elif ca_preset.san == 'FQDN' -%}{{ ['DNS:' ~ host_name ~ '.' ~ ca_tld] }}\
{%- elif ca_preset.san is defined -%}{{ ca_preset.san }}\
{%- endif -%}"
ocsp_must_staple: "{{ (has_acme | d(false)) and (ca_options.ocsp_must_staple | d(false)) }}"
register: csr
changed_when: no
- name: check if cert already exists
stat:
path: "{{ cert_path }}"
register: cert_exists
- name: slurp cert if exists
slurp:
src: "{{ cert_path }}"
when: cert_exists.stat.exists
register: cert
- name: check if the cert validity period is about to expire
community.crypto.x509_certificate_info:
content: "{{ cert.content | b64decode }}"
valid_at:
reissue_period: "+{%- if has_acme | d(false) == true -%}45d\
{%- else -%}{{ ca_reissue_period | d('8w') }}\
{%- endif -%}"
when: cert_exists.stat.exists
register: cert_info
- block:
- name: generate certificate on ca
community.crypto.x509_certificate_pipe:
content: "{{ (cert.content | b64decode) if cert_exists.stat.exists else omit }}"
csr_content: "{{ csr.csr }}"
provider: ownca
ownca_not_after: "{{ item.duration | d('+365d') }}"
ownca_not_before: -1d
ownca_digest: "{{ kt.digest | d(omit) }}"
ownca_path: "{{ ca_dir }}/{{ ca_ip }}{{ kt.name }}.{{ ca_crt_ext }}"
ownca_privatekey_path: "{{ ca_dir }}/{{ ca_ip }}{{ kt.name }}.{{ ca_key_ext }}"
ownca_privatekey_passphrase: "{{ ca_pk_inter_password }}"
force: "{{ cert_exists.stat.exists and not cert_info.valid_at.reissue_period }}"
register: cert
delegate_to: "{{ services.ca.hostname }}"
notify: "{{ ca_options.notify | d(omit) }}"
- name: save new cert if it was changed
copy:
dest: "{{ cert_path }}"
content: "{{ cert.certificate }}"
mode: "{{ k_mode | d(omit) }}"
owner: "{{ k_owner | d(omit) }}"
group: "{{ k_group | d(omit) }}"
follow: "{{ (ca_options | combine(item)).follow_symlinks | d(omit) }}"
when: cert is changed
notify: "{{ ca_options.notify | d(omit) }}"
when: has_acme | d(false) == false
- name: generate acme certificate
include_tasks: gen_acme.yml
when: has_acme | d(false) == true
- block:
- name: slurp certificate
slurp:
src: "{{ cert_path }}"
register: cert
- name: complete certificate chain
community.crypto.certificate_complete_chain:
input_chain: "{{ ((cert.content | b64decode).split('\n\n'))[0] }}"
root_certificates: /etc/ssl/certs
register: chain
- name: save chain to file
copy:
dest: "{{ item.chain }}"
content: |
{% set result = chain.complete_chain %}
{% set _ = result.pop(0) %}
{{ result | join('') }}
mode: "{{ k_mode | d(omit) }}"
owner: "{{ k_owner | d(omit) }}"
group: "{{ k_group | d(omit) }}"
follow: "{{ (ca_options | combine(item)).follow_symlinks | d(omit) }}"
notify: "{{ ca_options.notify | d(omit) }}"
when: item.chain is string
- block:
- name: slurp intermediate from ca
slurp:
src: "{{ ca_dir }}/{{ ca_ip }}{{ kt.name }}.{{ ca_crt_ext }}"
register: inter
delegate_to: "{{ services.ca.hostname }}"
- name: add intermediate cert if requested
blockinfile:
block: "{{ inter.content | b64decode }}"
insertafter: EOF
marker: ""
path: "{{ cert_path }}"
notify: "{{ ca_options.notify | d(omit) }}"
when: (use_acme | d(false) == false) and (cert is changed) and ((ca_options | combine(item)).concat_inter | d(true) == true)
- block:
- name: slurp root from ca
slurp:
src: "{{ ca_dir }}/{{ ca_rp }}{{ kt.name }}.{{ ca_crt_ext }}"
register: root
delegate_to: "{{ services.ca.hostname }}"
- name: add root cert if requested
blockinfile:
block: "{{ root.content | b64decode }}"
insertafter: EOF
marker: ""
path: "{{ cert_path }}"
notify: "{{ ca_options.notify | d(omit) }}"
when: (use_acme | d(false) == false) and (cert is changed) and ((ca_options | combine(item)).concat_root | d(false) == true)

@ -0,0 +1,44 @@
- include_tasks: prepare_item.yml
- name: slurp root from ca
slurp:
src: "{{ ca_dir }}/{{ ca_rp }}{{ kt.name }}.{{ ca_crt_ext }}"
register: root
delegate_to: "{{ services.ca.hostname }}"
- name: copy root to memory
set_fact:
"root_{{ kt.name }}": "{{ root.content | b64decode }}"
when: (ca_options | combine(item)).memory | d(false) == true
- name: copy root to remote node
copy:
dest: "{%- if item.path is defined -%}{{ item.path }}\
{%- else -%}{{ ca_options.path ~ '/' ~ ca_rp ~ kt.name ~ '.' ~ ca_crt_ext }}\
{%- endif -%}"
content: "{{ root.content | b64decode }}"
mode: "{{ k_mode | d(omit) }}"
owner: "{{ k_owner | d(omit) }}"
group: "{{ k_group | d(omit) }}"
when: (ca_options | combine(item)).path is defined
- name: copy root to system storage
block:
- name: ensure ca-certificates is installed
package:
name: ca-certificates
- name: upload root cert to user cert storage
copy:
dest: "/usr/local/share/ca-certificates/{{ ca_rp }}{{ kt.name }}.{{ ca_crt_ext }}"
content: "{{ root.content | b64decode }}"
- name: update ca certificates
command: /usr/sbin/update-ca-certificates
changed_when: no
when: (ca_options | combine(item)).system | d(false) == true

@ -0,0 +1,18 @@
- block:
- name: check if acme main account exists
community.crypto.acme_account_info:
account_key_src: "{{ ca_dir ~ '/acme-main.' ~ ca_key_ext }}"
account_key_passphrase: "{{ ca_acme_account_key_password }}"
acme_directory: "{{ ca_acme_endpoint | d('https://acme-v02.api.letsencrypt.org/directory') }}"
acme_version: "{{ ca_acme_version | d(2) }}"
register: acme_info
delegate_to: "{{ services.ca.hostname }}"
- name: determine acme support
set_fact:
has_acme: "{{ acme_info is defined and acme_info.exists and acme_info.account.status == 'valid' and (acme_disable | d(false) == false) }}"
rescue:
- name: revert has_acme
set_fact:
has_acme: false

@ -0,0 +1,86 @@
- name: define some acme parameters
set_fact:
acme_staging: "{{ (ca_options | d({}) | combine(item)).acme_staging | d(false) }}"
acme_upgrade_int_ca: "{{ cert_info is defined and ((cert_info.ocsp_uri is not defined) or (cert_info.ocsp_uri == None)) }}"
- name: determine if acme cert generation will be forced
set_fact:
acme_forced: "{{ acme_upgrade_int_ca or (always_update_acme is defined) }}"
- name: slurp account key from ca
slurp:
src: "{{ ca_dir ~ '/acme-' ~ ('staging' if acme_staging == true else 'main') ~ '.' ~ ca_key_ext }}"
register: acme_account_key
delegate_to: "{{ services.ca.hostname }}"
- name: define args for acme certificate generation
set_fact:
acme_common_args:
account_key_content: "{{ acme_account_key.content | b64decode }}"
account_key_passphrase: "{{ ca_acme_account_key_password }}"
acme_directory: "{%- if (acme_staging == false) or (acme_staging == None) -%}{{ ca_acme_endpoint | d('https://acme-v02.api.letsencrypt.org/directory') }}\
{%- else -%}{{ ca_acme_staging_endpoint | d('https://acme-staging-v02.api.letsencrypt.org/directory') }}\
{%- endif -%}"
acme_version: "{{ ca_acme_version | d(2) }}"
acme_extra_args:
challenge: dns-01
csr_content: "{{ csr.csr }}"
fullchain_dest: "{{ cert_path if ((ca_options | d({}) | combine(item)).concat_inter | d(true) == true) else omit }}"
dest: "{{ cert_path if ((ca_options | d({}) | combine(item)).concat_inter | d(true) == false) else omit }}"
modify_account: no
remaining_days: 45
force: "{{ acme_forced }}"
terms_agreed: yes
- name: generate acme challenge request
community.crypto.acme_certificate:
args: "{{ acme_common_args | combine(acme_extra_args) }}"
register: challenge
changed_when: no
- block:
- name: unset challenge_records
set_fact:
challenge_records: "{{ [] }}"
- name: fill challenge records
set_fact:
challenge_records: "{{ challenge_records + [{
'name': item2.key | regex_search('(.*).' ~ (tld | regex_escape()), '\\1') | first,
'type': 'TXT',
'value': item2.value[0]
}] }}"
loop: "{{ challenge['challenge_data_dns'] | dict2items }}"
loop_control:
loop_var: item2
- include_tasks: gen_acme_include.yml
- block:
- name: revoke cert if it already exists
community.crypto.acme_certificate_revoke:
certificate: "{{ cert_path }}"
revoke_reason: 4
args: "{{ acme_common_args }}"
when: (cert_exists is defined) and cert_exists.stat.exists and not acme_upgrade_int_ca
rescue:
- debug:
msg: failed to revoke certificate, ignoring
- name: finalize acme challenge request
community.crypto.acme_certificate:
data: "{{ challenge }}"
args: "{{ acme_common_args | combine(acme_extra_args) }}"
notify: "{{ ca_options.notify | d(omit) }}"
when: (challenge.cert_days is not defined) or (challenge.cert_days < 45) or acme_forced

@ -0,0 +1,7 @@
- name: add records to external ns
include_role:
name: external_ns
vars:
nse_items: "{{ challenge_records }}"
nse_function: add_records
nse_instant: true

@ -0,0 +1,74 @@
- name: define dh param dict
set_fact:
dh: "{{ {'remote_gen': true, 'size': 2048, 'backup': false} | combine(dh_params | d({})) }}"
- name: check if dhparam file exists
stat:
path: "{{ dh.path | mandatory }}"
register: res
- block:
- name: ensure cryptography toolkit is installed
include_tasks: tasks/install_packages.yml
vars:
package:
- alpine: py3-cryptography
debian: python3-cryptography
when: dh.remote_gen == false
- block:
- name: wait until ca becomes available
wait_for_connection:
timeout: 10
- name: create temporary file for dh params
tempfile:
state: file
register: tf
delegate_to: "{{ services.ca.hostname }}"
when: dh.remote_gen == true
- name: generate dh params
community.crypto.openssl_dhparam:
path: "{%- if dh.remote_gen == false -%}{{ dh.path | mandatory }}\
{%- else -%}{{ tf.path }}\
{%- endif -%}"
size: "{{ dh.size }}"
backup: "{{ dh.backup }}"
mode: "{{ (dh.mode | d('0400')) if (dh.remote_gen == false) else '0400' }}"
owner: "{{ (dh.owner | d(omit)) if (dh.remote_gen == false) else omit }}"
group: "{{ (dh.group | d(omit)) if (dh.remote_gen == false) else omit }}"
return_content: "{{ dh.remote_gen == true }}"
delegate_to: "{{ inventory_hostname if (dh.remote_gen == false) else services.ca.hostname }}"
notify: "{{ dh.notify | d(omit) }}"
register: dh_result
- block:
- name: remove temporary file
file:
path: "{{ tf.path }}"
state: absent
delegate_to: "{{ services.ca.hostname }}"
- name: copy dh result to remote node
copy:
content: "{{ dh_result.dhparams }}"
dest: "{{ dh.path | mandatory }}"
mode: "{{ dh.mode | d('0400') }}"
owner: "{{ dh.owner | d(omit) }}"
group: "{{ dh.group | d(omit) }}"
when: dh.remote_gen == true
when: (not res.stat.exists) or (dh.remote_gen == false)
- name: unset dh param dict
set_fact:
dh: "{{ {} }}"

@ -0,0 +1,154 @@
- name: ensure cryptography toolkit is installed
include_tasks: tasks/install_packages.yml
vars:
package:
- alpine: py3-cryptography
debian: python3-cryptography
- name: early check to ensure ca variables are defined
fail:
msg: "\"{{ item }}\" is not defined"
when: item is not defined
loop:
- ca_dir
- ca_key_types
- ca_rp
- ca_ip
- ca_crt_ext
- ca_csr_ext
- ca_key_ext
- name: create ca directories
file:
path: "{{ ca_dir }}"
state: directory
mode: 0700
- name: generate root private keys
community.crypto.openssl_privatekey:
path: "{{ ca_dir }}/{{ ca_rp }}{{ item.name }}.{{ ca_key_ext }}"
size: "{{ item.size | d(omit) }}"
curve: "{{ item.curve | d(omit) }}"
type: "{{ item.type }}"
backup: yes
cipher: auto
force: no
format: pkcs8
format_mismatch: convert
passphrase: "{{ ca_pk_password }}"
regenerate: never
mode: 0600
loop: "{{ ca_key_types }}"
- name: generate csr requests for all root keys
community.crypto.openssl_csr:
path: "{{ ca_dir }}/{{ ca_rp }}{{ item.name }}.{{ ca_csr_ext }}"
basic_constraints:
- 'CA:TRUE'
basic_constraints_critical: yes
common_name: "{{ org }} Root CA ({{ item.type | upper }})"
digest: "{{ item.digest | d(omit) }}"
key_usage:
- keyCertSign
- cRLSign
key_usage_critical: yes
privatekey_path: "{{ ca_dir }}/{{ ca_rp }}{{ item.name }}.{{ ca_key_ext }}"
privatekey_passphrase: "{{ ca_pk_password }}"
use_common_name_for_san: no
mode: 0600
loop: "{{ ca_key_types }}"
- name: generate root certificates
community.crypto.x509_certificate:
path: "{{ ca_dir }}/{{ ca_rp }}{{ item.name }}.{{ ca_crt_ext }}"
csr_path: "{{ ca_dir }}/{{ ca_rp }}{{ item.name }}.{{ ca_csr_ext }}"
privatekey_path: "{{ ca_dir }}/{{ ca_rp }}{{ item.name }}.{{ ca_key_ext }}"
privatekey_passphrase: "{{ ca_pk_password }}"
provider: selfsigned
selfsigned_not_after: "{{ ca_root_valid_until | mandatory }}"
selfsigned_digest: "{{ item.digest | d(omit) }}"
mode: 0600
loop: "{{ ca_key_types }}"
- name: generate inter private keys
community.crypto.openssl_privatekey:
path: "{{ ca_dir }}/{{ ca_ip }}{{ item.name }}.{{ ca_key_ext }}"
size: "{{ item.size | d(omit) }}"
curve: "{{ item.curve | d(omit) }}"
type: "{{ item.type }}"
backup: yes
cipher: auto
force: no
format: pkcs8
format_mismatch: convert
passphrase: "{{ ca_pk_inter_password }}"
regenerate: never
mode: 0600
loop: "{{ ca_key_types }}"
- name: generate csr requests for all inter keys
community.crypto.openssl_csr:
path: "{{ ca_dir }}/{{ ca_ip }}{{ item.name }}.{{ ca_csr_ext }}"
basic_constraints:
- 'CA:TRUE'
- 'pathlen:0'
basic_constraints_critical: yes
common_name: "{{ org }} Intermediate CA ({{ item.type | upper }})"
digest: "{{ item.digest | d(omit) }}"
key_usage:
- digitalSignature
- keyCertSign
- cRLSign
key_usage_critical: yes
privatekey_path: "{{ ca_dir }}/{{ ca_ip }}{{ item.name }}.{{ ca_key_ext }}"
privatekey_passphrase: "{{ ca_pk_inter_password }}"
use_common_name_for_san: no
crl_distribution_points:
- full_name: "URI:http://crl.{{ int_tld }}/{{ item.name }}.crl"
crl_issuer: "URI:http://crl.{{ int_tld }}"
name_constraints_permitted:
- "DNS:{{ tld }}"
- "email:{{ tld }}"
name_constraints_excluded:
- "IP:0.0.0.0/0"
mode: 0600
loop: "{{ ca_key_types }}"
- name: generate inter certificates
community.crypto.x509_certificate:
path: "{{ ca_dir }}/{{ ca_ip }}{{ item.name }}.{{ ca_crt_ext }}"
csr_path: "{{ ca_dir }}/{{ ca_ip }}{{ item.name }}.{{ ca_csr_ext }}"
privatekey_path: "{{ ca_dir }}/{{ ca_ip }}{{ item.name }}.{{ ca_key_ext }}"
privatekey_passphrase: "{{ ca_pk_inter_password }}"
provider: ownca
ownca_not_after: "{{ ca_inter_valid_until | mandatory }}"
ownca_digest: "{{ item.digest | d(omit) }}"
ownca_path: "{{ ca_dir }}/{{ ca_rp }}{{ item.name }}.{{ ca_crt_ext }}"
ownca_privatekey_path: "{{ ca_dir }}/{{ ca_rp }}{{ item.name }}.{{ ca_key_ext }}"
ownca_privatekey_passphrase: "{{ ca_pk_password }}"
mode: 0600
loop: "{{ ca_key_types }}"
- name: install acme
include_tasks: install_acme.yml
- name: add directories to backup plan
include_role:
name: backup
vars:
function: add
backup_items:
- "{{ ca_dir }}"

@ -0,0 +1,39 @@
- name: select key type for acme
set_fact:
kt: "{{ ca_key_types | selectattr('name', 'equalto', ca_acme_account_key_type | d('ecc384')) | list | first }}"
- name: generate acme account keys
community.crypto.openssl_privatekey:
path: "{{ ca_dir ~ '/acme-' ~ item ~ '.' ~ ca_key_ext }}"
size: "{{ kt.size | d(omit) }}"
curve: "{{ kt.curve | d(omit) }}"
type: "{{ kt.type }}"
backup: yes
cipher: auto
force: no
format: pkcs8
format_mismatch: convert
passphrase: "{{ ca_acme_account_key_password }}"
regenerate: never
mode: 0600
loop:
- main
- staging
- name: create acme accounts
community.crypto.acme_account:
account_key_src: "{{ ca_dir ~ '/acme-' ~ item ~ '.' ~ ca_key_ext }}"
account_key_passphrase: "{{ ca_acme_account_key_password }}"
acme_directory: "{%- if item == 'main' -%}{{ ca_acme_endpoint | d('https://acme-v02.api.letsencrypt.org/directory') }}\
{%- else -%}{{ ca_acme_staging_endpoint | d('https://acme-staging-v02.api.letsencrypt.org/directory') }}\
{%- endif -%}"
acme_version: "{{ ca_acme_version | d(2) }}"
contact:
- "mailto:{{ maintainer_email | d('admin@' ~ tld) }}"
state: present
terms_agreed: yes
loop:
- main
- staging

@ -0,0 +1,51 @@
- name: ca installation
include_tasks: install.yml
when: function == 'install'
- name: install roots
include_tasks: add_root.yml
loop: "{{ ca_default_items if (ca_roots is not defined) or (ca_roots == None) or ((ca_roots | length) == 0) else ca_roots }}"
when: function == 'roots'
- block:
- name: wait until ca becomes available
wait_for_connection:
timeout: 10
delegate_to: "{{ services.ca.hostname }}"
- name: check if acme can be used
include_tasks: check_acme.yml
- name: process roots if no acme will be used
include_tasks: add_root.yml
loop: "{{ ca_default_items if (ca_roots is not defined) or (ca_roots == None) or ((ca_roots | length) == 0) else ca_roots }}"
when: not has_acme
- name: ensure cryptography toolkit is installed
include_tasks: tasks/install_packages.yml
vars:
package:
- alpine: py3-cryptography
debian: python3-cryptography
- name: process certificates
include_tasks: add_cert.yml
loop: "{{ ca_default_items if (ca_certs is not defined) or (ca_certs == None) or ((ca_certs | length) == 0) else ca_certs }}"
when: function == 'certs'
- name: generate dhparams
include_tasks: gen_dhparam.yml
when: (function == 'dhparam' or function == 'dhparams')
- name: check acme availability
include_tasks: check_acme.yml
when: function == 'check_acme'

@ -0,0 +1,17 @@
- name: select key type
set_fact:
kt: "{{ ca_key_types | selectattr('name', 'equalto', item.type) | list | first }}"
- name: fail if key type is empty
fail:
msg: "key type must be one of: {{ ca_key_names | join(', ') }}"
when: (kt | length) == 0
- name: set preferred mode, owner and group
set_fact:
k_mode: "{{ (ca_options | d({}) | combine(item)).mode | d(omit) }}"
k_owner: "{{ (ca_options | d({}) | combine(item)).owner | d(omit) }}"
k_group: "{{ (ca_options | d({}) | combine(item)).group | d(omit) }}"
k_none: yes

@ -0,0 +1,24 @@
cdr_user: cdr
cdr_group: cdr
cdr_dir: /opt/cdr
cdr_port: 3000
cdr_default_config:
port: "{{ cdr_port }}"
db_type: pg
db_host: "{{ database_host }}"
db_user: "{{ database_user }}"
db_password: "{{ database_pass }}"
db_database: "{{ database_name }}"
db_table: cdr
record_dir: /opt/recordings
record_pretty_names: yes
ami_enable: yes
ami_host: 127.0.0.1
originate_enable: yes
originate_context: outbound
originate_timeout: 30

@ -0,0 +1,4 @@
- name: restart cdr
service:
name: cdr
state: restarted

@ -0,0 +1,141 @@
- name: set cdr_cfg
set_fact:
cdr_cfg: "{{ cdr_default_config | d({}) | combine(cdr_config | d({}), recursive=true) }}"
- name: install dependencies
include_tasks: tasks/install_packages.yml
vars:
package:
- nodejs
- npm
- name: add extra cname record
include_role:
name: ns
vars:
function: add_records
ns_add_default_record: no
ns_records:
- name: cdr
type: CNAME
value: "{{ host_fqdn }}"
when: "inventory_hostname != 'cdr'"
- name: create user and group
include_tasks: tasks/create_user.yml
vars:
user:
name: "{{ cdr_user }}"
group: "{{ cdr_group }}"
dir: "{{ cdr_dir }}"
notify: restart cdr
- name: ensure cdr dir exists
file:
path: "{{ cdr_dir }}"
state: directory
owner: "{{ cdr_user }}"
group: "{{ cdr_group }}"
- name: ensure recordings dir exists
file:
path: "{{ cdr_cfg.record_dir }}"
state: directory
- name: get source-mark status
stat:
path: "{{ cdr_dir }}/source-mark"
register: source_mark
- name: pause if source-mark is missing
pause:
prompt: source-mark is missing, source code has to be manually uploaded
when: source_mark.stat.exists == false
- name: create source-mark
file:
path: "{{ cdr_dir }}/source-mark"
state: touch
modification_time: preserve
access_time: preserve
- name: template env file
template:
src: env.j2
dest: "{{ cdr_dir }}/.env"
force: yes
owner: "{{ cdr_user }}"
group: "{{ cdr_group }}"
lstrip_blocks: yes
notify: restart cdr
- name: ensure app script has executable bit set
file:
path: "{{ cdr_dir }}/app.js"
mode: "+x"
- name: install npm dependencies
npm:
path: "{{ cdr_dir }}"
no_optional: yes
ignore_scripts: yes
production: yes
become: yes
become_user: "{{ cdr_user }}"
become_method: su
become_flags: '-s /bin/ash'
notify: restart cdr
changed_when: no
- name: template init script
template:
src: init.j2
dest: /etc/init.d/cdr
force: yes
mode: "+x"
notify: restart cdr
- name: install and configure nginx
include_role:
name: nginx
vars:
nginx:
servers:
- conf: nginx_server
override_server_name: cdr
certs: "{{ host_tls }}"
domains:
- "cdr.{{ host_tld }}"
- name: flush handlers
meta: flush_handlers
- name: add directories to backup plan
include_role:
name: backup
vars:
function: add
backup_items:
- "{{ cdr_dir }}"
- name: enable and start cdr
service:
name: cdr
enabled: yes
state: started

@ -0,0 +1,3 @@
{% for option in (cdr_cfg | d({}) | dict2items) -%}
{{ option.key | upper }}={{ option.value | quote }}
{% endfor -%}

@ -0,0 +1,14 @@
#!/sbin/openrc-run
name="$SVCNAME"
directory="{{ cdr_dir }}"
command="node {{ cdr_dir }}/app.js"
command_user="{{ cdr_user }}:{{ cdr_group }}"
pidfile="/var/run/$SVCNAME.pid"
supervisor="supervise-daemon"
respawn_max=0
depend() {
need net
use dns
}

@ -0,0 +1,11 @@
set_real_ip_from 10.0.0.0/8;
real_ip_header X-Real-IP;
real_ip_recursive on;
location / {
proxy_pass http://127.0.0.1:{{ cdr_port }};
proxy_http_version 1.1;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}

@ -0,0 +1,24 @@
- name: set combined cert info
set_fact:
combined: "{{ (common | d({})) | combine(cert | d({}), recursive=true) }}"
- name: generate certificate through acme-dns
include_role:
name: acme
vars:
acme_id: "{{ cert.id | d(host_name ~ ('-ecc' if (combined.ecc | d(false) == true) else '')) }}"
acme_cert: "{{ cert.cert }}"
acme_key: "{{ cert.key }}"
acme_chain: "{{ cert.chain | d(None) }}"
acme_cert_single: "{{ cert.cert_single | d(None) }}"
acme_ecc: "{{ combined.ecc | d(false) }}"
acme_stapling: no
acme_notify: "{{ combined.notify | d(None) }}"
acme_owner: "{{ combined.owner | d(None) }}"
acme_group: "{{ combined.group | d(None) }}"
acme_post_hook: "{{ combined.post_hook | d(None) }}"
acme_hostname: "{{ combined.hostname | d(None) }}"
acme_tld: "{{ combined.tld | d(None) }}"
acme_fqdn: "{{ combined.fqdn | d(None) }}"
acme_hosts: "{{ combined.hosts | d(None) }}"

@ -0,0 +1,46 @@
- name: set combined cert info
set_fact:
combined: "{{ cert | combine(common | d({}), recursive=true) }}"
- name: clear san list
set_fact:
san_list: "{{ [] }}"
- block:
- name: build san list
set_fact:
san_list: "{{ (san_list | d([])) + ['DNS:' ~ (item.fqdn | d((item.hostname | d(host_name)) ~ '.' ~ (item.tld | d(host_tld))))] }}"
loop: "{{ cert.hosts }}"
when: (cert.hosts is defined) and (cert.hosts | type_debug == 'list')
- name: generate certificate through external ns
include_role:
name: ca
vars:
function: certs
ca_options:
mode: '0400'
owner: "{{ combined.owner | d(None) }}"
group: "{{ combined.group | d(None) }}"
concat_inter: yes
preset: web
acme: yes
ocsp_must_staple: "{{ combined.stapling | d(false) }}"
notify: "{{ combined.notify | d(None) }}"
ca_certs:
- type: "{{ 'ecc384' if (combined.ecc | d(false) == true) else 'rsa2048' }}"
cert: "{{ cert.cert }}"
key: "{{ cert.key }}"
cn: "{% if cert.hosts is defined and cert.hosts | type_debug == 'list' -%}\
{{ cert.hosts[0].fqdn | d((cert.hosts[0].hostname | d(host_name)) ~ '.' ~ (cert.hosts[0].tld | d(host_tld))) }}\
{%- else -%}\
{{ combined.fqdn | d((combined.hostname | d(host_name)) ~ '.' ~ (combined.tld | d(host_tld))) }}\
{%- endif -%}"
san: "{% if san_list | length > 0 -%}\
{{ san_list }}\
{%- else -%}\
{{ 'DNS:' ~ (combined.fqdn | d((combined.hostname | d(host_name)) ~ '.' ~ (combined.tld | d(host_tld)))) }}\
{%- endif -%}"

@ -0,0 +1,2 @@
- fail:
msg: deployment of certs through internal CA is not implemented

@ -0,0 +1,41 @@
- name: validate cert parameter
fail:
msg: certs variable must be a dict or a list
when: (certs is not defined) or ((certs is not mapping) and (certs | type_debug != 'list'))
- name: validate common parameter
fail:
msg: common variable must be a dict
when: (common is defined) and (common is not mapping)
- name: validate certificates
include_tasks: validate.yml
loop: "{{ certs if (certs | type_debug == 'list') else [certs] }}"
loop_control:
loop_var: cert
- name: process certificates with acme dns
include_tasks: acme_dns.yml
loop: "{{ certs if (certs | type_debug == 'list') else [certs] }}"
loop_control:
loop_var: cert
when: services.acme_dns is defined
- name: process certificates with standalone dns
include_tasks: external_ns.yml
loop: "{{ certs if (certs | type_debug == 'list') else [certs] }}"
loop_control:
loop_var: cert
when: (services.external_ns is defined) and (services.acme_dns is not defined)
- name: process certificates with internal ca
include_tasks: internal_ca.yml
loop: "{{ certs if (certs | type_debug == 'list') else [certs] }}"
loop_control:
loop_var: cert
when: (services.ca is defined) and (services.external_ns is not defined) and (services.acme_dns is not defined)

@ -0,0 +1,46 @@
- name: validate mandatory parameters
fail:
msg: some mandatory parameters in cert variable are missing or invalid
when: (cert is not defined) or (cert is not mapping) or
(cert.key is not string) or (cert.cert is not string)
- name: validate optional parameters
fail:
msg: some optional parameters in cert variable are missing or invalid
when: ((cert.ca is defined) and (cert.ca is not string)) or
((cert.id is defined) and (cert.id is not string)) or
((cert.ecc is defined) and (cert.ecc is not boolean)) or
((cert.fqdn is defined) and (cert.fqdn is not string)) or
((cert.tld is defined) and (cert.tld is not string)) or
((cert.hostname is defined) and (cert.hostname is not string)) or
((cert.hosts is defined) and (cert.hosts | type_debug != 'list')) or
((cert.tld is defined) and (cert.tld is not string)) or
((cert.stapling is defined) and (cert.stapling is not boolean)) or
((cert.post_hook is defined) and (cert.post_hook is not string)) or
((cert.notify is defined) and (cert.notify is not string)) or
((cert.owner is defined) and (cert.owner is not string)) or
((cert.group is defined) and (cert.group is not string))
- name: validate parameter combinations
fail:
msg: parameters are defined in an invalid combination
when: ((cert.fqdn is defined) and (cert.hosts is defined)) or
((cert.tld is defined) and (cert.hosts is defined)) or
((cert.hostname is defined) and (cert.hosts is defined)) or
((cert.fqdn is defined) and (cert.tld is defined)) or
((cert.fqdn is defined) and (cert.hostname is defined))
- name: validate hosts
fail:
msg: host parameters are invalid or are defined in an invalid combination
when: ((host.fqdn is defined) and (host.fqdn is not string)) or
((host.tld is defined) and (host.tld is not string)) or
((host.hostname is defined) and (host.hostname is not string)) or
((host.fqdn is defined) and (host.tld is defined)) or
((host.fqdn is defined) and (host.hostname is defined))
loop: "{{ cert.hosts }}"
loop_control:
loop_var: host

@ -0,0 +1,72 @@
clamav_user: clamav
clamav_group: clamav
clamav_conf_dir: /etc/clamav
clamav_db_dir: /opt/clamav
clamav_conf_file: "{{ clamav_conf_dir }}/clamd.conf"
clamav_freshclam_conf_file: "{{ clamav_conf_dir }}/freshclam.conf"
clamav_milter_conf_file: "{{ clamav_conf_dir }}/clamav-milter.conf"
clamav_socket: /run/clamav/clamd.sock
clamav_max_file_size: "{{ mail_server.max_mail_size_bytes | d('25M') }}"
clamav_default_config:
clamav:
log_syslog: yes
log_facility: LOG_LOCAL0
extended_detection_info: yes
pid_file: /run/clamav/clamd.pid
database_directory: "{{ clamav_db_dir }}"
local_socket: "{{ clamav_socket }}"
local_socket_mode: 660
stream_max_length: "{{ clamav_max_file_size }}"
self_check: 3600
concurrent_database_reload: no
user: "{{ clamav_user }}"
detect_p_u_a: yes
heuristic_scan_precedence: no
alert_encrypted: yes
alert_encrypted_archive: yes
alert_encrypted_doc: yes
max_scan_time: 30000
max_file_size: "{{ clamav_max_file_size }}"
max_recursion: 12
alert_exceeds_max: yes
bytecode: yes
bytecode_security: Paranoid
freshclam:
log_syslog: yes
log_facility: LOG_LOCAL0
pid_file: /run/clamav/freshclam.pid
database_directory: "{{ clamav_db_dir }}"
database_owner: "{{ clamav_user }}"
update_log_file: /dev/stdout
checks: 4
test_databases: no
bytecode: yes
safe_browsing: yes
notify_clamd: "{{ clamav_conf_file }}"
scripted_updates: no
private_mirror: https://packages.microsoft.com/clamav
milter:
log_syslog: yes
log_facility: LOG_LOCAL0
log_infected: Basic
log_clean: Basic
milter_socket: "inet:{{ mail_server.clamav_port | d(7357) }}"
user: "{{ clamav_user }}"
clamd_socket: "unix:{{ clamav_socket }}"
max_file_size: "{{ clamav_max_file_size }}"
on_infected: Reject
add_header: Add
report_hostname: "{{ (mail_server.mta_actual_hostname ~ '.' ~ mail_server.tld) if
(mail_server.mta_actual_hostname is defined) and (mail_server.tld is defined) else 'clamav' }}"
support_multiple_recipients: yes
foreground: yes

@ -0,0 +1,16 @@
- name: restart clamd
service:
name: clamd
state: restarted
- name: restart freshclam
service:
name: freshclam
state: restarted
- name: restart clamav milter
service:
name: clamav-milter
state: restarted

@ -0,0 +1,97 @@
- name: set clamav_cfg
set_fact:
clamav_cfg: "{{ clamav_default_config | d({}) | combine(clamav_config | d({}), recursive=true) }}"
- name: install dependencies
include_tasks: tasks/install_packages.yml
vars:
package:
- clamav-daemon
- alpine: clamav-daemon-openrc
- clamav-milter
- name: create user and group
include_tasks: tasks/create_user.yml
vars:
user:
name: "{{ clamav_user }}"
group: "{{ clamav_group }}"
- name: create directories
file:
path: "{{ item }}"
state: directory
mode: 0700
owner: "{{ clamav_user }}"
group: "{{ clamav_group }}"
loop:
- "{{ clamav_conf_dir }}"
- "{{ clamav_db_dir }}"
- name: template clamav configs
template:
src: config.j2
dest: "{{ item.dest }}"
force: yes
mode: 0400
owner: "{{ clamav_user }}"
group: "{{ clamav_group }}"
lstrip_blocks: yes
notify: "{{ item.notify }}"
loop:
- { dest: "{{ clamav_conf_file }}", section: "clamav", notify: "restart clamd" }
- { dest: "{{ clamav_freshclam_conf_file }}", section: "freshclam", notify: "restart freshclam" }
- { dest: "{{ clamav_milter_conf_file }}", section: "milter", notify: "restart clamav milter" }
- name: edit init script for clamd
lineinfile:
path: /etc/init.d/clamd
regexp: '^CONF='
line: 'CONF={{ clamav_conf_file | quote }}'
notify: restart clamd
- name: edit init script for freshclam
lineinfile:
path: /etc/init.d/freshclam
regexp: '^CONF='
line: 'CONF={{ clamav_freshclam_conf_file | quote }}'
notify: restart freshclam
- name: template init script for clamav milter
template:
src: milter_init.j2
dest: /etc/init.d/clamav-milter
force: yes
mode: "+x"
notify: restart clamav milter
- name: flush handlers
meta: flush_handlers
- name: add directories to backup plan
include_role:
name: backup
vars:
function: add
backup_items:
- "{{ clamav_conf_dir }}"
- name: enable and start services
service:
name: "{{ item }}"
enabled: yes
state: started
loop:
- clamd
- freshclam
- clamav-milter

@ -0,0 +1,16 @@
{% macro clamav_option(option) -%}
{% set key = option.key.split('_') | map('capitalize') | join('') -%}
{% if option.value is boolean -%}
{{ key }} {{ 'yes' if option.value else 'no' }}
{% elif option.value != None -%}
{{ key }} {{ option.value }}
{% endif -%}
{% endmacro -%}
{% if clamav_cfg[item.section] is mapping -%}
{% for option in (clamav_cfg[item.section] | d({}) | dict2items) -%}
{{ clamav_option(option) }}
{%- endfor %}
{% endif -%}

@ -0,0 +1,14 @@
#!/sbin/openrc-run
name="$SVCNAME"
directory="{{ clamav_db_dir }}"
command="/usr/sbin/clamav-milter"
command_user="{{ clamav_user ~ ':' ~ clamav_group }}"
pidfile="/var/run/$SVCNAME.pid"
supervisor="supervise-daemon"
depend() {
need net
use dns
after clamd
}

@ -0,0 +1 @@
dropbear_dir: /etc/dropbear

@ -0,0 +1,19 @@
#!/sbin/openrc-run
depend() {
use logger dns
need net
after firewall
}
start() {
ebegin "Starting dropbear"
/usr/sbin/dropbear ${DROPBEAR_OPTS}
eend $?
}
stop() {
ebegin "Stopping dropbear"
start-stop-daemon --stop --pidfile /var/run/dropbear.pid
eend $?
}

@ -0,0 +1,16 @@
- name: restart syslog
service:
name: syslog
state: restarted
- name: restart crond
service:
name: cron
state: restarted
- name: restart dropbear
service:
name: dropbear
state: restarted

@ -0,0 +1,94 @@
- name: setup timezone
shell:
cmd: PATH=$PATH:/sbin; /sbin/setup-timezone -z {{ timezone | quote }}
chdir: /sbin
creates: "{{ ('/etc/zoneinfo', timezone) | path_join }}"
notify: restart syslog
when: timezone is string
- name: flush handlers
meta: flush_handlers
- name: upgrade alpine version
replace:
path: /etc/apk/repositories
regexp: '/alpine/v\d+\.\d+/'
replace: '/alpine/v{{ alpine_version }}/'
- name: change apk repository
replace:
path: /etc/apk/repositories
regexp: '^https:\/\/dl-cdn\.alpinelinux\.org\/alpine\/'
replace: 'https://mirror.yandex.ru/mirrors/alpine/'
when: use_alternative_apk_repo | d(false) == true
- block:
- name: update repository index
community.general.apk:
update_cache: yes
changed_when: no
register: apk_result
rescue:
- name: fix repository keys
command:
cmd: /sbin/apk fix --allow-untrusted alpine-keys
when: "'UNTRUSTED signature' in apk_result.stderr"
- name: update repository index in untrusted mode
command:
cmd: /sbin/apk --allow-untrusted update
when: "'UNTRUSTED signature' in apk_result.stderr"
- name: upgrade basic dependencies in untrusted mode
command:
cmd: /sbin/apk --allow-untrusted upgrade apk-tools alpine-keys
when: "'UNTRUSTED signature' in apk_result.stderr"
- name: update repository index
community.general.apk:
update_cache: yes
changed_when: no
- name: check if there are any updates
command:
cmd: /sbin/apk list -u
register: updates_found
changed_when: no
- name: pause and confirm updates
pause:
prompt: "{{ updates_found.stdout }}"
when: (updates_found.stdout | length > 0) and (interactive | d(true) == true) and (hosts_strategy | d('') != 'free')
changed_when: updates_found.stdout | length > 0
- name: upgrade all packages if updates are found
community.general.apk:
upgrade: yes
when: updates_found.stdout | length > 0
- name: collect apk-new files
find:
paths:
- /etc
- /usr
- /var
patterns: "*.apk-new"
recurse: yes
depth: 8
register: new_files
- name: remove apk-new files
file:
path: "{{ item.path }}"
state: absent
loop: "{{ new_files.files | flatten(levels=1) }}"

@ -0,0 +1,52 @@
- name: set timezone
community.general.timezone:
name: "{{ timezone }}"
notify: restart crond
when: timezone is defined
- name: flush handlers
meta: flush_handlers
- name: update repository index
apt:
force_apt_get: yes
update_cache: yes
changed_when: false
- name: ensure apt-show-versions is installed
apt:
force_apt_get: yes
name: apt-show-versions
state: latest
- name: get upgradeable packages
shell:
cmd: apt-show-versions --upgradeable
register: upgradeable
changed_when: false
- block:
- name: pause and confirm updates
pause:
prompt: "{{ upgradeable.stdout }}"
- name: upgrade all packages
apt:
force_apt_get: yes
install_recommends: no
upgrade: dist
when: "(upgradeable.stdout_lines is defined) and (upgradeable.stdout_lines | length > 0)"
- name: clean repository cache
apt:
force_apt_get: yes
autoclean: yes
autoremove: yes

@ -0,0 +1,147 @@
- block:
- name: try to connect
wait_for_connection:
timeout: 10
- set_fact:
ssh_ok: yes
rescue:
- name: save old ansible ssh args
set_fact:
old_ansible_ssh_extra_args: "{{ ansible_ssh_extra_args | d('') }}"
- name: disable key checking and enable password login
set_fact:
ssh_ok: no
host_key_checking: no
ansible_password: "{{ container_password | d(host_password) }}"
ansible_ssh_extra_args: "{{ ansible_ssh_extra_args | d('') }} -o StrictHostKeyChecking=no"
- name: try to connect without key checking
wait_for_connection:
timeout: 10
- name: gather facts
setup:
gather_subset:
- min
- distribution
- name: generate host ssh key
include_tasks: gen_ssh_key.yml
when: (use_ssh_keys | d(true) == true) and ('containers' not in group_names)
- block:
- name: remove default dropbear keys
file:
path: "{{ (dropbear_dir, item) | path_join }}"
state: absent
loop:
- dropbear_dss_host_key
- dropbear_rsa_host_key
- dropbear_ecdsa_host_key
notify: restart dropbear
- name: generate ed25519 dropbear key if missing
command:
cmd: "dropbearkey -t ed25519 -f {{ (dropbear_dir, 'dropbear_ed25519_host_key') | path_join | quote }}"
creates: "{{ (dropbear_dir, 'dropbear_ed25519_host_key') | path_join }}"
notify: restart dropbear
- name: get remote host public key
command:
cmd: "dropbearkey -y -f {{ (dropbear_dir, 'dropbear_ed25519_host_key') | path_join | quote }}"
register: pubkey
changed_when: no
- name: get actual public key
set_fact:
host_ssh_pubkey: "{{ pubkey.stdout_lines | map('regex_search', '^ssh-ed25519.*$') | select('string') | first }}"
- name: fail if public key is missing
fail:
msg: "remote host ssh public key is missing"
when: host_ssh_pubkey | length == 0
- name: add public key to known_hosts on ansible controller
known_hosts:
key: "{{ ansible_host }} {{ host_ssh_pubkey }}"
name: "{{ ansible_host }}"
delegate_to: localhost
- name: edit dropbear conf file
lineinfile:
path: /etc/conf.d/dropbear
regexp: '^DROPBEAR_OPTS=.*$'
line: "DROPBEAR_OPTS=\"-r {{ (dropbear_dir, 'dropbear_ed25519_host_key') | path_join | quote }} -jk -T 5 -K 360 -I 7200\""
notify: restart dropbear
- name: copy dropbear init file
copy:
src: dropbear_init
dest: /etc/init.d/dropbear
force: yes
notify: restart dropbear
- name: ensure remote host has ansible key in authorized_keys file
lineinfile:
path: /root/.ssh/authorized_keys
line: "{{ container_key.public_key }}"
create: yes
mode: 0400
when: container_key is defined and container_key.public_key is defined
when: ansible_distribution == 'Alpine'
- name: flush handlers
meta: flush_handlers
- name: if key checking was disabled
block:
- name: set it back on
set_fact:
host_key_checking: yes
ansible_ssh_extra_args: "{{ old_ansible_ssh_extra_args }}"
ansible_password: "{{ None }}"
- name: try to connect
wait_for_connection:
timeout: 10
- set_fact:
ssh_ok: true
when: not ssh_ok
- name: add etc directory to backup plan
include_role:
name: backup
vars:
function: add
backup_items:
- /etc
- name: alpine setup
include_tasks: alpine.yml
when: ansible_distribution == 'Alpine'
- name: debian setup
include_tasks: debian.yml
when: ansible_distribution == 'Debian'

@ -0,0 +1,7 @@
container_description: Managed by Ansible
container_pool: production
container_distro: alpine
container_template:
alpine: alpine-3.15-default_20211202_amd64.tar.xz
debian: debian-11-standard_11.3-1_amd64.tar.zst

@ -0,0 +1,186 @@
- name: specify connection parameters
set_fact:
pm_api_host: "{{ hostvars[selected_node]['ansible_host'] | mandatory }}"
pm_api_user: "{{ hostvars[selected_node]['api_user'] | d('root@pam') }}"
pm_api_password: "{{ hostvars[selected_node]['api_password'] | d(hostvars[selected_node]['ansible_password']) }}"
pm_lxc_storage: "{{ hostvars[selected_node]['lxc_storage'] | d('local-zfs') }}"
no_log: yes
- name: validate template and distribution parameters
fail:
msg: some container parameters are missing or invalid
when: (container_distro is not defined) or (container_template is not mapping) or
(container_template[container_distro] is not defined) or
(container_id is not defined) or (container_password is not defined)
- name: ensure pool exists on cluster node
command:
cmd: "pveum pool add {{ container_pool | quote }}"
register: pool_res
changed_when: pool_res.rc == 0
failed_when: (pool_res.rc != 0) and not ((pool_res.rc == 255) and ('already exists' in pool_res.stderr))
when: container_pool is defined
delegate_to: "{{ selected_node }}"
- block:
- name: ensure pip3 is installed on local node
package:
name: py3-pip
run_once: yes
- name: ensure proxmoxer is installed on local node
pip:
name: proxmoxer
run_once: yes
- name: generate host ssh key
include_tasks: gen_ssh_key.yml
when: use_ssh_keys | d(true) == true
- name: ensure there is a container template
community.general.proxmox_template:
node: "{{ selected_node }}"
api_host: "{{ pm_api_host }}"
api_user: "{{ pm_api_user }}"
api_password: "{{ pm_api_password }}"
content_type: vztmpl
template: "{{ container_template[container_distro] }}"
validate_certs: no
timeout: 20
- name: create container if not exists
community.general.proxmox:
node: "{{ selected_node }}"
api_host: "{{ pm_api_host }}"
api_user: "{{ pm_api_user }}"
api_password: "{{ pm_api_password }}"
cores: "{{ hardware.cores }}"
cpus: "{{ hardware.cpus }}"
cpuunits: "{{ hardware.cpuunits }}"
disk: "{{ hardware.disk | string }}"
memory: "{{ hardware.memory }}"
swap: "{{ hardware.swap }}"
description: "{{ container_description | d(omit) }}"
hostname: "{{ inventory_hostname }}"
pool: "{{ container_pool | d(omit) }}"
vmid: "{{ container_id }}"
password: "{{ container_password }}"
pubkey: "{{ (container_key | d({})).public_key | d(omit) }}"
ostemplate: "local:vztmpl/{{ container_template[container_distro] }}"
netif: "{\"net0\":\
\"name=eth0,hwaddr={{ container_mac | d(mac_prefix | community.general.random_mac(seed=inventory_hostname)) }},\
ip={{ ansible_host }}/{{ networks[container_network].gw | ansible.utils.ipaddr('prefix') }},\
gw={{ networks[container_network].gw | ansible.utils.ipaddr('address') }},\
bridge=vmbr0,\
firewall=0,\
tag={{ networks[container_network].tag }},\
type=veth,\
mtu={{ container_mtu | d(hostvars[selected_node]['container_mtu'] | d(1500)) }}\"}"
nameserver: "{%- if container_nameserver is defined -%}\
{{ hostvars[container_nameserver]['ansible_host'] }}\
{%- elif services.filtering_ns is defined -%}\
{%- if services.filtering_ns | type_debug == 'list' -%}
{{ hostvars[services.filtering_ns[0].hostname]['ansible_host'] }}\
{%- else -%}
{{ hostvars[services.filtering_ns.hostname]['ansible_host'] }}\
{%- endif -%}
{%- elif container_default_nameserver is defined -%}\
{{ container_default_nameserver }}\
{%- else -%}\
{{ omit }}\
{%- endif -%}"
onboot: yes
proxmox_default_behavior: no_defaults
storage: "{{ pm_lxc_storage }}"
unprivileged: yes
timeout: 240
mounts: >-
{ {%- for item in (container_mounts | d([])) -%}
"{{ item.id }}":"{{ pm_lxc_storage }}:{{ item.size | mandatory }},mp={{ item.mp | mandatory }}{% if item.readonly is defined and item.readonly %},ro=1{% endif %}",
{%- endfor -%} }
- block:
- name: add features to lxc config
lineinfile:
path: "/etc/pve/lxc/{{ container_id }}.conf"
line: "features: {{ container_features | join(',') }}"
when: container_features | d([]) | length > 0
- name: check that lxc config is correct
lineinfile:
path: "/etc/pve/lxc/{{ container_id }}.conf"
regexp: "^{{ item.name }}:(\\s*).*$"
line: "{{ item.name | mandatory }}:\\g<1>{{ item.value | mandatory }}"
backrefs: yes
loop:
- { name: cpus, value: "{{ hardware.cpus }}" }
- { name: cores, value: "{{ [hardware.cores, hostvars[selected_node]['max_cores'] | d(hardware.cores)] | min }}" }
- { name: cpuunits, value: "{{ hardware.cpuunits }}" }
- { name: memory, value: "{{ hardware.memory }}" }
- { name: swap, value: "{{ hardware.swap }}" }
- { name: onboot, value: "{{ '1' if (container_active | d(true) == true) else '0' }}" }
- name: set startup order and delay
lineinfile:
path: "/etc/pve/lxc/{{ container_id }}.conf"
regexp: '^startup:.*$'
line: "startup: {{ 'order=' ~ (container_order | d(role_dependency[host_primary_role] | d('0'))) ~ ((',up=' ~ container_startup_delay) if container_startup_delay is defined else '') }}"
insertbefore: '^[^\#]'
firstmatch: yes
when: (container_order is defined) or (role_dependency[host_primary_role] is defined) or (container_startup_delay is defined)
- name: ensure that cpulimit is not set
lineinfile:
path: "/etc/pve/lxc/{{ container_id }}.conf"
regexp: '^cpulimit:.*$'
state: absent
delegate_to: "{{ selected_node }}"
- name: start/stop container
community.general.proxmox:
node: "{{ selected_node }}"
api_host: "{{ pm_api_host }}"
api_user: "{{ pm_api_user }}"
api_password: "{{ pm_api_password }}"
vmid: "{{ container_id }}"
proxmox_default_behavior: no_defaults
state: "{{ 'started' if (container_active | d(true) == true) else 'stopped' }}"
- name: end playbook for current host if container is set to inactive
meta: end_host
when: container_active | d(true) == false
- name: wait until networking is avaliable
command:
cmd: "ping -c1 -W1 {{ ansible_host | quote }}"
register: ping_result
until: ping_result.rc == 0
retries: 5
delay: 2
changed_when: no
delegate_to: 127.0.0.1
- name: preconfigure container
include_tasks: preconf.yml

@ -0,0 +1,66 @@
- block:
- name: install basic dependencies
include_tasks: tasks/pct_command.yml
vars:
pct_command: "{{ item.pct_command }}"
chg_substr: "{{ item.chg_substr | d(omit) }}"
loop:
- pct_command: apk update
- pct_command: apk add python3
chg_substr: Installing
- pct_command: apk add dropbear
chg_substr: Installing
- pct_command: rc-update add dropbear
chg_substr: added to runlevel
- name: install dropbear-scp if this is not an ansible controller
include_tasks: tasks/pct_command.yml
vars:
pct_command: apk add dropbear-scp
chg_substr: Installing
when: (inventory_hostname != 'ansible') and ((primary_role is not defined) or (primary_role != 'ansible'))
and alpine_version is version('3.15', '<=')
- name: install openssh-sftp-server due to openssh 9 scp deprecation
include_tasks: tasks/pct_command.yml
vars:
pct_command: apk add openssh-sftp-server
chg_substr: Installing
when: alpine_version is version('3.16', '>=')
- name: start dropbear
include_tasks: tasks/pct_command.yml
vars:
pct_command: service dropbear start
chg_substr: \* Starting dropbear ... [ ok ]
when: (container_distro | lower) == 'alpine'
- block:
- name: install basic dependencies
include_tasks: tasks/pct_command.yml
vars:
pct_command: "{{ item.pct_command }}"
chg_substr: "{{ item.chg_substr | default(omit) }}"
loop:
- pct_command: apt-get --assume-yes update
- pct_command: apt-get --assume-yes install python3
chg_substr: The following NEW packages
- pct_command: apt-get --assume-yes install openssh-server
chg_substr: The following NEW packages
- pct_command: systemctl enable ssh.service
chg_substr: Synchronizing state
- name: edit sshd config
include_tasks: tasks/pct_command.yml
vars:
pct_command: "sed -i 's/PermitRootLogin prohibit-password/PermitRootLogin yes/g' /etc/ssh/sshd_config"
- name: start sshd
include_tasks: tasks/pct_command.yml
vars:
pct_command: systemctl start ssh.service
when: (container_distro | lower) in ['debian', 'ubuntu']

@ -0,0 +1,9 @@
coredns_user: coredns
coredns_group: coredns
coredns_conf_dir: /etc/coredns
coredns_conf_file: "{{ coredns_conf_dir }}/coredns.conf"
coredns_tls_file: "{{ coredns_conf_dir }}/tls.conf"
coredns_cert_file: "{{ coredns_conf_dir }}/ecc384.crt"
coredns_key_file: "{{ coredns_conf_dir }}/ecc384.key"

@ -0,0 +1,4 @@
- name: restart coredns
service:
name: coredns
state: restarted

@ -0,0 +1,119 @@
- name: check if record is an object
fail:
msg: record must be an object
when: record is not mapping
- name: check if record zone is a string
fail:
msg: record zone must be a string
when: record.zone is defined and record.zone is not string
- name: check if record zone exists
fail:
msg: '"{{ record.zone }}" does not seem to be a valid zone'
when: (record.zone is defined) and
(record.zone != 'root') and
((int_zones is not defined) or (record.zone not in int_zones))
- name: construct record parameters
set_fact:
ns_zone: "{%- if (record.zone is defined) and (record.zone != 'root') -%}{{ record.zone }}\
{%- else -%}{{ ns_tld | d(int_tld) }}\
{%- endif -%}"
ns_name: "{%- if record.name is defined -%}{{ record.name }}\
{%- else -%}{{ inventory_hostname }}\
{%- endif -%}"
ns_type: "{%- if record.type is defined -%}{{ record.type | upper }}\
{%- else -%}A\
{%- endif -%}"
ns_value: "{%- if record.value is defined -%}{{ record.value }}\
{%- else -%}{{ ansible_host }}\
{%- endif -%}"
- name: set ns_quote
set_fact:
ns_quote: "{{ '\"' if ns_type == 'TXT' else '' }}"
- name: construct full name
set_fact:
ns_full_name: '{%- if ns_name != "@" -%}{{ ns_name }}.{%- endif -%}{{ ns_zone }}'
- name: construct regex part
set_fact:
ns_regex_part: '{%- if record.allow_multiple is defined -%}{{ (ns_quote ~ ns_value ~ ns_quote) | regex_escape() }}\.?{%- else -%}{{ "" | string }}{%- endif -%}'
- name: construct regex
set_fact:
ns_regex: '^{{ ns_full_name | regex_escape() }}\s+\d+\s+IN\s+{{ ns_type | regex_escape() }}\s+{{ ns_regex_part }}'
- name: show debug info
debug:
msg: "{{ ns_zone }} {{ ns_name }} {{ ns_type }} {{ ns_quote ~ ns_value ~ ns_quote }} --> {{ ns_regex }}"
- name: slurp zone file
slurp:
src: "{{ coredns_conf_dir ~ '/' ~ (ns_tld | d(int_tld)) ~ '.zone' }}"
register: zf
changed_when: false
- name: enumerate stdout lines to check if an entry already exists
set_fact:
ns_exists: "{{ (zf.content | b64decode).split('\n') | select('search', ns_regex) | list | length > 0 }}"
- block:
- name: fail if there are multiple records
fail:
msg: single record mode is selected, but multiple records found
when: (zf.content | b64decode).split('\n') | select('search', ns_regex) | list | length > 1
- name: grab the value
set_fact:
ns_old_value: "{{ (zf.content | b64decode).split('\n') | select('search', ns_regex) | map('regex_search', '\\s+?(\\S+?)\\.?$', '\\1') | first | join('') }}"
- name: replace the record
lineinfile:
path: "{{ coredns_conf_dir ~ '/' ~ (ns_tld | d(int_tld)) ~ '.zone' }}"
regexp: '^\s*{{ ns_name | regex_escape() }}\s+IN\s+{{ ns_type | regex_escape() }}\s+'
line: "{{ ns_name }}\tIN\t{{ ns_type }}\t{{ ns_quote ~ ns_value ~ ns_quote }}"
backrefs: yes
when: ns_old_value != (ns_quote ~ ns_value ~ ns_quote)
register: rr1
when: ns_exists and rrset.allow_multiple is not defined
- name: add the record if it is missing
lineinfile:
path: "{{ coredns_conf_dir ~ '/' ~ (ns_tld | d(int_tld)) ~ '.zone' }}"
line: "{{ ns_name }}\tIN\t{{ ns_type }}\t{{ ns_quote ~ ns_value ~ ns_quote }}"
when: not ns_exists
register: rr2
- name: determine if records were changed
set_fact:
ns_records_changed: "{{ ((rr1 is defined) and rr1.changed) or ((rr2 is defined) and rr2.changed) }}"
- name: change serial
include_tasks: increase_serial.yml
when: ns_records_changed | d(false) == true
- name: restart coredns
service:
name: coredns
state: restarted
when: (ns_instant | d(false) == true) and (ns_records_changed or ns_serial_changed)

@ -0,0 +1,21 @@
- name: add default record
include_tasks: add_record.yml
vars:
record: {}
when: (ns_records | d([]) | length) == 0
- name: process other items
include_tasks: add_record.yml
loop: "{{ ns_records | d([]) }}"
loop_control:
loop_var: record
- name: restart coredns
service:
name: coredns
state: restarted
when: (ns_instant | d(false) == false) and
((ns_records_changed | d(false) == true) or
(ns_serial_changed | d(false) == true))

@ -0,0 +1,47 @@
- name: slurp zone file
slurp:
src: "{{ coredns_conf_dir ~ '/' ~ (ns_tld | d(int_tld)) ~ '.zone' }}"
register: zf
changed_when: false
- name: get SOA serial value
set_fact:
ns_old_serial: '{{ zf.content | b64decode | regex_search(''@\s+IN\s+SOA\s+\S+\s+\S+\s*\(\s*(\d+)'', ''\1'') | first }}'
- name: get current date
include_tasks: tasks/get_datetime.yml
vars:
format: YYMMDD
- name: replace outdated serial with current date
set_fact:
ns_new_serial: "{{ (current_date_time | string) ~ '01'}}"
when: ns_old_serial[:8] != (current_date_time | string)
- name: increase current serial
set_fact:
ns_new_serial: "{{ (ns_old_serial | int) + 1 }}"
when: (ns_old_serial[:8] == (current_date_time | string)) and ((ns_old_serial[8:10] | int) < 99)
- name: do not change current serial if it had more than 99 iterations
set_fact:
ns_new_serial: "{{ ns_old_serial }}"
when: (ns_old_serial[:8] == (current_date_time | string)) and ((ns_old_serial[8:10] | int) >= 99)
- name: insert new serial
replace:
path: "{{ coredns_conf_dir ~ '/' ~ (ns_tld | d(int_tld)) ~ '.zone' }}"
regexp: '(@\s+IN\s+SOA\s+\S+\s+\S+\s*\(\s*){{ ns_old_serial }}'
replace: '\g<1>{{ ns_new_serial }}'
register: result
- name: set fact if serial was changed
set_fact:
ns_serial_changed: "{{ result.changed }}"

@ -0,0 +1,93 @@
- name: install coredns and dependencies
include_tasks: tasks/install_packages.yml
vars:
package:
- coredns
- alpine: coredns-openrc
- name: create user and group
include_tasks: tasks/create_user.yml
vars:
user:
name: "{{ coredns_user }}"
group: "{{ coredns_group }}"
- name: create config directory
file:
path: "{{ coredns_conf_dir }}"
state: directory
owner: "{{ coredns_user }}"
group: "{{ coredns_group }}"
notify: restart coredns
- name: template corefile
template:
src: corefile.j2
dest: "{{ coredns_conf_file }}"
force: yes
owner: "{{ coredns_user }}"
group: "{{ coredns_group }}"
mode: 0400
notify: restart coredns
- name: template empty tls file if missing
copy:
content: ''
dest: "{{ coredns_tls_file }}"
force: no
owner: "{{ coredns_user }}"
group: "{{ coredns_group }}"
mode: 0400
notify: restart coredns
- name: template root zone if missing
template:
src: zone.j2
dest: "{{ coredns_conf_dir ~ '/' ~ (ns_tld | d(int_tld)) ~ '.zone' }}"
force: no
mode: 0400
owner: "{{ coredns_user }}"
group: "{{ coredns_group }}"
notify: restart coredns
- name: edit service config
lineinfile:
path: /etc/conf.d/coredns
regexp: "^COREDNS_CONFIG="
line: "COREDNS_CONFIG={{ coredns_conf_file | quote }}"
notify: restart coredns
- name: template init script
template:
src: init.j2
dest: /etc/init.d/coredns
force: yes
mode: 0755
notify: restart coredns
- name: flush handlers
meta: flush_handlers
- name: add directories to backup plan
include_role:
name: backup
vars:
function: add
backup_items:
- "{{ coredns_conf_dir }}"
- name: enable and start coredns
service:
name: coredns
enabled: yes
state: started

@ -0,0 +1,28 @@
- name: deploy ecc384 cert
include_role:
name: ca
vars:
function: certs
ca_options:
mode: '0400'
owner: "{{ coredns_user }}"
group: "{{ coredns_group }}"
concat_inter: true
preset: web
ocsp_must_staple: false
notify: restart coredns
ca_certs:
- type: ecc384
key: "{{ coredns_key_file }}"
cert: "{{ coredns_cert_file }}"
- name: template tls snippet file
template:
src: tls.j2
dest: "{{ coredns_tls_file }}"
force: yes
owner: "{{ coredns_user }}"
group: "{{ coredns_group }}"
mode: 0400
notify: restart coredns

@ -0,0 +1,13 @@
- name: install coredns
include_tasks: install.yml
when: function == 'install'
- name: install coredns tls enhancements
include_tasks: install_tls.yml
when: function == 'install_tls'
- name: add records
include_tasks: add_records.yml
when: function == 'add_records'

@ -0,0 +1,15 @@
(common) {
root {{ (coredns_conf_dir ~ '/') | quote }}
file {{ ((ns_tld | d(int_tld)) ~ '.zone') | quote }}
any
bufsize {{ ns_edns0_bufsize | d(1232) }}
errors
loadbalance
}
{{ ns_tld | d(int_tld) }} {
import common
}
import {{ coredns_tls_file | quote }}

@ -0,0 +1,14 @@
#!/sbin/openrc-run
name="$SVCNAME"
directory="{{ coredns_conf_dir }}"
command="/usr/bin/coredns"
command_args="-conf ${COREDNS_CONFIG} ${COREDNS_EXTRA_ARGS}"
command_user="{{ coredns_user }}:{{ coredns_group }}"
pidfile="/var/run/$SVCNAME.pid"
command_background=true
start_stop_daemon_args="--stdout-logger logger --stderr-logger logger"
depend() {
after net
}

@ -0,0 +1,9 @@
tls://{{ ns_tld | d(int_tld) }}:853 {
import common
tls {{ coredns_cert_file | quote }} {{ coredns_key_file | quote }}
}
https://{{ ns_tld | d(int_tld) }} {
import common
tls {{ coredns_cert_file | quote }} {{ coredns_key_file | quote }}
}

@ -0,0 +1,32 @@
{%- set primary_ns = inventory_hostname -%}
{%- if ns_server_group is defined -%}
{%- set primary_ns = hostvars[groups[ns_server_group][0]]['inventory_hostname'] -%}
{%- endif -%}
{%- set this_name = (ns_name | d(inventory_hostname)) -%}
{%- set this_primary_name = (hostvars[primary_ns]['ns_name'] | d(hostvars[primary_ns]['inventory_hostname'])) -%}
{%- set this_tld = (hostvars[primary_ns]['ns_tld'] | d(ns_tld) | d(int_tld)) -%}
$ORIGIN {{ this_tld }}.
$TTL {{ ns_ttl | d(300) }}
@ IN SOA {{ this_name ~ '.' ~ this_tld }}. {{ (ns_admin | replace('@', '.')) if ns_admin is defined else ('admin' ~ '.' ~ this_tld) }}. (
2021010101
{{ ns_refresh | d(1200) }}
{{ ns_retry | d(300) }}
{{ ns_expire | d(1209600) }}
{{ ns_neg_ttl | d(300) }}
)
{% if ns_server_group is defined -%}
{% for host in groups[ns_server_group] -%}
@ IN NS {{ (hostvars[host]['ns_name'] | d(hostvars[host]['inventory_hostname'])) ~ '.' ~ this_tld }}.
{{ hostvars[host]['ns_name'] | d(hostvars[host]['inventory_hostname']) }} IN A {{ hostvars[host]['ansible_host'] }}
{% endfor -%}
{% else -%}
@ IN NS {{ this_primary_name ~ '.' ~ this_tld }}.
{{ this_primary_name }} IN A {{ ansible_host }}
{% endif -%}

@ -0,0 +1 @@
crl_dir: /opt/crl

@ -0,0 +1,41 @@
- name: install and configure nginx
include_role:
name: nginx
vars:
nginx:
servers:
- conf: nginx_crl
http: true
- conf: nginx_crl
certs: true
- name: create crl directory
file:
path: "{{ crl_dir }}"
state: directory
mode: 0500
owner: nginx
group: nginx
- name: generate crls
include_role:
name: ca
vars:
function: crl
ca_options:
path: "{{ crl_dir }}"
mode: '0400'
owner: nginx
group: nginx
ca_crls:
- name: add directories to backup plan
include_role:
name: backup
vars:
function: add
backup_items:
- "{{ crl_dir }}"

@ -0,0 +1,4 @@
location / {
root {{ crl_dir }};
try_files $uri =404;
}

@ -0,0 +1,290 @@
dovecot_user: dovecot
dovecot_group: dovecot
dovecot_mail_user: dovemail
dovecot_mail_group: dovemail
dovecot_null_user: dovenull
dovecot_conf_dir: /etc/dovecot
dovecot_tls_dir: "{{ dovecot_conf_dir }}/tls"
dovecot_sieve_dir: "{{ dovecot_conf_dir }}/sieve"
dovecot_mail_dir: /opt/mail
dovecot_script_dir: "{{ dovecot_conf_dir }}/scripts"
dovecot_tls_dh2048: "{{ dovecot_tls_dir }}/dh2048.pem"
dovecot_tls_int_ecc384_key: "{{ dovecot_tls_dir }}/ecc384.key"
dovecot_tls_int_ecc384_cert: "{{ dovecot_tls_dir }}/ecc384.crt"
dovecot_tls_int_rsa2048_key: "{{ dovecot_tls_dir }}/rsa2048.key"
dovecot_tls_int_rsa2048_cert: "{{ dovecot_tls_dir }}/rsa2048.crt"
dovecot_drafts_name: Drafts
dovecot_junk_name: Junk
dovecot_sent_name: Sent
dovecot_trash_name: Trash
dovecot_expunged_name: .EXPUNGED
dovecot_max_quota_mb: 5000
dovecot_default_config:
protocols: imap lmtp sieve
hostname: "{{ (mail_server.mua_actual_hostname | d(host_name)) ~ '@' ~ mail_server.tld }}"
login_greeting: "IMAPS {{ org }} (Dovecot) ready"
auth_cache_ttl: 20m
auth_cache_size: 2M
auth_cache_negative_ttl: 5m
auth_mechanisms:
- plain
- login
- digest-md5
- cram-md5
- scram-sha-1
- scram-sha-256
auth_default_realm: "{{ mail_server.tld }}"
auth_realms: "{{ mail_server.tld }}"
auth_worker_max_count: 5
default_internal_user: "{{ dovecot_user }}"
default_internal_group: "{{ dovecot_group }}"
default_login_user: "{{ dovecot_null_user }}"
default_process_limit: 50
default_vsz_limit: 64M
disable_plaintext_auth: yes
imap_capability: "+SPECIAL-USE"
imap_id_send: '"name" * "version" * support-email postmaster@{{ mail_server.tld }}'
mail_attachment_detection_options: add-flags
mail_attribute_dict: "file:%h/mail_attrib"
mail_gid: "{{ dovecot_mail_group }}"
mail_home: "{{ dovecot_mail_dir }}/%Ld/%Ln"
mail_location: "mdbox:%h/mail:UTF-8"
mail_max_keyword_length: 100
mail_server_admin: "mailto:{{ maintainer_email }}"
mail_server_comment: "Dovecot IMAPS server - {{ org }}"
mail_temp_scan_interval: 24h
mail_uid: "{{ dovecot_mail_user }}"
postmaster_address: "postmaster@{{ mail_server.tld }}"
quota_full_tempfail: yes
recipient_delimiter: '+'
submission_client_workarounds: whitespace-before-path mailbox-for-path
ssl: required
ssl_cert: "<{{ dovecot_tls_int_ecc384_cert }}"
ssl_key: "<{{ dovecot_tls_int_ecc384_key }}"
ssl_alt_cert: "<{{ dovecot_tls_int_rsa2048_cert }}"
ssl_alt_key: "<{{ dovecot_tls_int_rsa2048_key }}"
ssl_cipher_suites: "TLS_CHACHA20_POLY1305_SHA256:TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256"
ssl_dh: "<{{ dovecot_tls_dh2048 }}"
ssl_min_protocol: TLSv1.2
ssl_prefer_server_ciphers: yes
mail_plugins: "$mail_plugins mailbox_alias lazy_expunge listescape trash quota acl"
dovecot_protocols:
imap:
imap_metadata: yes
mail_plugins: "$mail_plugins imap_zlib imap_quota imap_acl imap_sieve"
lmtp:
mail_plugins: "$mail_plugins sieve"
lmtp_client_workarounds: whitespace-before-path mailbox-for-path
lmtp_user_concurrency_limit: 25
lda:
mail_plugins: "$mail_plugins sieve"
lda_mailbox_autocreate: yes
lda_mailbox_autosubscribe: yes
sieve:
mail_max_userip_connections: 50
dovecot_namespaces:
- name: inbox
opts:
inbox: yes
separator: '/'
mailboxes:
- name: INBOX
opts:
auto: subscribe
- name: "{{ dovecot_drafts_name }}"
opts:
auto: subscribe
special_use: '\Drafts'
- name: "{{ dovecot_junk_name }}"
opts:
auto: subscribe
special_use: '\Junk'
autoexpunge: 90d
- name: "{{ dovecot_sent_name }}"
opts:
auto: subscribe
special_use: '\Sent'
- name: "{{ dovecot_trash_name }}"
opts:
auto: subscribe
special_use: '\Trash'
autoexpunge: 90d
- name: "{{ dovecot_expunged_name }}"
opts:
auto: create
autoexpunge: 180d
- name: shared
opts:
type: shared
separator: '/'
prefix: 'Общие/%%u/'
location: 'mdbox:%%h/mail:INDEXPVT=%h/shared_idx/%%u'
subscriptions: no
list: children
dovecot_dicts:
acl: "pgsql:{{ dovecot_conf_dir }}/dovecot-dict-sql.conf.ext"
dovecot_plugin_config:
trash: "{{ dovecot_conf_dir }}/dovecot-trash.conf.ext"
lazy_expunge: "{{ dovecot_expunged_name }}"
lazy_expunge_only_last_instance: yes
acl: "vfile:{{ dovecot_conf_dir }}/dovecot.acl"
acl_shared_dict: "proxy::acl"
quota: "count:Account quota"
quota_exceeded_message: Mailbox quota exceeded
quota_grace: "5%%"
quota_max_mail_size: "{{ mail_server.max_mail_size_bytes ~ 'B' }}"
quota_rule: "*:storage={{ dovecot_max_quota_mb }}M"
quota_rule2: "{{ dovecot_trash_name }}:storage=+200M"
quota_rule3: "{{ dovecot_expunged_name }}:ignore"
quota_status_success: DUNNO
quota_status_nouser: DUNNO
quota_status_overquota: "452 4.2.2 User mailbox is full"
quota_vsizes: yes
sieve_extensions: "-enotify -editheader"
sieve_global_extensions: "+vnd.dovecot.pipe +vnd.dovecot.filter +vnd.dovecot.execute"
sieve_max_actions: 64
sieve_plugins: sieve_imapsieve sieve_extprograms
sieve_pipe_bin_dir: "{{ dovecot_script_dir }}"
sieve_execute_bin_dir: "{{ dovecot_script_dir }}"
sieve_filter_bin_dir: "{{ dovecot_script_dir }}"
sieve_spamtest_status_type: text
sieve_spamtest_status_header: X-Spam
sieve_spamtest_text_value0: No
sieve_spamtest_text_value10: Yes
sieve_before: "{{ dovecot_sieve_dir }}/spam-to-folder.sieve"
dovecot_user_pass_db:
- type: passdb
opts:
driver: sql
args: "{{ dovecot_conf_dir }}/dovecot-sql.conf.ext"
- type: userdb
opts:
driver: prefetch
- type: userdb
opts:
driver: sql
args: "{{ dovecot_conf_dir }}/dovecot-sql.conf.ext"
dovecot_services:
imap:
opts:
service_count: 16
process_limit: 256
imap-login:
opts:
service_count: 0
process_min_avail: 1
client_limit: 16
service_count: 32
listeners:
- type: inet_listener
name: imap
opts:
port: 143
- type: inet_listener
name: imaps
opts:
port: 993
ssl: yes
lmtp:
opts:
client_limit: 1
vsz_limit: 192M
listeners:
- type: inet_listener
opts:
port: "{{ mail_server.mua_lmtp_port }}"
auth:
listeners:
- type: inet_listener
opts:
port: "{{ mail_server.mua_auth_port }}"
- type: unix_listener auth-userdb
opts:
mode: 0666
user: "{{ dovecot_user }}"
group: "{{ dovecot_group }}"
quota-status:
opts:
executable: "/usr/libexec/dovecot/quota-status -p postfix"
listeners:
- type: inet_listener
opts:
port: "{{ mail_server.mua_quota_port }}"
auth-worker:
opts:
user: "{{ dovecot_user }}"
group: "{{ dovecot_group }}"
dict:
opts:
user: "{{ dovecot_user }}"
group: "{{ dovecot_group }}"
listeners:
- type: unix_listener dict
opts:
mode: 0666
user: "{{ dovecot_user }}"
group: "{{ dovecot_group }}"
managesieve-login:
opts:
service_count: 0
process_min_avail: 1
managesieve:
opts:
process_limit: 512
dovecot_sieve_scripts:
- src: sieve-spam
dest: spam-to-folder

@ -0,0 +1,4 @@
- name: restart dovecot
service:
name: dovecot
state: restarted

@ -0,0 +1,241 @@
- name: set dovecot_cfg
set_fact:
dovecot_cfg: "{{ dovecot_default_config | d({}) | combine(dovecot_config | d({}), recursive=true) }}"
- name: install dovecot
include_tasks: tasks/install_packages.yml
vars:
package:
- dovecot
- dovecot-lmtpd
- dovecot-openrc
- dovecot-pgsql
- dovecot-pigeonhole-plugin
- name: create user and group
include_tasks: tasks/create_user.yml
vars:
user:
name: "{{ dovecot_user }}"
group: "{{ dovecot_group }}"
- name: create dovemail user and group
include_tasks: tasks/create_user.yml
vars:
user:
name: "{{ dovecot_mail_user }}"
group: "{{ dovecot_mail_group }}"
- name: create dovenull user and group
include_tasks: tasks/create_user.yml
vars:
user:
name: "{{ dovecot_null_user }}"
- name: create dovecot conf dir
file:
path: "{{ dovecot_conf_dir }}"
state: directory
mode: 0755
owner: "{{ dovecot_user }}"
group: "{{ dovecot_group }}"
- name: create dovecot tls dir
file:
path: "{{ dovecot_tls_dir }}"
state: directory
mode: 0700
- name: create dovecot mail dir
file:
path: "{{ dovecot_mail_dir }}"
state: directory
mode: "g+s,o-rwx"
owner: "{{ dovecot_mail_user }}"
group: "{{ dovecot_mail_group }}"
- name: create dovecot sieve dir
file:
path: "{{ dovecot_sieve_dir }}"
state: directory
mode: 0755
owner: "{{ dovecot_mail_user }}"
group: "{{ dovecot_mail_group }}"
- name: generate dh params
include_role:
name: ca
vars:
function: dhparams
dh_params:
path: "{{ dovecot_tls_dh2048 }}"
mode: '0400'
remote_gen: yes
notify: restart dovecot
- name: remove unneeded dovecot files
file:
path: "{{ dovecot_conf_dir ~ '/' ~ item }}"
state: absent
loop:
- conf.d
- dovecot-dict-auth.conf.ext
- dovecot-oauth2.conf.ext
- dovecot-openssl.cnf
- users
notify: restart dovecot
- name: get dovemail user info
getent:
database: passwd
key: "{{ dovecot_mail_user }}"
changed_when: no
- name: set dovemail uid
set_fact:
dovecot_dovemail_uid: "{{ getent_passwd[dovecot_mail_user][1] }}"
- name: template dovecot configuration
template:
src: "{{ item if item is string else item.src }}.j2"
dest: "{{ dovecot_conf_dir ~ '/' ~ ((item ~ '.conf.ext') if item is string else item.dest) }}"
force: yes
mode: "{{ '0400' if (item is string) else (item.mode | d('0400')) }}"
lstrip_blocks: yes
loop:
- { src: dovecot-dict-sql, dest: dovecot-dict-sql.conf.ext, mode: '0444' }
- dovecot-sql
- dovecot-trash
- { src: dovecot-acl, dest: dovecot.acl }
- { src: dovecot, dest: dovecot.conf }
notify: restart dovecot
- name: edit permissions of dovecot plugin files
file:
path: "{{ dovecot_conf_dir ~ '/' ~ item }}"
state: file
owner: "{{ dovecot_mail_user }}"
group: "{{ dovecot_mail_group }}"
loop:
- dovecot.acl
- dovecot-sql.conf.ext
- dovecot-trash.conf.ext
- dovecot-dict-sql.conf.ext
notify: restart dovecot
- name: template sieve scripts
template:
src: "{{ item.src }}.j2"
dest: "{{ dovecot_sieve_dir ~ '/' ~ item.dest ~ '.sieve' }}"
force: yes
mode: 0400
owner: "{{ dovecot_mail_user }}"
group: "{{ dovecot_mail_group }}"
loop: "{{ dovecot_sieve_scripts | d([]) }}"
register: result
- name: compile scripts
shell:
cmd: "sievec {{ (dovecot_sieve_dir ~ '/') | quote }}"
when: result.changed
notify: restart dovecot
- name: collect svbin files
find:
paths: "{{ dovecot_sieve_dir }}/"
patterns: "*.svbin"
recurse: yes
depth: 3
register: svbin_files
- name: change svbin permissions
file:
path: "{{ item.path }}"
mode: 0400
owner: "{{ dovecot_mail_user }}"
group: "{{ dovecot_mail_group }}"
loop: "{{ svbin_files.files | d([]) | flatten(levels=1) }}"
notify: restart dovecot
- name: add extra cname record
include_role:
name: ns
vars:
function: add_records
ns_add_default_record: no
ns_records:
- name: "{{ mail_server.mua_actual_hostname }}"
type: CNAME
value: "{{ host_fqdn }}"
when: mail_server.mua_actual_hostname is defined
- name: deploy certs
include_role:
name: certs
vars:
common:
owner: root
group: root
post_hook: service dovecot restart
notify: restart dovecot
hostname: "{{ mail_server.mua_actual_hostname }}"
certs:
- cert: "{{ dovecot_tls_int_ecc384_cert }}"
key: "{{ dovecot_tls_int_ecc384_key }}"
ecc: yes
- cert: "{{ dovecot_tls_int_rsa2048_cert }}"
key: "{{ dovecot_tls_int_rsa2048_key }}"
ecc: no
- name: flush handlers
meta: flush_handlers
- name: add directories to backup plan
include_role:
name: backup
vars:
function: add
backup_items:
- "{{ dovecot_conf_dir }}"
- "{{ dovecot_tls_dir }}"
- "{{ dovecot_sieve_dir }}"
- "{{ dovecot_script_dir }}"
- name: add mail dir to backup plan
include_role:
name: backup
vars:
function: add
backup_items:
- "{{ dovecot_mail_dir }}"
when: dovecot_backup_mail_dir | d(false) == true
- name: enable and start dovecot
service:
name: dovecot
enabled: yes
state: started

@ -0,0 +1,8 @@
* user={{ mail_server.admin_email }} lrwstipekxa
INBOX owner lrwstipek
{{ dovecot_sent_name }} owner lrwstipek
{{ dovecot_drafts_name }} owner lrwstipek
{{ dovecot_junk_name }} owner lrwstipek
{{ dovecot_trash_name }} owner lrwstipek
{{ dovecot_expunged_name }} owner
{{ dovecot_expunged_name }} anyone

@ -0,0 +1,22 @@
connect = host={{ hostvars[mail_server.db_server_hostname]['ansible_host'] }} user={{ mail_server.db_user }} password={{ mail_server.db_pass }} dbname={{ mail_server.db_name }}
map {
pattern = shared/shared-boxes/user/$to/$from
table = mail_user_shares
value_field = dummy
fields {
from_user = $from
to_user = $to
}
}
map {
pattern = shared/shared-boxes/anyone/$from
table = mail_anyone_shares
value_field = dummy
fields {
from_user = $from
}
}

@ -0,0 +1,47 @@
driver = pgsql
connect = host={{ hostvars[mail_server.db_server_hostname]['ansible_host'] }} user={{ mail_server.db_user }} password={{ mail_server.db_pass }} dbname={{ mail_server.db_name }}
default_pass_scheme = PLAIN
password_query = \
SELECT username AS user, \
( \
SELECT domain FROM mail_domains WHERE id = domain_id \
) AS domain, \
password_plaintext AS password, \
'{{ dovecot_mail_dir }}/%Ld/%Ln' AS userdb_home, \
concat('*:bytes=', coalesce(nullif(quota_mb, 0), {{ dovecot_max_quota_mb }}), 'M') AS userdb_quota_rule, \
{{ dovecot_dovemail_uid }} AS userdb_uid \
FROM mail_users \
WHERE \
LOWER(username) = '%Ln' AND \
domain_id = ( \
SELECT id FROM mail_domains WHERE LOWER(domain) = '%Ld' \
) AND \
enabled = true;
user_query = \
SELECT username AS user, \
( \
SELECT domain FROM mail_domains WHERE id = domain_id \
) AS domain, \
'{{ dovecot_mail_dir }}/%Ld/%Ln' AS home, \
concat('*:bytes=', coalesce(nullif(quota_mb, 0), {{ dovecot_max_quota_mb }}), 'M') AS quota_rule, \
{{ dovecot_dovemail_uid }} AS uid \
FROM mail_users \
WHERE \
LOWER(username) = '%Ln' AND \
domain_id = ( \
SELECT id FROM mail_domains WHERE LOWER(domain) = '%Ld' \
) AND \
enabled = true;
iterate_query = \
SELECT username AS user, \
( \
SELECT domain FROM mail_domains WHERE id = domain_id \
) AS domain \
FROM mail_users \
WHERE enabled = true;

@ -0,0 +1,3 @@
1 {{ dovecot_trash_name }}
2 {{ dovecot_junk_name }}
3 {{ dovecot_sent_name }}

@ -0,0 +1,94 @@
{% macro dovecot_option(option, padding = 0) -%}
{{- '' if (padding == 0) else (' ' * 4 * padding) -}}
{% if option.value is boolean -%}
{{ option.key }} = {{ 'yes' if option.value else 'no' }}
{% elif option.value | type_debug == 'list' -%}
{{ option.key }} = {{ option.value | join(' ') }}
{% elif option.value is mapping -%}
{{ option.key }} {
{% for suboption in (option.value | d({}) | dict2items) -%}
{{- dovecot_option(suboption, padding + 1) }}
{% endfor -%}
}
{% else -%}
{{ option.key }} = {{ option.value if option.value != None else '' }}
{% endif -%}
{% endmacro -%}
{% for option in (dovecot_cfg | d({}) | dict2items) -%}
{{ dovecot_option(option) }}
{%- endfor %}
first_valid_uid = {{ dovecot_dovemail_uid }}
last_valid_uid = {{ dovecot_dovemail_uid }}
{% for proto in (dovecot_protocols | d({}) | dict2items) -%}
protocol {{ proto.key }} {
{% for option in (proto.value | d({}) | dict2items) -%}
{{ dovecot_option(option, 1) }}
{%- endfor -%}
}
{% endfor %}
{% for namespace in (dovecot_namespaces | d([])) -%}
namespace {{ namespace.name }} {
{% for option in (namespace.opts | d({}) | dict2items) -%}
{{ dovecot_option(option, 1) }}
{%- endfor -%}
{% for mailbox in (namespace.mailboxes | d([])) -%}
{{- ' ' -}}mailbox {{ mailbox.name }} {
{% for mailbox_option in (mailbox.opts | d({}) | dict2items) -%}
{{ dovecot_option(mailbox_option, 2) }}
{%- endfor -%}
{{- ' ' -}}}
{% endfor -%}
}
{% endfor %}
{% if dovecot_dicts is mapping -%}
dict {
{% for option in (dovecot_dicts | d({}) | dict2items) -%}
{{ dovecot_option(option, 1) }}
{%- endfor -%}
}
{% endif %}
{% if dovecot_plugin_config is mapping -%}
plugin {
{% for option in (dovecot_plugin_config | d({}) | dict2items) -%}
{{ dovecot_option(option, 1) }}
{%- endfor -%}
}
{% endif %}
{% for db in (dovecot_user_pass_db | d([])) -%}
{{ db.type }} {
{% for option in (db.opts | d({}) | dict2items) -%}
{{ dovecot_option(option, 1) }}
{%- endfor -%}
}
{% endfor %}
{% for service in (dovecot_services | d({}) | dict2items) -%}
service {{ service.key }} {
{% for option in (service.value.opts | d({}) | dict2items) -%}
{{ dovecot_option(option, 1) }}
{%- endfor %}
{% for listener in (service.value.listeners | d([])) -%}
{{- ' ' -}}{{ listener.type }} {{ listener.name | d('') }} {
{% for listener_option in (listener.opts | d({}) | dict2items) -%}
{{ dovecot_option(listener_option, 2) }}
{%- endfor -%}
{{- ' ' -}}}
{% endfor -%}
}
{% endfor %}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save