diff --git a/ansible.cfg b/ansible.cfg new file mode 100644 index 0000000..83356cb --- /dev/null +++ b/ansible.cfg @@ -0,0 +1,24 @@ +[defaults] +ansible_managed = Ansible managed - DO NOT EDIT HERE +forks = 10 +host_key_checking = False +inventory = environments/production +retry_files_enabled = False +roles_path = roles/ +vault_password_file = ansible_vault.key + +[privilege_escalation] +become = True +become_user = root +become_method = sudo + +[persistent_connection] +connect_interval = 1 +connect_retries = 30 +connect_timeout = 30 + +[ssh_connection] +retries = 5 +pipelining = True +ssh_args = "-o ControlMaster=auto -o ControlPersist=30m" +control_path = "./ssh/ansible-%%C" diff --git a/ansible_requirements.yaml b/ansible_requirements.yaml new file mode 100644 index 0000000..4f51be8 --- /dev/null +++ b/ansible_requirements.yaml @@ -0,0 +1,6 @@ +--- + +collections: + - name: 'ansible.posix' + - name: 'community.docker' + - name: 'community.general' diff --git a/environments/production/group_vars/wafs.yaml b/environments/production/group_vars/wafs.yaml new file mode 100644 index 0000000..e130b41 --- /dev/null +++ b/environments/production/group_vars/wafs.yaml @@ -0,0 +1,8 @@ +--- + +####################### +#### open-appsec #### +####################### +openappsec_advanced_model_url: 'https://some_internal_url/open-appsec-advanced-model.tgz' +openappsec_advanced_model_sha: 'sha_of_the_advanced_model' +openappsec_token: 'cp-some-long-token' diff --git a/environments/production/hosts b/environments/production/hosts new file mode 100644 index 0000000..4dd8008 --- /dev/null +++ b/environments/production/hosts @@ -0,0 +1,3 @@ +[wafs] +waf0.example.local +waf1.example.local diff --git a/playbooks/production/wafs.yaml b/playbooks/production/wafs.yaml new file mode 100644 index 0000000..8d92ae7 --- /dev/null +++ b/playbooks/production/wafs.yaml @@ -0,0 +1,9 @@ +--- + +# wafs +- name: 'ansible role for wafs' + become: true + hosts: 'wafs' + serial: '1' + roles: + - 'openappsec' diff --git a/roles/openappsec/files/dehydrated/config b/roles/openappsec/files/dehydrated/config new file mode 100644 index 0000000..b21f3ee --- /dev/null +++ b/roles/openappsec/files/dehydrated/config @@ -0,0 +1,15 @@ +# generic +CHALLENGETYPE="http-01" +CONTACT_EMAIL="info@example.com" +IP_VERSION="4" +RENEW_DAYS="30" + +# dirs +ACCOUNTDIR="/mnt/certs/accounts" +CERTDIR="/mnt/certs/certs" +CHAINCACHE="/mnt/certs/chains" +WELLKNOWN="/mnt/certs/challenge" + +# oscp +OCSP_MUST_STAPLE="yes" +OCSP_FETCH="no" diff --git a/roles/openappsec/files/dehydrated/domains.txt b/roles/openappsec/files/dehydrated/domains.txt new file mode 100644 index 0000000..a9966bb --- /dev/null +++ b/roles/openappsec/files/dehydrated/domains.txt @@ -0,0 +1,3 @@ +# example.com +example.com +subdomain.example.com diff --git a/roles/openappsec/files/nginx/conf.d/example.com.conf b/roles/openappsec/files/nginx/conf.d/example.com.conf new file mode 100644 index 0000000..2d85e1b --- /dev/null +++ b/roles/openappsec/files/nginx/conf.d/example.com.conf @@ -0,0 +1,70 @@ +server { + listen 80 default_server proxy_protocol; + server_name example.com; + + # proxy protocol settings + set_real_ip_from 10.0.0.1/32; + real_ip_header proxy_protocol; + real_ip_recursive on; + + # logging + access_log syslog:server=log.example.local vhost; + error_log syslog:server=log.example.local; + + location ^~ /.well-known/acme-challenge { + alias /mnt/certs/challenge; + } + + # health uri + location /health { + + # return 'rp-ok' in plain text + add_header Content-Type text/plain; + return 200 'waf-ok'; + } + + location / { + return 301 https://$host$request_uri; + } +} + +server { + listen 443 default_server http2 proxy_protocol ssl; + server_name example.com; + + # proxy protocol settings + set_real_ip_from 10.0.0.1/32; + real_ip_header proxy_protocol; + real_ip_recursive on; + + # logging + access_log syslog:server=log.example.local vhost; + error_log syslog:server=log.example.local; + + # certificates + ssl_certificate /mnt/certs/certs/example.com/fullchain.pem; + ssl_certificate_key /mnt/certs/certs/example.com/privkey.pem; + + # tls settings + ssl_dhparam /etc/nginx/dhparam.pem; + ssl_session_timeout 4h; + ssl_session_tickets off; + ssl_session_cache shared:SSL:20m; + ssl_ecdh_curve secp384r1; + ssl_prefer_server_ciphers off; + ssl_protocols TLSv1.3 TLSv1.2; + ssl_stapling on; + ssl_stapling_verify on; + + # health uri + location /health { + + # return 'rp-ok' in plain text + add_header Content-Type text/plain; + return 200 'rp-ok'; + } + + location / { + return 301 https://example.com; + } +} diff --git a/roles/openappsec/files/nginx/conf.d/subdomain.example.com.conf b/roles/openappsec/files/nginx/conf.d/subdomain.example.com.conf new file mode 100644 index 0000000..062d83b --- /dev/null +++ b/roles/openappsec/files/nginx/conf.d/subdomain.example.com.conf @@ -0,0 +1,67 @@ +upstream application01 { + server 10.0.0.10:443; +} + +server { + listen 80 proxy_protocol; + server_name subdomain.example.com; + + # proxy protocol settings + set_real_ip_from 10.0.0.1/32; + real_ip_header proxy_protocol; + real_ip_recursive on; + + # logging + access_log syslog:server=log.example.local vhost; + error_log syslog:server=log.example.local; + + location ^~ /.well-known/acme-challenge { + alias /mnt/certs/challenge; + } + + location / { + return 301 https://$host$request_uri; + } +} + +server { + listen 443 proxy_protocol ssl http2; + server_name subdomain.example.com; + + # proxy protocol settings + set_real_ip_from 10.0.0.1/32; + real_ip_header proxy_protocol; + real_ip_recursive on; + + # logging + access_log syslog:server=log.example.local vhost; + error_log syslog:server=log.example.local; + + # certificates + ssl_certificate /mnt/certs/certs/subdomain.example.com/fullchain.pem; + ssl_certificate_key /mnt/certs/certs/subdomain.example.com/privkey.pem; + + # tls settings + ssl_dhparam /etc/nginx/dhparam.pem; + ssl_session_timeout 4h; + ssl_session_tickets off; + ssl_session_cache shared:SSL:20m; + ssl_ecdh_curve secp384r1; + ssl_prefer_server_ciphers off; + ssl_protocols TLSv1.3 TLSv1.2; + ssl_stapling on; + ssl_stapling_verify on; + + location / { + + # set headers + proxy_set_header Host $host; + proxy_set_header X-Real-IP $proxy_protocol_addr; + proxy_set_header X-Forwarded-For $proxy_protocol_addr; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Proto $scheme; + + # pass upstream + proxy_pass https://application01; + } +} diff --git a/roles/openappsec/files/nginx/dhparam.pem b/roles/openappsec/files/nginx/dhparam.pem new file mode 100644 index 0000000..6a6d261 --- /dev/null +++ b/roles/openappsec/files/nginx/dhparam.pem @@ -0,0 +1,5 @@ +-----BEGIN DH PARAMETERS----- + ... +( openssl dhparam -out dhparam.pem 2048 ) + ... +-----END DH PARAMETERS----- diff --git a/roles/openappsec/files/nginx/nginx.conf b/roles/openappsec/files/nginx/nginx.conf new file mode 100644 index 0000000..543ad04 --- /dev/null +++ b/roles/openappsec/files/nginx/nginx.conf @@ -0,0 +1,27 @@ +worker_processes 2; +load_module /usr/lib/nginx/modules/ngx_cp_attachment_module.so; + +events { + worker_connections 1024; +} + +http { + include mime.types; + default_type application/octet-stream; + server_tokens off; + + log_format vhost 'openappsec-waf "$host" "$proxy_protocol_addr" "$request" "$http_user_agent" "$status"'; + + sendfile on; + keepalive_timeout 65; + fastcgi_param HTTP_PROXY ""; + + ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384; + + resolver 1.1.1.1 8.8.8.8; + resolver_timeout 2s; + + proxy_cache_valid 200 1d; + + include /etc/nginx/conf.d/*.conf; +} diff --git a/roles/openappsec/handlers/main.yaml b/roles/openappsec/handlers/main.yaml new file mode 100644 index 0000000..c3dfc0d --- /dev/null +++ b/roles/openappsec/handlers/main.yaml @@ -0,0 +1,13 @@ +--- + +# open-appsec agent +- name: 'restart openappsec-agent' + community.docker.docker_container: + name: 'openappsec-agent' + restart: 'true' + +# open-appsec nginx +- name: 'restart nginx' + community.docker.docker_container: + name: 'openappsec-nginx' + restart: 'true' diff --git a/roles/openappsec/tasks/main.yaml b/roles/openappsec/tasks/main.yaml new file mode 100644 index 0000000..a21c37b --- /dev/null +++ b/roles/openappsec/tasks/main.yaml @@ -0,0 +1,28 @@ +--- + +# create network +- name: 'create network' + ansible.builtin.import_tasks: 'openappsec/network.yaml' + +# install open-appsec +- name: 'install open-appsec' + ansible.builtin.import_tasks: 'openappsec/install.yaml' + + +# configure nginx +- name: 'configure nginx' + ansible.builtin.import_tasks: 'nginx/config.yaml' + +# loop vhosts +- name: 'config - vhosts' + ansible.builtin.import_tasks: 'nginx/vhosts.yaml' + + +# import dehydrated +- name: 'dehydrated' + ansible.builtin.import_tasks: 'nginx/dehydrated.yaml' + + +# cleanup docker +- name: 'cleanup docker' + ansible.builtin.import_tasks: 'openappsec/cleanup.yaml' diff --git a/roles/openappsec/tasks/nginx/config.yaml b/roles/openappsec/tasks/nginx/config.yaml new file mode 100644 index 0000000..91470e3 --- /dev/null +++ b/roles/openappsec/tasks/nginx/config.yaml @@ -0,0 +1,21 @@ +--- + +# configure dhparam file +- name: 'configure - dhparam' + ansible.builtin.copy: + src: 'files/nginx/dhparam.pem' + dest: '/etc/nginx/dhparam.pem' + owner: 'root' + group: 'root' + mode: '0644' + notify: 'restart nginx' + +# configure nginx +- name: 'config - configure nginx' + ansible.builtin.copy: + src: 'files/nginx/nginx.conf' + dest: '/etc/nginx/nginx.conf' + owner: 'root' + group: 'root' + mode: '0644' + notify: 'restart nginx' diff --git a/roles/openappsec/tasks/nginx/dehydrated.yaml b/roles/openappsec/tasks/nginx/dehydrated.yaml new file mode 100644 index 0000000..0605978 --- /dev/null +++ b/roles/openappsec/tasks/nginx/dehydrated.yaml @@ -0,0 +1,45 @@ +--- + +# install dehydrated +- name: 'install - dehydrated' + ansible.builtin.apt: + name: 'dehydrated' + state: 'present' + cache_valid_time: '120' + when: 'ansible_os_family == "Debian"' + + +# copy dehydrated configuration file from template +- name: 'revproxy - config - copy config from template' + ansible.builtin.copy: + src: 'files/dehydrated/config' + dest: '/etc/dehydrated/config' + owner: 'root' + group: 'root' + mode: '0644' + +# copy dehydrated configuration file from template +- name: 'revproxy - config - copy domains.txt from template' + ansible.builtin.copy: + src: 'files/dehydrated/domains.txt' + dest: '/etc/dehydrated/domains.txt' + owner: 'root' + group: 'root' + mode: '0644' + +# create dehydrated folder +- name: 'directory - dehydrated folder' + ansible.builtin.file: + path: '/mnt/certs/challenge' + state: 'directory' + owner: 'root' + group: 'root' + mode: '0755' + + +# symlink archive folder +- name: 'directory - dehydrated archive folder' + ansible.builtin.file: + src: '/mnt/certs/archive' + dest: '/etc/dehydrated/archive' + state: 'link' diff --git a/roles/openappsec/tasks/nginx/vhosts.yaml b/roles/openappsec/tasks/nginx/vhosts.yaml new file mode 100644 index 0000000..2f7f219 --- /dev/null +++ b/roles/openappsec/tasks/nginx/vhosts.yaml @@ -0,0 +1,13 @@ +--- + +# configure vhosts +- name: 'config - vhosts' + ansible.builtin.copy: + src: "{{ item }}" + dest: '/etc/nginx/conf.d/' + owner: 'root' + group: 'root' + mode: '0644' + notify: 'restart nginx' + with_fileglob: + - "files/nginx/conf.d/*" diff --git a/roles/openappsec/tasks/openappsec/cleanup.yaml b/roles/openappsec/tasks/openappsec/cleanup.yaml new file mode 100644 index 0000000..a33d4e8 --- /dev/null +++ b/roles/openappsec/tasks/openappsec/cleanup.yaml @@ -0,0 +1,18 @@ +--- + +# cleanup +- name: 'docker - prune all' + community.docker.docker_prune: + containers: true + images: true + networks: true + volumes: true + builder_cache: true + +- name: 'docker - force prune' + ansible.builtin.command: + "docker system prune \ + --all \ + --force \ + --volumes" + changed_when: false diff --git a/roles/openappsec/tasks/openappsec/install.yaml b/roles/openappsec/tasks/openappsec/install.yaml new file mode 100644 index 0000000..6bf1a70 --- /dev/null +++ b/roles/openappsec/tasks/openappsec/install.yaml @@ -0,0 +1,102 @@ +--- + +# create open-appsec directories +- name: 'create open-appsec directories' + ansible.builtin.file: + path: "/mnt/openappsec/{{ item }}" + state: 'directory' + mode: '0775' + with_items: + - 'conf' + - 'data' + - 'logs' + - 'model' + +# download advanced model +- name: 'download advanced model' + ansible.builtin.get_url: + url: '{{ openappsec_advanced_model_url }}' + dest: '/mnt/openappsec/model/advanced.tgz' + checksum: "sha256:{{ openappsec_advanced_model_sha }}" + notify: 'restart openappsec-agent' + + +# docker pull open-appsec agent +- name: 'run open-appsec agent' + community.docker.docker_container: + + # container_default_behavior + auto_remove: 'no' + container_default_behavior: 'no_defaults' + detach: 'yes' + init: 'no' + interactive: 'no' + memory: '0' + paused: 'no' + privileged: 'no' + pull: 'yes' + read_only: 'no' + state: 'started' + tty: 'no' + ipc_mode: 'host' + + # open-appsec - agent + name: 'openappsec-agent' + image: 'ghcr.io/openappsec/agent:1.1.4' + restart_policy: 'unless-stopped' + command: ["/cp-nano-agent", "--token", "{{ openappsec_token }}"] + + networks: + - name: 'openappsec' + + volumes: + - '/mnt/openappsec/conf:/etc/cp/conf' + - '/mnt/openappsec/data:/etc/cp/data' + - '/mnt/openappsec/logs:/var/log/nano_agent' + - '/mnt/openappsec/model/advanced.tgz:/advanced-model/open-appsec-advanced-model.tgz:rw' + + env: + + # global + TZ: '{{ timezone }}' + + +# docker pull open-appsec nginx +- name: 'run open-appsec nginx' + community.docker.docker_container: + + # container_default_behavior + auto_remove: 'no' + container_default_behavior: 'no_defaults' + detach: 'yes' + init: 'no' + interactive: 'no' + memory: '0' + paused: 'no' + privileged: 'no' + pull: 'yes' + read_only: 'no' + state: 'started' + tty: 'no' + ipc_mode: 'host' + + # open-appsec nginx + name: 'openappsec-nginx' + image: 'ghcr.io/openappsec/nginx-attachment:1.1.4' + restart_policy: 'unless-stopped' + + networks: + - name: 'openappsec' + + volumes: + - '/etc/nginx:/etc/nginx' + - '/mnt/certs:/mnt/certs' + + ports: + - '80:80/tcp' # http + - '443:443/tcp' # https + + env: + + # global + TZ: '{{ timezone }}' diff --git a/roles/openappsec/tasks/openappsec/network.yaml b/roles/openappsec/tasks/openappsec/network.yaml new file mode 100644 index 0000000..33aadb7 --- /dev/null +++ b/roles/openappsec/tasks/openappsec/network.yaml @@ -0,0 +1,6 @@ +--- + +# create network +- name: 'docker create openappsec network' + community.docker.docker_network: + name: 'openappsec'