diff --git a/.github/BOTMETA.yml b/.github/BOTMETA.yml
index 7072bcdff8..451701dd2d 100644
--- a/.github/BOTMETA.yml
+++ b/.github/BOTMETA.yml
@@ -217,6 +217,8 @@ files:
     maintainers: Akasurde
   $lookups/random_string.py:
     maintainers: Akasurde
+  $lookups/random_words.py:
+    maintainers: konstruktoid
   $lookups/redis.py:
     maintainers: $team_ansible_core jpmens
   $lookups/shelvefile.py: {}
diff --git a/plugins/lookup/random_words.py b/plugins/lookup/random_words.py
new file mode 100644
index 0000000000..a2381aa38f
--- /dev/null
+++ b/plugins/lookup/random_words.py
@@ -0,0 +1,119 @@
+# -*- coding: utf-8 -*-
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+"""The community.general.random_words Ansible lookup plugin."""
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+DOCUMENTATION = r"""
+    name: random_words
+    author:
+      - Thomas Sjögren (@konstruktoid)
+    short_description: Return a number of random words
+    version_added: "4.0.0"
+    requirements:
+      - xkcdpass U(https://github.com/redacted/XKCD-password-generator)
+    description:
+      - Returns a number of random words. The output can for example be used for
+        passwords.
+      - See U(https://xkcd.com/936/) for background.
+    options:
+      numwords:
+        description:
+          - The number of words.
+        default: 6
+        type: int
+      min_length:
+        description:
+          - Minimum length of words to make password.
+        default: 5
+        type: int
+      max_length:
+        description:
+          - Maximum length of words to make password.
+        default: 9
+        type: int
+      delimiter:
+        description:
+          - The delimiter character between words.
+        default: " "
+        type: str
+      case:
+        description:
+          - The method for setting the case of each word in the passphrase.
+        choices: ["alternating", "upper", "lower", "random", "capitalize"]
+        default: "lower"
+        type: str
+"""
+
+EXAMPLES = r"""
+- name: Generate password with default settings
+  ansible.builtin.debug:
+    var: lookup('community.general.random_words')
+  # Example result: 'traitor gigabyte cesarean unless aspect clear'
+
+- name: Generate password with six, five character, words
+  ansible.builtin.debug:
+    var: lookup('community.general.random_words', min_length=5, max_length=5)
+  # Example result: 'brink banjo getup staff trump comfy'
+
+- name: Generate password with three capitalized words and the '-' delimiter
+  ansible.builtin.debug:
+    var: lookup('community.general.random_words', numwords=3, delimiter='-', case='capitalize')
+  # Example result: 'Overlabor-Faucet-Coastline'
+
+- name: Generate password with three words without any delimiter
+  ansible.builtin.debug:
+    var: lookup('community.general.random_words', numwords=3, delimiter='')
+  # Example result: 'deskworkmonopolystriking'
+  # https://www.ncsc.gov.uk/blog-post/the-logic-behind-three-random-words
+"""
+
+RETURN = r"""
+  _raw:
+    description: A single-element list containing random words.
+    type: list
+    elements: str
+"""
+
+from ansible.errors import AnsibleLookupError
+from ansible.plugins.lookup import LookupBase
+
+try:
+    from xkcdpass import xkcd_password as xp
+
+    HAS_XKCDPASS = True
+except ImportError:
+    HAS_XKCDPASS = False
+
+
+class LookupModule(LookupBase):
+    """The random_words Ansible lookup class."""
+
+    def run(self, terms, variables=None, **kwargs):
+
+        if not HAS_XKCDPASS:
+            raise AnsibleLookupError(
+                "Python xkcdpass library is required. "
+                'Please install using "pip install xkcdpass"'
+            )
+
+        self.set_options(var_options=variables, direct=kwargs)
+        method = self.get_option("case")
+        delimiter = self.get_option("delimiter")
+        max_length = self.get_option("max_length")
+        min_length = self.get_option("min_length")
+        numwords = self.get_option("numwords")
+
+        words = xp.locate_wordfile()
+        wordlist = xp.generate_wordlist(
+            max_length=max_length, min_length=min_length, wordfile=words
+        )
+
+        values = xp.generate_xkcdpassword(
+            wordlist, case=method, delimiter=delimiter, numwords=numwords
+        )
+
+        return [values]
diff --git a/tests/integration/targets/lookup_random_words/aliases b/tests/integration/targets/lookup_random_words/aliases
new file mode 100644
index 0000000000..bc987654d9
--- /dev/null
+++ b/tests/integration/targets/lookup_random_words/aliases
@@ -0,0 +1,3 @@
+shippable/posix/group2
+skip/aix
+skip/python2.6  # lookups are controller only, and we no longer support Python 2.6 on the controller
diff --git a/tests/integration/targets/lookup_random_words/dependencies.yml b/tests/integration/targets/lookup_random_words/dependencies.yml
new file mode 100644
index 0000000000..eef89942d7
--- /dev/null
+++ b/tests/integration/targets/lookup_random_words/dependencies.yml
@@ -0,0 +1,6 @@
+---
+- hosts: localhost
+  tasks:
+    - name: Install xkcdpass Python package
+      pip:
+        name: xkcdpass
diff --git a/tests/integration/targets/lookup_random_words/runme.sh b/tests/integration/targets/lookup_random_words/runme.sh
new file mode 100755
index 0000000000..afdff7bb9d
--- /dev/null
+++ b/tests/integration/targets/lookup_random_words/runme.sh
@@ -0,0 +1,9 @@
+#!/usr/bin/env bash
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+set -eux
+
+ANSIBLE_ROLES_PATH=../ \
+    ansible-playbook dependencies.yml -v "$@"
+
+ANSIBLE_ROLES_PATH=../ \
+    ansible-playbook test.yml -v "$@"
diff --git a/tests/integration/targets/lookup_random_words/test.yml b/tests/integration/targets/lookup_random_words/test.yml
new file mode 100644
index 0000000000..b33c14b605
--- /dev/null
+++ b/tests/integration/targets/lookup_random_words/test.yml
@@ -0,0 +1,28 @@
+---
+- hosts: localhost
+  gather_facts: false
+  tasks:
+  - name: Call random_words plugin
+    set_fact:
+      result1: "{{ query('community.general.random_words') }}"
+      result2: "{{ query('community.general.random_words', min_length=5, max_length=5) }}"
+      result3: "{{ query('community.general.random_words', delimiter='!') }}"
+      result4: "{{ query('community.general.random_words', numwords=3, delimiter='-', case='capitalize') }}"
+      result5: "{{ query('community.general.random_words', numwords=3, delimiter='') }}"
+
+  - name: Check results
+    assert:
+      that:
+        - result1 | length == 1
+        - result1[0] | length >= 35
+        - result2 | length == 1
+        - result2[0] | length == 35
+        - result3 | length == 1
+        - result3[0].count("!") == 5
+        - result4 | length == 1
+        - result4[0] | length >= 17
+        - result4[0] | length <= 29
+        - result4[0] | regex_findall("[A-Z]") | length == 3
+        - result4[0].count("-") == 2
+        - result5 | length == 1
+        - result5[0] | length >= 18