diff --git a/ansible/octavia-certificates.yml b/ansible/octavia-certificates.yml
new file mode 100644
index 0000000000..aeebb814f1
--- /dev/null
+++ b/ansible/octavia-certificates.yml
@@ -0,0 +1,5 @@
+---
+- name: Apply role octavia-certificates
+  hosts: localhost
+  roles:
+    - octavia-certificates
diff --git a/ansible/roles/octavia-certificates/defaults/main.yml b/ansible/roles/octavia-certificates/defaults/main.yml
new file mode 100644
index 0000000000..67fe9085af
--- /dev/null
+++ b/ansible/roles/octavia-certificates/defaults/main.yml
@@ -0,0 +1,45 @@
+---
+#####################
+# Certificate options.
+#####################
+
+octavia_certs_work_dir: "{{ node_config }}/octavia-certificates"
+
+# OpenSSL configuration file path.
+octavia_certs_openssl_cnf_path: openssl.cnf
+
+# For more info see: https://en.wikipedia.org/wiki/Certificate_signing_request
+# Country; The two-letter ISO code for the country where your organization is located
+octavia_certs_country: US
+# Province, Region, County or State
+octavia_certs_state: Oregon
+# Business name / Organization
+octavia_certs_organization: OpenStack
+# Department Name / Organizational Unit
+octavia_certs_organizational_unit: Octavia
+
+# Server CA.
+octavia_certs_server_ca_expiry: 3650
+octavia_certs_server_ca_country: "{{ octavia_certs_country }}"
+octavia_certs_server_ca_state: "{{ octavia_certs_state }}"
+octavia_certs_server_ca_organization: "{{ octavia_certs_organization }}"
+octavia_certs_server_ca_organizational_unit: "{{ octavia_certs_organizational_unit }}"
+octavia_certs_server_ca_common_name: server-ca.example.org
+
+# Client CA.
+octavia_certs_client_ca_expiry: 3650
+octavia_certs_client_ca_country: "{{ octavia_certs_country }}"
+octavia_certs_client_ca_state: "{{ octavia_certs_state }}"
+octavia_certs_client_ca_organization: "{{ octavia_certs_organization }}"
+octavia_certs_client_ca_organizational_unit: "{{ octavia_certs_organizational_unit }}"
+octavia_certs_client_ca_common_name: client-ca.example.org
+
+# Client certificate.
+octavia_certs_client_expiry: 365
+octavia_certs_client_req_country: "{{ octavia_certs_country }}"
+octavia_certs_client_req_state: "{{ octavia_certs_state }}"
+octavia_certs_client_req_organization: "{{ octavia_certs_organization }}"
+octavia_certs_client_req_organizational_unit: "{{ octavia_certs_organizational_unit }}"
+# NOTE(yoctozepto): This should ideally be per controller, i.e. controller
+# generates its key&CSR and this CA signs it.
+octavia_certs_client_req_common_name: client.example.org
diff --git a/ansible/roles/octavia-certificates/files/openssl.cnf b/ansible/roles/octavia-certificates/files/openssl.cnf
new file mode 100644
index 0000000000..34161b89e7
--- /dev/null
+++ b/ansible/roles/octavia-certificates/files/openssl.cnf
@@ -0,0 +1,49 @@
+[ client_ca ]
+new_certs_dir     = .
+database          = index.txt
+serial            = serial
+RANDFILE          = .rand
+
+private_key       = client_ca.key.pem
+certificate       = client_ca.cert.pem
+
+# SHA-1 is deprecated, so use SHA-2 instead.
+default_md        = sha256
+
+name_opt          = ca_default
+cert_opt          = ca_default
+default_days      = 3650
+
+x509_extensions   = client_cert
+
+policy            = policy_any
+
+[ policy_any ]
+countryName            = supplied
+stateOrProvinceName    = optional
+organizationName       = optional
+organizationalUnitName = optional
+commonName             = supplied
+emailAddress           = optional
+
+[ req ]
+distinguished_name = req_distinguished_name
+x509_extensions    = v3_ca
+
+# SHA-1 is deprecated, so use SHA-2 instead.
+default_md         = sha256
+
+[ req_distinguished_name ]
+
+[ v3_ca ]
+subjectKeyIdentifier = hash
+authorityKeyIdentifier = keyid:always
+basicConstraints = critical, CA:TRUE
+keyUsage = critical, cRLSign, keyCertSign
+
+[ client_cert ]
+subjectKeyIdentifier = hash
+authorityKeyIdentifier = keyid:always
+basicConstraints = critical, CA:FALSE
+keyUsage = critical, digitalSignature
+extendedKeyUsage = clientAuth
diff --git a/ansible/roles/octavia-certificates/tasks/client_ca.yml b/ansible/roles/octavia-certificates/tasks/client_ca.yml
new file mode 100644
index 0000000000..b9cd711e18
--- /dev/null
+++ b/ansible/roles/octavia-certificates/tasks/client_ca.yml
@@ -0,0 +1,43 @@
+---
+
+- name: Create client_ca index.txt
+  copy:
+    content: ''
+    dest: "{{ octavia_certs_work_dir }}/client_ca/index.txt"
+    force: no
+    mode: 0660
+
+- name: Create client_ca serial
+  copy:
+    content: "1000\n"
+    dest: "{{ octavia_certs_work_dir }}/client_ca/serial"
+    force: no
+    mode: 0660
+
+- name: Create client_ca private key
+  command: >
+    openssl genrsa -aes256 -out client_ca.key.pem
+    -passout pass:{{ octavia_client_ca_password }} 4096
+  args:
+    chdir: "{{ octavia_certs_work_dir }}/client_ca"
+    creates: "{{ octavia_certs_work_dir }}/client_ca/client_ca.key.pem"
+
+- name: Create client_ca certificate
+  vars:
+    client_ca_subject:
+      C: "{{ octavia_certs_client_ca_country }}"
+      ST: "{{ octavia_certs_client_ca_state }}"
+      O: "{{ octavia_certs_client_ca_organization }}"
+      OU: "{{ octavia_certs_client_ca_organizational_unit }}"
+      CN: "{{ octavia_certs_client_ca_common_name }}"
+  command: >
+    openssl req -new -x509 -config ../openssl.cnf
+    -key client_ca.key.pem
+    -days {{ octavia_certs_client_ca_expiry }}
+    -out client_ca.cert.pem
+    -subj "/{{ client_ca_subject.items() | map('join', '=') | join('/') }}"
+    -passin pass:{{ octavia_client_ca_password }}
+    -batch
+  args:
+    chdir: "{{ octavia_certs_work_dir }}/client_ca"
+    creates: "{{ octavia_certs_work_dir }}/client_ca/client_ca.cert.pem"
diff --git a/ansible/roles/octavia-certificates/tasks/client_cert.yml b/ansible/roles/octavia-certificates/tasks/client_cert.yml
new file mode 100644
index 0000000000..384c7d81b0
--- /dev/null
+++ b/ansible/roles/octavia-certificates/tasks/client_cert.yml
@@ -0,0 +1,50 @@
+---
+
+# NOTE(yoctozepto): This should ideally be per controller, i.e. controller
+# generates its key&CSR and this CA signs it.
+
+- name: Create a key for the client certificate
+  command: >
+    openssl genrsa -out client.key.pem 4096
+  args:
+    chdir: "{{ octavia_certs_work_dir }}/client_ca"
+    creates: "{{ octavia_certs_work_dir }}/client_ca/client.key.pem"
+
+- name: Create the certificate request for the client certificate
+  vars:
+    client_req_subject:
+      C: "{{ octavia_certs_client_req_country }}"
+      ST: "{{ octavia_certs_client_req_state }}"
+      O: "{{ octavia_certs_client_req_organization }}"
+      OU: "{{ octavia_certs_client_req_organizational_unit }}"
+      CN: "{{ octavia_certs_client_req_common_name }}"
+  command: >
+    openssl req -new -config ../openssl.cnf
+    -key client.key.pem
+    -out client.csr.pem
+    -subj "/{{ client_req_subject.items() | map('join', '=') | join('/') }}"
+    -batch
+  args:
+    chdir: "{{ octavia_certs_work_dir }}/client_ca"
+    creates: "{{ octavia_certs_work_dir }}/client_ca/client.csr.pem"
+
+- name: Sign the client certificate request
+  command: >
+    openssl ca -config ../openssl.cnf
+    -name client_ca
+    -days {{ octavia_certs_client_expiry }}
+    -in client.csr.pem
+    -out client.cert.pem
+    -key {{ octavia_client_ca_password }}
+    -notext
+    -batch
+  args:
+    chdir: "{{ octavia_certs_work_dir }}/client_ca"
+    creates: "{{ octavia_certs_work_dir }}/client_ca/client.cert.pem"
+
+- name: Create a concatenated client certificate and key file
+  assemble:
+    regexp: ^client\.(cert|key)\.pem$
+    src: "{{ octavia_certs_work_dir }}/client_ca"
+    dest: "{{ octavia_certs_work_dir }}/client_ca/client.cert-and-key.pem"
+    mode: "0660"
diff --git a/ansible/roles/octavia-certificates/tasks/main.yml b/ansible/roles/octavia-certificates/tasks/main.yml
new file mode 100644
index 0000000000..58ad6c9e55
--- /dev/null
+++ b/ansible/roles/octavia-certificates/tasks/main.yml
@@ -0,0 +1,44 @@
+---
+# This play adapts https://docs.openstack.org/octavia/victoria/admin/guides/certificates.html
+
+# Kolla-Ansible prepares the Server CA certificate and key for use by Octavia
+# to generate Amphorae certificates.
+
+# Kolla-Ansible prepares and controls the Client CA certificate and key.
+# Client CA is used to generate certificates for Octavia controllers.
+
+- name: Ensure server_ca and client_ca directories exist
+  file:
+    path: "{{ octavia_certs_work_dir }}/{{ item }}"
+    state: "directory"
+    mode: 0770
+  loop:
+    - server_ca
+    - client_ca
+
+- name: Copy openssl.cnf
+  copy:
+    src: "{{ octavia_certs_openssl_cnf_path }}"
+    dest: "{{ octavia_certs_work_dir }}/openssl.cnf"
+
+- import_tasks: server_ca.yml
+
+- import_tasks: client_ca.yml
+
+- import_tasks: client_cert.yml
+
+- name: Ensure {{ node_custom_config }}/octavia directory exists
+  file:
+    path: "{{ node_custom_config }}/octavia"
+    state: "directory"
+    mode: 0770
+
+- name: Copy the to-be-deployed keys and certs to {{ node_custom_config }}/octavia
+  copy:
+    src: "{{ octavia_certs_work_dir }}/{{ item.src }}"
+    dest: "{{ node_custom_config }}/octavia/{{ item.dest }}"
+  with_items:
+    - { src: "server_ca/server_ca.cert.pem", dest: "server_ca.cert.pem" }
+    - { src: "server_ca/server_ca.key.pem", dest: "server_ca.key.pem" }
+    - { src: "client_ca/client_ca.cert.pem", dest: "client_ca.cert.pem" }
+    - { src: "client_ca/client.cert-and-key.pem", dest: "client.cert-and-key.pem" }
diff --git a/ansible/roles/octavia-certificates/tasks/server_ca.yml b/ansible/roles/octavia-certificates/tasks/server_ca.yml
new file mode 100644
index 0000000000..15c30f8934
--- /dev/null
+++ b/ansible/roles/octavia-certificates/tasks/server_ca.yml
@@ -0,0 +1,29 @@
+---
+
+- name: Generate server_ca private key
+  command: >
+    openssl genrsa -aes256 -out server_ca.key.pem
+    -passout pass:{{ octavia_ca_password }} 4096
+  args:
+    chdir: "{{ octavia_certs_work_dir }}/server_ca"
+    creates: "{{ octavia_certs_work_dir }}/server_ca/server_ca.key.pem"
+
+- name: Create server_ca certificate
+  vars:
+    server_ca_subject:
+      C: "{{ octavia_certs_server_ca_country }}"
+      ST: "{{ octavia_certs_server_ca_state }}"
+      O: "{{ octavia_certs_server_ca_organization }}"
+      OU: "{{ octavia_certs_server_ca_organizational_unit }}"
+      CN: "{{ octavia_certs_server_ca_common_name }}"
+  command: >
+    openssl req -new -x509 -config ../openssl.cnf
+    -key server_ca.key.pem
+    -days {{ octavia_certs_server_ca_expiry }}
+    -out server_ca.cert.pem
+    -subj "/{{ server_ca_subject.items() | map('join', '=') | join('/') }}"
+    -passin pass:{{ octavia_ca_password }}
+    -batch
+  args:
+    chdir: "{{ octavia_certs_work_dir }}/server_ca"
+    creates: "{{ octavia_certs_work_dir }}/server_ca/server_ca.cert.pem"
diff --git a/etc/kolla/passwords.yml b/etc/kolla/passwords.yml
index 902d8d1d62..80fafec360 100644
--- a/etc/kolla/passwords.yml
+++ b/etc/kolla/passwords.yml
@@ -170,6 +170,7 @@ manila_keystone_password:
 octavia_database_password:
 octavia_keystone_password:
 octavia_ca_password:
+octavia_client_ca_password:
 
 searchlight_keystone_password:
 
diff --git a/tests/check-config.sh b/tests/check-config.sh
index de6577e08f..ce77711ab2 100755
--- a/tests/check-config.sh
+++ b/tests/check-config.sh
@@ -16,6 +16,7 @@ function check_config {
     for f in $(sudo find /etc/kolla \
                 -not -regex /etc/kolla/config.* \
                 -not -regex /etc/kolla/certificates.* \
+                -not -regex /etc/kolla/octavia-certificates.* \
                 -not -regex .*pem \
                 -not -regex .*key \
                 -not -regex ".*ca-certificates.*" \
diff --git a/tests/run.yml b/tests/run.yml
index f9190b0ee1..7caad43edf 100644
--- a/tests/run.yml
+++ b/tests/run.yml
@@ -194,24 +194,6 @@
               dest: ironic-agent.kernel
       when: scenario == "ironic"
 
-    - block:
-        - name: ensure octavia config directory exists
-          file:
-            path: /etc/kolla/config/octavia
-            state: directory
-            mode: 0777
-
-        - name: create dummy TLS certificates for octavia
-          file:
-            path: "/etc/kolla/config/octavia/{{ item }}"
-            state: touch
-          with_items:
-            - client.cert-and-key.pem
-            - client_ca.cert.pem
-            - server_ca.cert.pem
-            - server_ca.key.pem
-      when: scenario == 'magnum'
-
     - name: ensure /etc/ansible exists
       file:
         path: /etc/ansible
@@ -282,6 +264,13 @@
         docker_image_tag: "{{ build_image_tag if need_build_image else (zuul.branch | basename) ~ docker_image_tag_suffix }}"
         docker_image_prefix: "{{ 'primary:4000/lokolla/' if need_build_image else 'kolla/' }}"
 
+    # NOTE(yoctozepto): k-a octavia-certificates should run before k-a bootstrap-servers
+    # because the latter hijacks /etc/kolla permissions (due to same directory on the
+    # same host being used by both)
+    - name: create TLS certificates for octavia
+      command: kolla-ansible octavia-certificates
+      when: scenario == 'magnum'
+
     # NOTE(mgoddard): We are using the script module here and later to ensure
     # we use the local copy of these scripts, rather than the one on the remote
     # host, which could be checked out to a previous release (in an upgrade
diff --git a/tools/kolla-ansible b/tools/kolla-ansible
index 9d8ba9e137..87cc7a4404 100755
--- a/tools/kolla-ansible
+++ b/tools/kolla-ansible
@@ -121,29 +121,30 @@ Environment variables:
     EXTRA_OPTS                         Additional arguments to pass to ansible-playbook
 
 Commands:
-    prechecks           Do pre-deployment checks for hosts
-    check               Do post-deployment smoke tests
-    mariadb_recovery    Recover a completely stopped mariadb cluster
-    mariadb_backup      Take a backup of MariaDB databases
-                            --full (default)
-                            --incremental
-    bootstrap-servers   Bootstrap servers with kolla deploy dependencies
-    destroy             Destroy Kolla containers, volumes and host configuration
-                            --include-images to also destroy Kolla images
-                            --include-dev to also destroy dev mode repos
-    deploy              Deploy and start all kolla containers
-    deploy-bifrost      Deploy and start bifrost container
-    deploy-servers      Enroll and deploy servers with bifrost
-    deploy-containers   Only deploy and start containers (no config updates or bootstrapping)
-    post-deploy         Do post deploy on deploy node
-    pull                Pull all images for containers (only pulls, no running container changes)
-    reconfigure         Reconfigure OpenStack service
-    stop                Stop Kolla containers
-    certificates        Generate self-signed certificate for TLS *For Development Only*
-    upgrade             Upgrades existing OpenStack Environment
-    upgrade-bifrost     Upgrades an existing bifrost container
-    genconfig           Generate configuration files for enabled OpenStack services
-    prune-images        Prune orphaned Kolla images
+    prechecks            Do pre-deployment checks for hosts
+    check                Do post-deployment smoke tests
+    mariadb_recovery     Recover a completely stopped mariadb cluster
+    mariadb_backup       Take a backup of MariaDB databases
+                             --full (default)
+                             --incremental
+    bootstrap-servers    Bootstrap servers with kolla deploy dependencies
+    destroy              Destroy Kolla containers, volumes and host configuration
+                             --include-images to also destroy Kolla images
+                             --include-dev to also destroy dev mode repos
+    deploy               Deploy and start all kolla containers
+    deploy-bifrost       Deploy and start bifrost container
+    deploy-servers       Enroll and deploy servers with bifrost
+    deploy-containers    Only deploy and start containers (no config updates or bootstrapping)
+    post-deploy          Do post deploy on deploy node
+    pull                 Pull all images for containers (only pulls, no running container changes)
+    reconfigure          Reconfigure OpenStack service
+    stop                 Stop Kolla containers
+    certificates         Generate self-signed certificate for TLS *For Development Only*
+    octavia-certificates Generate certificates for octavia deployment
+    upgrade              Upgrades existing OpenStack Environment
+    upgrade-bifrost      Upgrades an existing bifrost container
+    genconfig            Generate configuration files for enabled OpenStack services
+    prune-images         Prune orphaned Kolla images
 EOF
 }
 
@@ -179,6 +180,7 @@ pull
 reconfigure
 stop
 certificates
+octavia-certificates
 upgrade
 upgrade-bifrost
 genconfig
@@ -431,6 +433,10 @@ EOF
         ACTION="Generate TLS Certificates"
         PLAYBOOK="${BASEDIR}/ansible/certificates.yml"
         ;;
+(octavia-certificates)
+        ACTION="Generate octavia Certificates"
+        PLAYBOOK="${BASEDIR}/ansible/octavia-certificates.yml"
+        ;;
 (genconfig)
         ACTION="Generate configuration files for enabled OpenStack services"
         EXTRA_OPTS="$EXTRA_OPTS -e kolla_action=config"
diff --git a/zuul.d/base.yaml b/zuul.d/base.yaml
index 87760c25e2..e7c0739a24 100644
--- a/zuul.d/base.yaml
+++ b/zuul.d/base.yaml
@@ -131,7 +131,7 @@
     parent: kolla-ansible-base
     voting: false
     files:
-      - ^ansible/roles/(designate|magnum|octavia)/
+      - ^ansible/roles/(designate|magnum|octavia|octavia-certificates)/
       - ^tests/test-dashboard.sh
       - ^tests/test-magnum.sh
     vars: