diff --git a/README.md b/README.md index b2773cc..2ed6cc9 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,36 @@ ansible-playbook ansible/deploy-client.yml --private-key ~/.ssh/id_rsa The playbooks can all be run in any order. However they are designed with the assumption that you will run only either the TLS or non TLS playbooks, not both. Currently we do not support converting a cluster from non-TLS to TLS or vice versa. +## SASL Authentication Deployments + +### TLS + SASL Cluster + +Deploy a cluster with both TLS encryption and SASL authentication: + +```bash +export REDPANDA_SASL_PASSWORD="your-secure-password" +export SR_SERVICE_PASSWORD="schema-registry-password" +export PP_SERVICE_PASSWORD="pandaproxy-password" + +ansible-playbook ansible/provision-cluster-tls-sasl.yml \ + --private-key ~/.ssh/id_rsa \ + --inventory artifacts/hosts_gcp_$DEPLOYMENT_PREFIX.ini +``` + +### Managing Users and ACLs + +After deploying a SASL-enabled cluster, you can manage additional users and ACLs: + +```bash +export REDPANDA_SASL_PASSWORD="your-admin-password" +export PRODUCER_APP_PASSWORD="producer-password" +export CONSUMER_APP_PASSWORD="consumer-password" + +ansible-playbook ansible/manage-sasl-users.yml \ + --private-key ~/.ssh/id_rsa \ + --inventory artifacts/hosts_gcp_$DEPLOYMENT_PREFIX.ini +``` + ## Additional Documentation More information on consuming this collection diff --git a/ansible/manage-sasl-users.yml b/ansible/manage-sasl-users.yml new file mode 100644 index 0000000..4a063e3 --- /dev/null +++ b/ansible/manage-sasl-users.yml @@ -0,0 +1,76 @@ +--- +# manages users, roles, and ACLs declaratively via user_management role +- name: Manage SASL Users, Roles, and ACLs + hosts: redpanda[0] + become: true + vars: + enable_tls: true + redpanda_truststore_file: "/etc/redpanda/certs/truststore.pem" + sasl_admin_username: "admin" + sasl_admin_password: "{{ lookup('env', 'REDPANDA_SASL_PASSWORD') }}" + + # users (set state: absent to delete) + sasl_users: + - username: "producer_app" + password: "{{ lookup('env', 'PRODUCER_APP_PASSWORD') | default('producer-secret', true) }}" + mechanism: "SCRAM-SHA-256" + state: present + + - username: "consumer_app" + password: "{{ lookup('env', 'CONSUMER_APP_PASSWORD') | default('consumer-secret', true) }}" + mechanism: "SCRAM-SHA-256" + state: present + + # roles (enterprise RBAC) + sasl_roles: [] + + # kafka ACLs + sasl_acls: + - principal: "User:producer_app" + resource_type: topic + resource_name: "events-" + pattern_type: prefixed + operation: + - write + - describe + permission: allow + state: present + + - principal: "User:consumer_app" + resource_type: topic + resource_name: "events-" + pattern_type: prefixed + operation: + - read + - describe + permission: allow + state: present + + - principal: "User:consumer_app" + resource_type: group + resource_name: "consumer-" + pattern_type: prefixed + operation: read + permission: allow + state: present + + # schema registry ACLs (enterprise) + schema_registry_acls: + - principal: "User:producer_app" + resource_type: subject + resource_name: "events-" + pattern_type: prefixed + operation: write + permission: allow + state: present + + - principal: "User:consumer_app" + resource_type: subject + resource_name: "events-" + pattern_type: prefixed + operation: read + permission: allow + state: present + + roles: + - role: redpanda.cluster.user_management diff --git a/ansible/operation-apply-license.yml b/ansible/operation-apply-license.yml index e05b9e0..f678896 100644 --- a/ansible/operation-apply-license.yml +++ b/ansible/operation-apply-license.yml @@ -5,32 +5,54 @@ vars: rpk_bin: rpk + # SASL/TLS settings for authenticated clusters + kafka_enable_authorization: false + admin_api_require_auth: false + sasl_superuser_username: "admin" + sasl_superuser_password: "" + enable_tls: false + redpanda_truststore_file: /etc/redpanda/certs/truststore.pem + redpanda_kafka_port: 9092 + + rpk_opts: >- + -X brokers={{ hostvars[inventory_hostname].private_ip | default(ansible_default_ipv4.address) }}:{{ redpanda_kafka_port }} + {% if enable_tls | default(false) %}-X tls.enabled=true -X tls.ca={{ redpanda_truststore_file }}{% endif %} + {% if kafka_enable_authorization | default(false) and sasl_superuser_password != '' %}-X user={{ sasl_superuser_username }} -X pass={{ sasl_superuser_password }} -X sasl.mechanism=SCRAM-SHA-256{% endif %} + + rpk_admin_opts: >- + {% if enable_tls | default(false) %}-X admin.tls.enabled=true -X admin.tls.ca={{ redpanda_truststore_file }}{% endif %} + {% if admin_api_require_auth | default(false) and sasl_superuser_password != '' %}-X user={{ sasl_superuser_username }} -X pass={{ sasl_superuser_password }}{% endif %} + tasks: - name: Check cluster health ansible.builtin.shell: | - {{ rpk_bin }} cluster health | grep -i 'healthy:' | tr -d '[:space:]' | awk -F ':' '{print tolower($2)}' + {{ rpk_bin }} cluster health {{ rpk_opts }} {{ rpk_admin_opts }} | grep -i 'healthy:' | tr -d '[:space:]' | awk -F ':' '{print tolower($2)}' register: health_check run_once: true failed_when: "health_check.stdout != 'true'" changed_when: false + no_log: "{{ kafka_enable_authorization | default(false) }}" - name: Set Redpanda license (string) - ansible.builtin.command: rpk cluster license set {{ redpanda_license }} + ansible.builtin.command: "{{ rpk_bin }} cluster license set {{ redpanda_license }} {{ rpk_opts }} {{ rpk_admin_opts }}" run_once: true changed_when: false when: - redpanda_license is defined + no_log: true - name: Set Redpanda license (path) - ansible.builtin.command: rpk cluster license set --path {{ redpanda_license_path }} + ansible.builtin.command: "{{ rpk_bin }} cluster license set --path {{ redpanda_license_path }} {{ rpk_opts }} {{ rpk_admin_opts }}" changed_when: false run_once: true when: - redpanda_license_path is defined + no_log: "{{ kafka_enable_authorization | default(false) }}" - name: Check broker status ansible.builtin.shell: | - {{ rpk_bin }} redpanda admin brokers list | grep -q 'active.*true' + {{ rpk_bin }} redpanda admin brokers list {{ rpk_admin_opts }} | grep -q 'active.*true' register: broker_status changed_when: false failed_when: broker_status.rc != 0 + no_log: "{{ kafka_enable_authorization | default(false) }}" diff --git a/ansible/operation-configure-logging.yml b/ansible/operation-configure-logging.yml index 43418ee..7142420 100644 --- a/ansible/operation-configure-logging.yml +++ b/ansible/operation-configure-logging.yml @@ -5,15 +5,34 @@ vars: rpk_bin: rpk + # SASL/TLS settings for authenticated clusters + kafka_enable_authorization: false + admin_api_require_auth: false + sasl_superuser_username: "admin" + sasl_superuser_password: "" + enable_tls: false + redpanda_truststore_file: /etc/redpanda/certs/truststore.pem + redpanda_kafka_port: 9092 + + rpk_opts: >- + -X brokers={{ hostvars[inventory_hostname].private_ip | default(ansible_default_ipv4.address) }}:{{ redpanda_kafka_port }} + {% if enable_tls | default(false) %}-X tls.enabled=true -X tls.ca={{ redpanda_truststore_file }}{% endif %} + {% if kafka_enable_authorization | default(false) and sasl_superuser_password != '' %}-X user={{ sasl_superuser_username }} -X pass={{ sasl_superuser_password }} -X sasl.mechanism=SCRAM-SHA-256{% endif %} + + rpk_admin_opts: >- + {% if enable_tls | default(false) %}-X admin.tls.enabled=true -X admin.tls.ca={{ redpanda_truststore_file }}{% endif %} + {% if admin_api_require_auth | default(false) and sasl_superuser_password != '' %}-X user={{ sasl_superuser_username }} -X pass={{ sasl_superuser_password }}{% endif %} + tasks: - name: Check cluster health ansible.builtin.shell: | set -o pipefail - {{ rpk_bin }} cluster health | grep -i 'healthy:' | tr -d '[:space:]' | awk -F ':' '{print tolower($2)}' + {{ rpk_bin }} cluster health {{ rpk_opts }} {{ rpk_admin_opts }} | grep -i 'healthy:' | tr -d '[:space:]' | awk -F ':' '{print tolower($2)}' register: health_check run_once: true failed_when: "health_check.stdout != 'true'" changed_when: false + no_log: "{{ kafka_enable_authorization | default(false) }}" - name: Apply logging role ansible.builtin.include_role: @@ -59,10 +78,11 @@ - name: Check broker status ansible.builtin.shell: | set -o pipefail - {{ rpk_bin }} redpanda admin brokers list | grep -q 'active.*true' + {{ rpk_bin }} redpanda admin brokers list {{ rpk_admin_opts }} | grep -q 'active.*true' register: broker_status changed_when: false failed_when: broker_status.rc != 0 + no_log: "{{ kafka_enable_authorization | default(false) }}" - name: Display logging configuration summary ansible.builtin.debug: diff --git a/ansible/operation-rolling-restart.yml b/ansible/operation-rolling-restart.yml index 63224d6..9341e7d 100644 --- a/ansible/operation-rolling-restart.yml +++ b/ansible/operation-rolling-restart.yml @@ -6,43 +6,66 @@ vars: rpk_bin: rpk + # SASL/TLS settings for authenticated clusters + kafka_enable_authorization: false + admin_api_require_auth: false + sasl_superuser_username: "admin" + sasl_superuser_password: "" + enable_tls: false + redpanda_truststore_file: /etc/redpanda/certs/truststore.pem + redpanda_kafka_port: 9092 + + rpk_opts: >- + -X brokers={{ hostvars[inventory_hostname].private_ip | default(ansible_default_ipv4.address) }}:{{ redpanda_kafka_port }} + {% if enable_tls | default(false) %}-X tls.enabled=true -X tls.ca={{ redpanda_truststore_file }}{% endif %} + {% if kafka_enable_authorization | default(false) and sasl_superuser_password != '' %}-X user={{ sasl_superuser_username }} -X pass={{ sasl_superuser_password }} -X sasl.mechanism=SCRAM-SHA-256{% endif %} + + rpk_admin_opts: >- + {% if enable_tls | default(false) %}-X admin.tls.enabled=true -X admin.tls.ca={{ redpanda_truststore_file }}{% endif %} + {% if admin_api_require_auth | default(false) and sasl_superuser_password != '' %}-X user={{ sasl_superuser_username }} -X pass={{ sasl_superuser_password }}{% endif %} + tasks: - name: Check cluster health ansible.builtin.shell: | - {{ rpk_bin }} cluster health | grep -i 'healthy:' | tr -d '[:space:]' | awk -F ':' '{print tolower($2)}' + {{ rpk_bin }} cluster health {{ rpk_opts }} {{ rpk_admin_opts }} | grep -i 'healthy:' | tr -d '[:space:]' | awk -F ':' '{print tolower($2)}' register: health_check failed_when: "health_check.stdout != 'true'" changed_when: false + no_log: "{{ kafka_enable_authorization | default(false) }}" - name: Get node ID ansible.builtin.shell: | - {{ rpk_bin }} cluster info | awk '$2 == "{{ ansible_host }}" {gsub("\\*", "", $1); print $1}' + {{ rpk_bin }} cluster info {{ rpk_opts }} | awk '$2 == "{{ ansible_host }}" {gsub("\\*", "", $1); print $1}' register: node_id changed_when: false + no_log: "{{ kafka_enable_authorization | default(false) }}" - name: Enable maintenance mode - ansible.builtin.command: "{{ rpk_bin }} cluster maintenance enable {{ node_id.stdout }} --wait" + ansible.builtin.command: "{{ rpk_bin }} cluster maintenance enable {{ node_id.stdout }} --wait {{ rpk_opts }} {{ rpk_admin_opts }}" register: maintenance_result failed_when: - "'Successfully enabled maintenance mode' not in maintenance_result.stdout" - "'Maintenance mode is already enabled for node' not in maintenance_result.stdout" changed_when: "'Successfully enabled maintenance mode' in maintenance_result.stdout" + no_log: "{{ kafka_enable_authorization | default(false) }}" - name: Verify maintenance mode status ansible.builtin.shell: | - {{ rpk_bin }} cluster maintenance status | grep -q '{{ node_id.stdout }}' + {{ rpk_bin }} cluster maintenance status {{ rpk_opts }} {{ rpk_admin_opts }} | grep -q '{{ node_id.stdout }}' register: maintenance_status failed_when: maintenance_status.rc != 0 changed_when: false + no_log: "{{ kafka_enable_authorization | default(false) }}" - name: Check cluster health after enabling maintenance mode ansible.builtin.shell: | - {{ rpk_bin }} cluster health --watch --exit-when-healthy | grep -i 'healthy:' | tr -d '[:space:]' | awk -F ':' '{print tolower($2)}' + {{ rpk_bin }} cluster health --watch --exit-when-healthy {{ rpk_opts }} {{ rpk_admin_opts }} | grep -i 'healthy:' | tr -d '[:space:]' | awk -F ':' '{print tolower($2)}' register: health_check_maintenance failed_when: "health_check_maintenance.stdout != 'true'" retries: 10 delay: 30 changed_when: false + no_log: "{{ kafka_enable_authorization | default(false) }}" - name: Stop Redpanda service ansible.builtin.systemd: @@ -55,30 +78,34 @@ state: started - name: Disable maintenance mode - ansible.builtin.command: "{{ rpk_bin }} cluster maintenance disable {{ node_id.stdout }}" + ansible.builtin.command: "{{ rpk_bin }} cluster maintenance disable {{ node_id.stdout }} {{ rpk_opts }} {{ rpk_admin_opts }}" register: disable_maintenance_result changed_when: "'Successfully disabled maintenance mode' in disable_maintenance_result.stdout" failed_when: "'Successfully disabled maintenance mode' not in disable_maintenance_result.stdout" + no_log: "{{ kafka_enable_authorization | default(false) }}" - name: Verify maintenance mode is disabled ansible.builtin.shell: | - {{ rpk_bin }} cluster maintenance status | grep -qv '{{ node_id.stdout }}' + {{ rpk_bin }} cluster maintenance status {{ rpk_opts }} {{ rpk_admin_opts }} | grep -qv '{{ node_id.stdout }}' register: maintenance_status_after failed_when: maintenance_status_after.rc != 0 changed_when: false + no_log: "{{ kafka_enable_authorization | default(false) }}" - name: Check cluster health after disabling maintenance mode ansible.builtin.shell: | - {{ rpk_bin }} cluster health --watch --exit-when-healthy | grep -i 'healthy:' | tr -d '[:space:]' | awk -F ':' '{print tolower($2)}' + {{ rpk_bin }} cluster health --watch --exit-when-healthy {{ rpk_opts }} {{ rpk_admin_opts }} | grep -i 'healthy:' | tr -d '[:space:]' | awk -F ':' '{print tolower($2)}' register: health_check_maintenance failed_when: "health_check_maintenance.stdout != 'true'" retries: 10 delay: 30 changed_when: false + no_log: "{{ kafka_enable_authorization | default(false) }}" - name: Check broker status ansible.builtin.shell: | - {{ rpk_bin }} redpanda admin brokers list | grep -q 'active.*true' + {{ rpk_bin }} redpanda admin brokers list {{ rpk_admin_opts }} | grep -q 'active.*true' register: broker_status changed_when: false failed_when: broker_status.rc != 0 + no_log: "{{ kafka_enable_authorization | default(false) }}" diff --git a/ansible/provision-cluster-tls-sasl.yml b/ansible/provision-cluster-tls-sasl.yml new file mode 100644 index 0000000..239e8b7 --- /dev/null +++ b/ansible/provision-cluster-tls-sasl.yml @@ -0,0 +1,69 @@ +--- +- name: Provision Redpanda Cluster with TLS and SASL + hosts: redpanda + vars: + # Redpanda version + redpanda_version: latest + + # Disable no_log for debugging + redpanda_broker_no_log: false + + # TLS configuration + root_ca_dir: "{{ playbook_dir }}/tls/ca" + enable_tls: true + advertise_public_ips: true + create_demo_certs: true + handle_cert_install: true + ca_cert_file: "tls/ca/ca.crt" + node_cert_file: "tls/certs/{{ ansible_hostname }}/node.crt" + + # SASL configuration + kafka_enable_authorization: true + admin_api_require_auth: true + schema_registry_enable_authorization: true + + # Require authentication on HTTP APIs (Schema Registry and HTTP Proxy) + schema_registry_authn_method: http_basic + pandaproxy_authn_method: http_basic + + # Apply Enterprise license + redpanda_license_file: "{{ playbook_dir }}/../redpanda.license" + + sasl_superuser_username: "admin" + sasl_superuser_password: "{{ lookup('env', 'REDPANDA_SASL_PASSWORD') | default('change-me-in-production', true) }}" + + # Service accounts + schema_registry_service_user: "schema_registry_client" + schema_registry_service_password: "{{ lookup('env', 'SR_SERVICE_PASSWORD') | default('change-me', true) }}" + pandaproxy_service_user: "pandaproxy_client" + pandaproxy_service_password: "{{ lookup('env', 'PP_SERVICE_PASSWORD') | default('change-me', true) }}" + + # Kafka listeners with SASL + redpanda_kafka_listeners: + - address: "{{ hostvars[inventory_hostname].private_ip }}" + port: 9092 + name: "external" + authentication_method: "sasl" + + # Advertised listener + redpanda_advertised_kafka_listeners: + - address: "{{ hostvars[inventory_hostname].advertised_ip }}" + port: 9092 + name: "external" + + tasks: + - name: Install and configure CA certs for running TLS + ansible.builtin.include_role: + name: redpanda.cluster.demo_certs + + - name: Install system prereqs + ansible.builtin.include_role: + name: redpanda.cluster.system_setup + + - name: Handle sysctl changes + ansible.builtin.include_role: + name: redpanda.cluster.sysctl_setup + + - name: Install and start redpanda + ansible.builtin.include_role: + name: redpanda.cluster.redpanda_broker