diff --git a/.cloud-init.metadata b/.cloud-init.metadata
index 245563f..6803c51 100644
--- a/.cloud-init.metadata
+++ b/.cloud-init.metadata
@@ -1 +1 @@
-cbde66f717b7883c4ab64b145042de54f131afab SOURCES/cloud-init-20.3.tar.gz
+2ae378aa2ae23b34b0ff123623ba5e2fbdc4928d SOURCES/cloud-init-21.1.tar.gz
diff --git a/.gitignore b/.gitignore
index e8608c9..103bcf7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1 @@
-SOURCES/cloud-init-20.3.tar.gz
+SOURCES/cloud-init-21.1.tar.gz
diff --git a/SOURCES/0001-Add-initial-redhat-setup.patch b/SOURCES/0001-Add-initial-redhat-setup.patch
index 6f85c2d..b67fcae 100644
--- a/SOURCES/0001-Add-initial-redhat-setup.patch
+++ b/SOURCES/0001-Add-initial-redhat-setup.patch
@@ -1,8 +1,18 @@
-From 25ea7a28d69518319ae1ed1b3cd510147868fd29 Mon Sep 17 00:00:00 2001
+From 074cb9b011623849cfa95c1d7cc813bb28f03ff0 Mon Sep 17 00:00:00 2001
 From: Eduardo Otubo <otubo@redhat.com>
-Date: Mon, 5 Oct 2020 13:49:36 +0200
+Date: Fri, 7 May 2021 13:36:03 +0200
 Subject: Add initial redhat setup
 
+Merged patches (21.1):
+- 915d30ad Change gating file to correct rhel version
+- 311f318d Removing net-tools dependency
+- 74731806 Adding man pages to Red Hat spec file
+- 758d333d Removing blocking test from yaml configuration file
+- c7e7c59c Changing permission of cloud-init-generator to 755
+- 8b85abbb Installing man pages in the correct place with correct permissions
+- c6808d8d Fix unit failure of cloud-final.service if NetworkManager was not present.
+- 11866ef6 Report full specific version with "cloud-init --version"
+
 Rebase notes (18.5):
 - added bash_completition file
 - added cloud-id file
@@ -33,36 +43,36 @@ setup.py:
 Signed-off-by: Eduardo Otubo <otubo@redhat.com>
 ---
  .gitignore                            |   1 +
- cloudinit/config/cc_chef.py           |  67 ++++-
+ cloudinit/config/cc_chef.py           |  67 +++-
  cloudinit/settings.py                 |   7 +-
  redhat/.gitignore                     |   1 +
- redhat/Makefile                       |  71 +++++
- redhat/Makefile.common                |  37 +++
+ redhat/Makefile                       |  71 ++++
+ redhat/Makefile.common                |  37 ++
  redhat/cloud-init-tmpfiles.conf       |   1 +
- redhat/cloud-init.spec.template       | 517 ++++++++++++++++++++++++++++++++++
- redhat/gating.yaml                    |   9 +
+ redhat/cloud-init.spec.template       | 530 ++++++++++++++++++++++++++
+ redhat/gating.yaml                    |   8 +
  redhat/rpmbuild/BUILD/.gitignore      |   3 +
  redhat/rpmbuild/RPMS/.gitignore       |   3 +
  redhat/rpmbuild/SOURCES/.gitignore    |   3 +
  redhat/rpmbuild/SPECS/.gitignore      |   3 +
  redhat/rpmbuild/SRPMS/.gitignore      |   3 +
  redhat/scripts/frh.py                 |  27 ++
- redhat/scripts/git-backport-diff      | 327 +++++++++++++++++++++
- redhat/scripts/git-compile-check      | 215 ++++++++++++++
- redhat/scripts/process-patches.sh     |  77 +++++
+ redhat/scripts/git-backport-diff      | 327 ++++++++++++++++
+ redhat/scripts/git-compile-check      | 215 +++++++++++
+ redhat/scripts/process-patches.sh     |  77 ++++
  redhat/scripts/tarball_checksum.sh    |   3 +
  rhel/README.rhel                      |   5 +
  rhel/cloud-init-tmpfiles.conf         |   1 +
- rhel/cloud.cfg                        |  69 +++++
- rhel/systemd/cloud-config.service     |  18 ++
+ rhel/cloud.cfg                        |  69 ++++
+ rhel/systemd/cloud-config.service     |  18 +
  rhel/systemd/cloud-config.target      |  11 +
- rhel/systemd/cloud-final.service      |  19 ++
+ rhel/systemd/cloud-final.service      |  24 ++
  rhel/systemd/cloud-init-local.service |  31 ++
  rhel/systemd/cloud-init.service       |  25 ++
  rhel/systemd/cloud-init.target        |   7 +
  setup.py                              |  23 +-
  tools/read-version                    |  28 +-
- 30 files changed, 1562 insertions(+), 50 deletions(-)
+ 30 files changed, 1579 insertions(+), 50 deletions(-)
  create mode 100644 redhat/.gitignore
  create mode 100644 redhat/Makefile
  create mode 100644 redhat/Makefile.common
@@ -90,7 +100,7 @@ Signed-off-by: Eduardo Otubo <otubo@redhat.com>
  create mode 100644 rhel/systemd/cloud-init.target
 
 diff --git a/cloudinit/config/cc_chef.py b/cloudinit/config/cc_chef.py
-index aaf7136..97ef649 100644
+index aaf71366..97ef649a 100644
 --- a/cloudinit/config/cc_chef.py
 +++ b/cloudinit/config/cc_chef.py
 @@ -6,7 +6,70 @@
@@ -175,10 +185,10 @@ index aaf7136..97ef649 100644
  REQUIRED_CHEF_DIRS = tuple([
      '/etc/chef',
 diff --git a/cloudinit/settings.py b/cloudinit/settings.py
-index ca4ffa8..3a04a58 100644
+index 91e1bfe7..e690c0fd 100644
 --- a/cloudinit/settings.py
 +++ b/cloudinit/settings.py
-@@ -46,13 +46,16 @@ CFG_BUILTIN = {
+@@ -47,13 +47,16 @@ CFG_BUILTIN = {
      ],
      'def_log_file': '/var/log/cloud-init.log',
      'log_cfgs': [],
@@ -199,7 +209,7 @@ index ca4ffa8..3a04a58 100644
      'vendor_data': {'enabled': True, 'prefix': []},
 diff --git a/rhel/README.rhel b/rhel/README.rhel
 new file mode 100644
-index 0000000..aa29630
+index 00000000..aa29630d
 --- /dev/null
 +++ b/rhel/README.rhel
 @@ -0,0 +1,5 @@
@@ -210,14 +220,14 @@ index 0000000..aa29630
 + - grub_dpkg
 diff --git a/rhel/cloud-init-tmpfiles.conf b/rhel/cloud-init-tmpfiles.conf
 new file mode 100644
-index 0000000..0c6d2a3
+index 00000000..0c6d2a3b
 --- /dev/null
 +++ b/rhel/cloud-init-tmpfiles.conf
 @@ -0,0 +1 @@
 +d /run/cloud-init 0700 root root - -
 diff --git a/rhel/cloud.cfg b/rhel/cloud.cfg
 new file mode 100644
-index 0000000..82e8bf6
+index 00000000..82e8bf62
 --- /dev/null
 +++ b/rhel/cloud.cfg
 @@ -0,0 +1,69 @@
@@ -292,7 +302,7 @@ index 0000000..82e8bf6
 +# vim:syntax=yaml
 diff --git a/rhel/systemd/cloud-config.service b/rhel/systemd/cloud-config.service
 new file mode 100644
-index 0000000..f3dcd4b
+index 00000000..f3dcd4be
 --- /dev/null
 +++ b/rhel/systemd/cloud-config.service
 @@ -0,0 +1,18 @@
@@ -316,7 +326,7 @@ index 0000000..f3dcd4b
 +WantedBy=cloud-init.target
 diff --git a/rhel/systemd/cloud-config.target b/rhel/systemd/cloud-config.target
 new file mode 100644
-index 0000000..ae9b7d0
+index 00000000..ae9b7d02
 --- /dev/null
 +++ b/rhel/systemd/cloud-config.target
 @@ -0,0 +1,11 @@
@@ -333,10 +343,10 @@ index 0000000..ae9b7d0
 +After=cloud-init-local.service cloud-init.service
 diff --git a/rhel/systemd/cloud-final.service b/rhel/systemd/cloud-final.service
 new file mode 100644
-index 0000000..739b7e3
+index 00000000..e281c0cf
 --- /dev/null
 +++ b/rhel/systemd/cloud-final.service
-@@ -0,0 +1,19 @@
+@@ -0,0 +1,24 @@
 +[Unit]
 +Description=Execute cloud user/final scripts
 +After=network-online.target cloud-config.service rc-local.service
@@ -350,6 +360,11 @@ index 0000000..739b7e3
 +RemainAfterExit=yes
 +TimeoutSec=0
 +KillMode=process
++# Restart NetworkManager if it is present and running.
++ExecStartPost=/bin/sh -c 'u=NetworkManager.service; \
++ out=$(systemctl show --property=SubState $u) || exit; \
++ [ "$out" = "SubState=running" ] || exit 0; \
++ systemctl reload-or-try-restart $u'
 +
 +# Output needs to appear in instance console output
 +StandardOutput=journal+console
@@ -358,7 +373,7 @@ index 0000000..739b7e3
 +WantedBy=cloud-init.target
 diff --git a/rhel/systemd/cloud-init-local.service b/rhel/systemd/cloud-init-local.service
 new file mode 100644
-index 0000000..8f9f6c9
+index 00000000..8f9f6c9f
 --- /dev/null
 +++ b/rhel/systemd/cloud-init-local.service
 @@ -0,0 +1,31 @@
@@ -395,7 +410,7 @@ index 0000000..8f9f6c9
 +WantedBy=cloud-init.target
 diff --git a/rhel/systemd/cloud-init.service b/rhel/systemd/cloud-init.service
 new file mode 100644
-index 0000000..d0023a0
+index 00000000..d0023a05
 --- /dev/null
 +++ b/rhel/systemd/cloud-init.service
 @@ -0,0 +1,25 @@
@@ -426,7 +441,7 @@ index 0000000..d0023a0
 +WantedBy=cloud-init.target
 diff --git a/rhel/systemd/cloud-init.target b/rhel/systemd/cloud-init.target
 new file mode 100644
-index 0000000..083c3b6
+index 00000000..083c3b6f
 --- /dev/null
 +++ b/rhel/systemd/cloud-init.target
 @@ -0,0 +1,7 @@
@@ -438,7 +453,7 @@ index 0000000..083c3b6
 +Description=Cloud-init target
 +After=multi-user.target
 diff --git a/setup.py b/setup.py
-index cbacf48..d5cd01a 100755
+index cbacf48e..d5cd01a4 100755
 --- a/setup.py
 +++ b/setup.py
 @@ -125,14 +125,6 @@ INITSYS_FILES = {
@@ -503,7 +518,7 @@ index cbacf48..d5cd01a 100755
          'console_scripts': [
              'cloud-init = cloudinit.cmd.main:main',
 diff --git a/tools/read-version b/tools/read-version
-index 02c9064..79755f7 100755
+index 02c90643..79755f78 100755
 --- a/tools/read-version
 +++ b/tools/read-version
 @@ -71,32 +71,8 @@ version_long = None
@@ -542,5 +557,5 @@ index 02c9064..79755f7 100755
  # version is X.Y.Z[+xxx.gHASH]
  # version_long is None or X.Y.Z-xxx-gHASH
 -- 
-1.8.3.1
+2.27.0
 
diff --git a/SOURCES/0002-Do-not-write-NM_CONTROLLED-no-in-generated-interface.patch b/SOURCES/0002-Do-not-write-NM_CONTROLLED-no-in-generated-interface.patch
index ffa06c2..3dc704f 100644
--- a/SOURCES/0002-Do-not-write-NM_CONTROLLED-no-in-generated-interface.patch
+++ b/SOURCES/0002-Do-not-write-NM_CONTROLLED-no-in-generated-interface.patch
@@ -1,6 +1,6 @@
-From d9024cd3bd3bf09b05eb75ba3d81bd15f519c9f8 Mon Sep 17 00:00:00 2001
+From 472c2b5d4342b6ab6ce1584dc39bed0e6c1ca2e7 Mon Sep 17 00:00:00 2001
 From: Eduardo Otubo <otubo@redhat.com>
-Date: Mon, 5 Oct 2020 13:49:46 +0200
+Date: Fri, 7 May 2021 13:36:06 +0200
 Subject: Do not write NM_CONTROLLED=no in generated interface config  files
 
 Conflicts 20.3:
@@ -13,14 +13,14 @@ Signed-off-by: Eduardo Otubo <otubo@redhat.com>
 Signed-off-by: Ryan McCabe <rmccabe@redhat.com>
 ---
  cloudinit/net/sysconfig.py  |  2 +-
- tests/unittests/test_net.py | 30 ------------------------------
- 2 files changed, 1 insertion(+), 31 deletions(-)
+ tests/unittests/test_net.py | 28 ----------------------------
+ 2 files changed, 1 insertion(+), 29 deletions(-)
 
 diff --git a/cloudinit/net/sysconfig.py b/cloudinit/net/sysconfig.py
-index 0a5d481..23e467d 100644
+index 99a4bae4..3d276666 100644
 --- a/cloudinit/net/sysconfig.py
 +++ b/cloudinit/net/sysconfig.py
-@@ -277,7 +277,7 @@ class Renderer(renderer.Renderer):
+@@ -289,7 +289,7 @@ class Renderer(renderer.Renderer):
      #                                         details about this)
  
      iface_defaults = {
@@ -30,7 +30,7 @@ index 0a5d481..23e467d 100644
          'suse': {'BOOTPROTO': 'static', 'STARTMODE': 'auto'},
      }
 diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py
-index 54cc846..9985a97 100644
+index 38d934d4..c67b5fcc 100644
 --- a/tests/unittests/test_net.py
 +++ b/tests/unittests/test_net.py
 @@ -535,7 +535,6 @@ GATEWAY=172.19.3.254
@@ -49,15 +49,15 @@ index 54cc846..9985a97 100644
  ONBOOT=yes
  TYPE=Ethernet
  USERCTL=no
-@@ -754,7 +752,6 @@ IPV6ADDR_SECONDARIES="2001:DB9::10/64 2001:DB10::10/64"
- IPV6INIT=yes
+@@ -756,7 +754,6 @@ IPV6_AUTOCONF=no
  IPV6_DEFAULTGW=2001:DB8::1
+ IPV6_FORCE_ACCEPT_RA=no
  NETMASK=255.255.252.0
 -NM_CONTROLLED=no
  ONBOOT=yes
  TYPE=Ethernet
  USERCTL=no
-@@ -882,7 +879,6 @@ NETWORK_CONFIGS = {
+@@ -884,7 +881,6 @@ NETWORK_CONFIGS = {
                  BOOTPROTO=none
                  DEVICE=eth1
                  HWADDR=cf:d6:af:48:e8:80
@@ -65,7 +65,7 @@ index 54cc846..9985a97 100644
                  ONBOOT=yes
                  TYPE=Ethernet
                  USERCTL=no"""),
-@@ -899,7 +895,6 @@ NETWORK_CONFIGS = {
+@@ -901,7 +897,6 @@ NETWORK_CONFIGS = {
                  IPADDR=192.168.21.3
                  NETMASK=255.255.255.0
                  METRIC=10000
@@ -73,15 +73,15 @@ index 54cc846..9985a97 100644
                  ONBOOT=yes
                  TYPE=Ethernet
                  USERCTL=no"""),
-@@ -1028,7 +1023,6 @@ NETWORK_CONFIGS = {
-                 IPV6ADDR=2001:1::1/64
-                 IPV6INIT=yes
+@@ -1032,7 +1027,6 @@ NETWORK_CONFIGS = {
+                 IPV6_AUTOCONF=no
+                 IPV6_FORCE_ACCEPT_RA=no
                  NETMASK=255.255.255.0
 -                NM_CONTROLLED=no
                  ONBOOT=yes
                  TYPE=Ethernet
                  USERCTL=no
-@@ -1622,7 +1616,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true
+@@ -1737,7 +1731,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true
                  DHCPV6C=yes
                  IPV6INIT=yes
                  MACADDR=aa:bb:cc:dd:ee:ff
@@ -89,15 +89,15 @@ index 54cc846..9985a97 100644
                  ONBOOT=yes
                  TYPE=Bond
                  USERCTL=no"""),
-@@ -1630,7 +1623,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true
+@@ -1745,7 +1738,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true
                  BOOTPROTO=dhcp
                  DEVICE=bond0.200
                  DHCLIENT_SET_DEFAULT_ROUTE=no
 -                NM_CONTROLLED=no
                  ONBOOT=yes
                  PHYSDEV=bond0
-                 TYPE=Ethernet
-@@ -1647,7 +1639,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true
+                 USERCTL=no
+@@ -1763,7 +1755,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true
                  IPV6_DEFAULTGW=2001:4800:78ff:1b::1
                  MACADDR=bb:bb:bb:bb:bb:aa
                  NETMASK=255.255.255.0
@@ -105,7 +105,7 @@ index 54cc846..9985a97 100644
                  ONBOOT=yes
                  PRIO=22
                  STP=no
-@@ -1657,7 +1648,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true
+@@ -1773,7 +1764,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true
                  BOOTPROTO=none
                  DEVICE=eth0
                  HWADDR=c0:d6:9f:2c:e8:80
@@ -113,15 +113,15 @@ index 54cc846..9985a97 100644
                  ONBOOT=yes
                  TYPE=Ethernet
                  USERCTL=no"""),
-@@ -1674,7 +1664,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true
+@@ -1790,7 +1780,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true
                  MTU=1500
                  NETMASK=255.255.255.0
                  NETMASK1=255.255.255.0
 -                NM_CONTROLLED=no
                  ONBOOT=yes
                  PHYSDEV=eth0
-                 TYPE=Ethernet
-@@ -1685,7 +1674,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true
+                 USERCTL=no
+@@ -1800,7 +1789,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true
                  DEVICE=eth1
                  HWADDR=aa:d6:9f:2c:e8:80
                  MASTER=bond0
@@ -129,7 +129,7 @@ index 54cc846..9985a97 100644
                  ONBOOT=yes
                  SLAVE=yes
                  TYPE=Ethernet
-@@ -1695,7 +1683,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true
+@@ -1810,7 +1798,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true
                  DEVICE=eth2
                  HWADDR=c0:bb:9f:2c:e8:80
                  MASTER=bond0
@@ -137,7 +137,7 @@ index 54cc846..9985a97 100644
                  ONBOOT=yes
                  SLAVE=yes
                  TYPE=Ethernet
-@@ -1705,7 +1692,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true
+@@ -1820,7 +1807,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true
                  BRIDGE=br0
                  DEVICE=eth3
                  HWADDR=66:bb:9f:2c:e8:80
@@ -145,7 +145,7 @@ index 54cc846..9985a97 100644
                  ONBOOT=yes
                  TYPE=Ethernet
                  USERCTL=no"""),
-@@ -1714,7 +1700,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true
+@@ -1829,7 +1815,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true
                  BRIDGE=br0
                  DEVICE=eth4
                  HWADDR=98:bb:9f:2c:e8:80
@@ -153,7 +153,7 @@ index 54cc846..9985a97 100644
                  ONBOOT=yes
                  TYPE=Ethernet
                  USERCTL=no"""),
-@@ -1723,7 +1708,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true
+@@ -1838,7 +1823,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true
                  DEVICE=eth5
                  DHCLIENT_SET_DEFAULT_ROUTE=no
                  HWADDR=98:bb:9f:2c:e8:8a
@@ -161,7 +161,7 @@ index 54cc846..9985a97 100644
                  ONBOOT=no
                  TYPE=Ethernet
                  USERCTL=no"""),
-@@ -2177,7 +2161,6 @@ iface bond0 inet6 static
+@@ -2294,7 +2278,6 @@ iface bond0 inet6 static
          MTU=9000
          NETMASK=255.255.255.0
          NETMASK1=255.255.255.0
@@ -169,7 +169,7 @@ index 54cc846..9985a97 100644
          ONBOOT=yes
          TYPE=Bond
          USERCTL=no
-@@ -2187,7 +2170,6 @@ iface bond0 inet6 static
+@@ -2304,7 +2287,6 @@ iface bond0 inet6 static
          DEVICE=bond0s0
          HWADDR=aa:bb:cc:dd:e8:00
          MASTER=bond0
@@ -177,7 +177,7 @@ index 54cc846..9985a97 100644
          ONBOOT=yes
          SLAVE=yes
          TYPE=Ethernet
-@@ -2209,7 +2191,6 @@ iface bond0 inet6 static
+@@ -2326,7 +2308,6 @@ iface bond0 inet6 static
          DEVICE=bond0s1
          HWADDR=aa:bb:cc:dd:e8:01
          MASTER=bond0
@@ -185,7 +185,7 @@ index 54cc846..9985a97 100644
          ONBOOT=yes
          SLAVE=yes
          TYPE=Ethernet
-@@ -2266,7 +2247,6 @@ iface bond0 inet6 static
+@@ -2383,7 +2364,6 @@ iface bond0 inet6 static
                  BOOTPROTO=none
                  DEVICE=en0
                  HWADDR=aa:bb:cc:dd:e8:00
@@ -193,15 +193,15 @@ index 54cc846..9985a97 100644
                  ONBOOT=yes
                  TYPE=Ethernet
                  USERCTL=no"""),
-@@ -2283,7 +2263,6 @@ iface bond0 inet6 static
+@@ -2402,7 +2382,6 @@ iface bond0 inet6 static
                  MTU=2222
                  NETMASK=255.255.255.0
                  NETMASK1=255.255.255.0
 -                NM_CONTROLLED=no
                  ONBOOT=yes
                  PHYSDEV=en0
-                 TYPE=Ethernet
-@@ -2349,7 +2328,6 @@ iface bond0 inet6 static
+                 USERCTL=no
+@@ -2467,7 +2446,6 @@ iface bond0 inet6 static
                  DEVICE=br0
                  IPADDR=192.168.2.2
                  NETMASK=255.255.255.0
@@ -209,23 +209,7 @@ index 54cc846..9985a97 100644
                  ONBOOT=yes
                  PRIO=22
                  STP=no
-@@ -2363,7 +2341,6 @@ iface bond0 inet6 static
-                 HWADDR=52:54:00:12:34:00
-                 IPV6ADDR=2001:1::100/96
-                 IPV6INIT=yes
--                NM_CONTROLLED=no
-                 ONBOOT=yes
-                 TYPE=Ethernet
-                 USERCTL=no
-@@ -2375,7 +2352,6 @@ iface bond0 inet6 static
-                 HWADDR=52:54:00:12:34:01
-                 IPV6ADDR=2001:1::101/96
-                 IPV6INIT=yes
--                NM_CONTROLLED=no
-                 ONBOOT=yes
-                 TYPE=Ethernet
-                 USERCTL=no
-@@ -2469,7 +2445,6 @@ iface bond0 inet6 static
+@@ -2591,7 +2569,6 @@ iface bond0 inet6 static
                  HWADDR=52:54:00:12:34:00
                  IPADDR=192.168.1.2
                  NETMASK=255.255.255.0
@@ -233,7 +217,7 @@ index 54cc846..9985a97 100644
                  ONBOOT=no
                  TYPE=Ethernet
                  USERCTL=no
-@@ -2479,7 +2454,6 @@ iface bond0 inet6 static
+@@ -2601,7 +2578,6 @@ iface bond0 inet6 static
                  DEVICE=eth1
                  HWADDR=52:54:00:12:34:aa
                  MTU=1480
@@ -241,7 +225,7 @@ index 54cc846..9985a97 100644
                  ONBOOT=yes
                  TYPE=Ethernet
                  USERCTL=no
-@@ -2488,7 +2462,6 @@ iface bond0 inet6 static
+@@ -2610,7 +2586,6 @@ iface bond0 inet6 static
                  BOOTPROTO=none
                  DEVICE=eth2
                  HWADDR=52:54:00:12:34:ff
@@ -249,7 +233,7 @@ index 54cc846..9985a97 100644
                  ONBOOT=no
                  TYPE=Ethernet
                  USERCTL=no
-@@ -2905,7 +2878,6 @@ class TestRhelSysConfigRendering(CiTestCase):
+@@ -3027,7 +3002,6 @@ class TestRhelSysConfigRendering(CiTestCase):
  BOOTPROTO=dhcp
  DEVICE=eth1000
  HWADDR=07-1c-c6-75-a4-be
@@ -257,7 +241,7 @@ index 54cc846..9985a97 100644
  ONBOOT=yes
  TYPE=Ethernet
  USERCTL=no
-@@ -3026,7 +2998,6 @@ GATEWAY=10.0.2.2
+@@ -3148,7 +3122,6 @@ GATEWAY=10.0.2.2
  HWADDR=52:54:00:12:34:00
  IPADDR=10.0.2.15
  NETMASK=255.255.255.0
@@ -265,7 +249,7 @@ index 54cc846..9985a97 100644
  ONBOOT=yes
  TYPE=Ethernet
  USERCTL=no
-@@ -3096,7 +3067,6 @@ USERCTL=no
+@@ -3218,7 +3191,6 @@ USERCTL=no
  #
  BOOTPROTO=dhcp
  DEVICE=eth0
@@ -274,5 +258,5 @@ index 54cc846..9985a97 100644
  TYPE=Ethernet
  USERCTL=no
 -- 
-1.8.3.1
+2.27.0
 
diff --git a/SOURCES/0003-limit-permissions-on-def_log_file.patch b/SOURCES/0003-limit-permissions-on-def_log_file.patch
index 7ec19f6..941adaf 100644
--- a/SOURCES/0003-limit-permissions-on-def_log_file.patch
+++ b/SOURCES/0003-limit-permissions-on-def_log_file.patch
@@ -1,6 +1,6 @@
-From de22eafc9046b8ea6fddda7440df5a05f5a40607 Mon Sep 17 00:00:00 2001
+From 6134624f10ef56534e37624adc12f11b09910591 Mon Sep 17 00:00:00 2001
 From: Eduardo Otubo <otubo@redhat.com>
-Date: Mon, 5 Oct 2020 13:49:53 +0200
+Date: Fri, 7 May 2021 13:36:08 +0200
 Subject: limit permissions on def_log_file
 
 This sets a default mode of 0600 on def_log_file, and makes this
@@ -10,18 +10,22 @@ LP: #1541196
 Resolves: rhbz#1424612
 X-approved-upstream: true
 
+Conflicts 21.1:
+    cloudinit/stages.py: adjusting call of ensure_file() to use more
+recent version
+
 Signed-off-by: Eduardo Otubo <otubo@redhat.com>
 ---
  cloudinit/settings.py         | 1 +
- cloudinit/stages.py           | 3 ++-
+ cloudinit/stages.py           | 1 +
  doc/examples/cloud-config.txt | 4 ++++
- 3 files changed, 7 insertions(+), 1 deletion(-)
+ 3 files changed, 6 insertions(+)
 
 diff --git a/cloudinit/settings.py b/cloudinit/settings.py
-index 3a04a58..439eee0 100644
+index e690c0fd..43a1490c 100644
 --- a/cloudinit/settings.py
 +++ b/cloudinit/settings.py
-@@ -45,6 +45,7 @@ CFG_BUILTIN = {
+@@ -46,6 +46,7 @@ CFG_BUILTIN = {
          'None',
      ],
      'def_log_file': '/var/log/cloud-init.log',
@@ -30,22 +34,19 @@ index 3a04a58..439eee0 100644
      'mount_default_fields': [None, None, 'auto', 'defaults,nofail', '0', '2'],
      'ssh_deletekeys': False,
 diff --git a/cloudinit/stages.py b/cloudinit/stages.py
-index 765f4aa..d769375 100644
+index 3ef4491c..83e25dd1 100644
 --- a/cloudinit/stages.py
 +++ b/cloudinit/stages.py
-@@ -147,8 +147,9 @@ class Init(object):
+@@ -147,6 +147,7 @@ class Init(object):
      def _initialize_filesystem(self):
          util.ensure_dirs(self._initial_subdirs())
          log_file = util.get_cfg_option_str(self.cfg, 'def_log_file')
 +        log_file_mode = util.get_cfg_option_int(self.cfg, 'def_log_file_mode')
          if log_file:
--            util.ensure_file(log_file)
-+            util.ensure_file(log_file, mode=log_file_mode)
+             util.ensure_file(log_file, preserve_mode=True)
              perms = self.cfg.get('syslog_fix_perms')
-             if not perms:
-                 perms = {}
 diff --git a/doc/examples/cloud-config.txt b/doc/examples/cloud-config.txt
-index f3ae5e6..b5b1fdd 100644
+index de9a0f87..bb33ad45 100644
 --- a/doc/examples/cloud-config.txt
 +++ b/doc/examples/cloud-config.txt
 @@ -414,10 +414,14 @@ timezone: US/Eastern
@@ -64,5 +65,5 @@ index f3ae5e6..b5b1fdd 100644
  
  # you can set passwords for a user or multiple users
 -- 
-1.8.3.1
+2.27.0
 
diff --git a/SOURCES/0004-sysconfig-Don-t-write-BOOTPROTO-dhcp-for-ipv6-dhcp.patch b/SOURCES/0004-sysconfig-Don-t-write-BOOTPROTO-dhcp-for-ipv6-dhcp.patch
index ad8c142..4d5a0d2 100644
--- a/SOURCES/0004-sysconfig-Don-t-write-BOOTPROTO-dhcp-for-ipv6-dhcp.patch
+++ b/SOURCES/0004-sysconfig-Don-t-write-BOOTPROTO-dhcp-for-ipv6-dhcp.patch
@@ -1,6 +1,6 @@
-From bb87d9a83ddbc5bf84fbdab9c58dedc0c9629eea Mon Sep 17 00:00:00 2001
+From 699d37a6ff3e343e214943794aac09e4156c2b2b Mon Sep 17 00:00:00 2001
 From: Eduardo Otubo <otubo@redhat.com>
-Date: Mon, 5 Oct 2020 13:51:34 +0200
+Date: Fri, 7 May 2021 13:36:10 +0200
 Subject: sysconfig: Don't write BOOTPROTO=dhcp for ipv6 dhcp
 
 Don't write BOOTPROTO=dhcp for ipv6 dhcp, as BOOTPROTO applies
@@ -20,10 +20,10 @@ Signed-off-by: Eduardo Otubo <otubo@redhat.com>
  1 file changed, 1 insertion(+)
 
 diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py
-index 9985a97..2cc57fe 100644
+index c67b5fcc..4ea0e597 100644
 --- a/tests/unittests/test_net.py
 +++ b/tests/unittests/test_net.py
-@@ -1614,6 +1614,7 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true
+@@ -1729,6 +1729,7 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true
                  BOOTPROTO=none
                  DEVICE=bond0
                  DHCPV6C=yes
@@ -32,5 +32,5 @@ index 9985a97..2cc57fe 100644
                  MACADDR=aa:bb:cc:dd:ee:ff
                  ONBOOT=yes
 -- 
-1.8.3.1
+2.27.0
 
diff --git a/SOURCES/0005-DataSourceAzure.py-use-hostnamectl-to-set-hostname.patch b/SOURCES/0005-DataSourceAzure.py-use-hostnamectl-to-set-hostname.patch
index 08474eb..100d3a2 100644
--- a/SOURCES/0005-DataSourceAzure.py-use-hostnamectl-to-set-hostname.patch
+++ b/SOURCES/0005-DataSourceAzure.py-use-hostnamectl-to-set-hostname.patch
@@ -1,6 +1,6 @@
-From 9c6562c6d3516df8d11aa7cf7cd9cc62e5c91a70 Mon Sep 17 00:00:00 2001
+From ccc75c1be3ae08d813193071c798fc905b5c03e5 Mon Sep 17 00:00:00 2001
 From: Eduardo Otubo <otubo@redhat.com>
-Date: Mon, 5 Oct 2020 13:51:37 +0200
+Date: Fri, 7 May 2021 13:36:12 +0200
 Subject: DataSourceAzure.py: use hostnamectl to set hostname
 
 RH-Author: Vitaly Kuznetsov <vkuznets@redhat.com>
@@ -40,10 +40,10 @@ Signed-off-by: Miroslav Rezanina <mrezanin@redhat.com>
  1 file changed, 1 insertion(+), 1 deletion(-)
 
 diff --git a/cloudinit/sources/DataSourceAzure.py b/cloudinit/sources/DataSourceAzure.py
-index f3c6452..1c214db 100755
+index cee630f7..553b5a7e 100755
 --- a/cloudinit/sources/DataSourceAzure.py
 +++ b/cloudinit/sources/DataSourceAzure.py
-@@ -258,7 +258,7 @@ def get_hostname(hostname_command='hostname'):
+@@ -296,7 +296,7 @@ def get_hostname(hostname_command='hostname'):
  
  
  def set_hostname(hostname, hostname_command='hostname'):
@@ -53,5 +53,5 @@ index f3c6452..1c214db 100755
  
  @azure_ds_telemetry_reporter
 -- 
-1.8.3.1
+2.27.0
 
diff --git a/SOURCES/0006-include-NOZEROCONF-yes-in-etc-sysconfig-network.patch b/SOURCES/0006-include-NOZEROCONF-yes-in-etc-sysconfig-network.patch
index 02058ba..6276255 100644
--- a/SOURCES/0006-include-NOZEROCONF-yes-in-etc-sysconfig-network.patch
+++ b/SOURCES/0006-include-NOZEROCONF-yes-in-etc-sysconfig-network.patch
@@ -1,6 +1,6 @@
-From bdcad981ac530277529d1c77fb5e9e6f89409bd8 Mon Sep 17 00:00:00 2001
+From dfea0490b899804761fbd7aa23822783d7c36ec5 Mon Sep 17 00:00:00 2001
 From: Eduardo Otubo <otubo@redhat.com>
-Date: Mon, 5 Oct 2020 13:51:44 +0200
+Date: Fri, 7 May 2021 13:36:13 +0200
 Subject: include 'NOZEROCONF=yes' in /etc/sysconfig/network
 
 RH-Author: Eduardo Otubo <otubo@redhat.com>
@@ -27,10 +27,10 @@ Signed-off-by: Miroslav Rezanina <mrezanin@redhat.com>
  2 files changed, 10 insertions(+), 2 deletions(-)
 
 diff --git a/cloudinit/net/sysconfig.py b/cloudinit/net/sysconfig.py
-index 23e467d..af093dd 100644
+index 3d276666..d5440998 100644
 --- a/cloudinit/net/sysconfig.py
 +++ b/cloudinit/net/sysconfig.py
-@@ -888,7 +888,16 @@ class Renderer(renderer.Renderer):
+@@ -925,7 +925,16 @@ class Renderer(renderer.Renderer):
          # Distros configuring /etc/sysconfig/network as a file e.g. Centos
          if sysconfig_path.endswith('network'):
              util.ensure_dir(os.path.dirname(sysconfig_path))
@@ -49,10 +49,10 @@ index 23e467d..af093dd 100644
                  netcfg.append('NETWORKING_IPV6=yes')
                  netcfg.append('IPV6_AUTOCONF=no')
 diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py
-index 2cc57fe..9985a97 100644
+index 4ea0e597..c67b5fcc 100644
 --- a/tests/unittests/test_net.py
 +++ b/tests/unittests/test_net.py
-@@ -1614,7 +1614,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true
+@@ -1729,7 +1729,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true
                  BOOTPROTO=none
                  DEVICE=bond0
                  DHCPV6C=yes
@@ -61,5 +61,5 @@ index 2cc57fe..9985a97 100644
                  MACADDR=aa:bb:cc:dd:ee:ff
                  ONBOOT=yes
 -- 
-1.8.3.1
+2.27.0
 
diff --git a/SOURCES/0007-Remove-race-condition-between-cloud-init-and-Network.patch b/SOURCES/0007-Remove-race-condition-between-cloud-init-and-Network.patch
index 816a799..9c9e4cc 100644
--- a/SOURCES/0007-Remove-race-condition-between-cloud-init-and-Network.patch
+++ b/SOURCES/0007-Remove-race-condition-between-cloud-init-and-Network.patch
@@ -1,6 +1,6 @@
-From a52c7b659c6569c78aad4b92303f289009da476c Mon Sep 17 00:00:00 2001
+From 24894dcf45a307f44e29dc5d5b2d864b75fd982c Mon Sep 17 00:00:00 2001
 From: Eduardo Otubo <otubo@redhat.com>
-Date: Mon, 5 Oct 2020 13:51:50 +0200
+Date: Fri, 7 May 2021 13:36:14 +0200
 Subject: Remove race condition between cloud-init and NetworkManager
 
 Message-id: <20200302104635.11648-1-otubo@redhat.com>
@@ -114,13 +114,12 @@ Date:   Thu May 28 08:44:06 2020 +0200
     Signed-off-by: Eduardo Otubo <otubo@redhat.com>
     Signed-off-by: Miroslav Rezanina <mrezanin@redhat.com>
 ---
- rhel/cloud.cfg                   | 2 +-
- rhel/systemd/cloud-final.service | 2 ++
- rhel/systemd/cloud-init.service  | 1 +
- 3 files changed, 4 insertions(+), 1 deletion(-)
+ rhel/cloud.cfg                  | 2 +-
+ rhel/systemd/cloud-init.service | 1 +
+ 2 files changed, 2 insertions(+), 1 deletion(-)
 
 diff --git a/rhel/cloud.cfg b/rhel/cloud.cfg
-index 82e8bf6..9ecba21 100644
+index 82e8bf62..9ecba215 100644
 --- a/rhel/cloud.cfg
 +++ b/rhel/cloud.cfg
 @@ -6,7 +6,7 @@ ssh_pwauth:   0
@@ -132,21 +131,8 @@ index 82e8bf6..9ecba21 100644
  ssh_genkeytypes:  ~
  syslog_fix_perms: ~
  disable_vmware_customization: false
-diff --git a/rhel/systemd/cloud-final.service b/rhel/systemd/cloud-final.service
-index 739b7e3..05add07 100644
---- a/rhel/systemd/cloud-final.service
-+++ b/rhel/systemd/cloud-final.service
-@@ -11,6 +11,8 @@ ExecStart=/usr/bin/cloud-init modules --mode=final
- RemainAfterExit=yes
- TimeoutSec=0
- KillMode=process
-+ExecStartPost=/bin/echo "trying to reload or restart NetworkManager.service"
-+ExecStartPost=/usr/bin/systemctl try-reload-or-restart NetworkManager.service
- 
- # Output needs to appear in instance console output
- StandardOutput=journal+console
 diff --git a/rhel/systemd/cloud-init.service b/rhel/systemd/cloud-init.service
-index d0023a0..0b3d796 100644
+index d0023a05..0b3d796d 100644
 --- a/rhel/systemd/cloud-init.service
 +++ b/rhel/systemd/cloud-init.service
 @@ -5,6 +5,7 @@ Wants=sshd-keygen.service
@@ -158,5 +144,5 @@ index d0023a0..0b3d796 100644
  Before=sshd-keygen.service
  Before=sshd.service
 -- 
-1.8.3.1
+2.27.0
 
diff --git a/SOURCES/0008-net-exclude-OVS-internal-interfaces-in-get_interface.patch b/SOURCES/0008-net-exclude-OVS-internal-interfaces-in-get_interface.patch
new file mode 100644
index 0000000..38f08cc
--- /dev/null
+++ b/SOURCES/0008-net-exclude-OVS-internal-interfaces-in-get_interface.patch
@@ -0,0 +1,496 @@
+From b48dda73da94782d7ab0c455fa382d3a5ef3c419 Mon Sep 17 00:00:00 2001
+From: Daniel Watkins <oddbloke@ubuntu.com>
+Date: Mon, 8 Mar 2021 12:50:57 -0500
+Subject: net: exclude OVS internal interfaces in get_interfaces (#829)
+
+`get_interfaces` is used to in two ways, broadly: firstly, to determine
+the available interfaces when converting cloud network configuration
+formats to cloud-init's network configuration formats; and, secondly, to
+ensure that any interfaces which are specified in network configuration
+are (a) available, and (b) named correctly.  The first of these is
+unaffected by this commit, as no clouds support Open vSwitch
+configuration in their network configuration formats.
+
+For the second, we check that MAC addresses of physical devices are
+unique.  In some OVS configurations, there are OVS-created devices which
+have duplicate MAC addresses, either with each other or with physical
+devices.  As these interfaces are created by OVS, we can be confident
+that (a) they will be available when appropriate, and (b) that OVS will
+name them correctly.  As such, this commit excludes any OVS-internal
+interfaces from the set of interfaces returned by `get_interfaces`.
+
+LP: #1912844
+---
+ cloudinit/net/__init__.py                     |  62 +++++++++
+ cloudinit/net/tests/test_init.py              | 119 ++++++++++++++++++
+ .../sources/helpers/tests/test_openstack.py   |   5 +
+ cloudinit/sources/tests/test_oracle.py        |   4 +
+ .../integration_tests/bugs/test_lp1912844.py  | 103 +++++++++++++++
+ .../test_datasource/test_configdrive.py       |   8 ++
+ tests/unittests/test_net.py                   |  20 +++
+ 7 files changed, 321 insertions(+)
+ create mode 100644 tests/integration_tests/bugs/test_lp1912844.py
+
+diff --git a/cloudinit/net/__init__.py b/cloudinit/net/__init__.py
+index de65e7af..385b7bcc 100644
+--- a/cloudinit/net/__init__.py
++++ b/cloudinit/net/__init__.py
+@@ -6,6 +6,7 @@
+ # This file is part of cloud-init. See LICENSE file for license information.
+ 
+ import errno
++import functools
+ import ipaddress
+ import logging
+ import os
+@@ -19,6 +20,19 @@ from cloudinit.url_helper import UrlError, readurl
+ LOG = logging.getLogger(__name__)
+ SYS_CLASS_NET = "/sys/class/net/"
+ DEFAULT_PRIMARY_INTERFACE = 'eth0'
++OVS_INTERNAL_INTERFACE_LOOKUP_CMD = [
++    "ovs-vsctl",
++    "--format",
++    "csv",
++    "--no-headings",
++    "--timeout",
++    "10",
++    "--columns",
++    "name",
++    "find",
++    "interface",
++    "type=internal",
++]
+ 
+ 
+ def natural_sort_key(s, _nsre=re.compile('([0-9]+)')):
+@@ -133,6 +147,52 @@ def master_is_openvswitch(devname):
+     return os.path.exists(ovs_path)
+ 
+ 
++@functools.lru_cache(maxsize=None)
++def openvswitch_is_installed() -> bool:
++    """Return a bool indicating if Open vSwitch is installed in the system."""
++    ret = bool(subp.which("ovs-vsctl"))
++    if not ret:
++        LOG.debug(
++            "ovs-vsctl not in PATH; not detecting Open vSwitch interfaces"
++        )
++    return ret
++
++
++@functools.lru_cache(maxsize=None)
++def get_ovs_internal_interfaces() -> list:
++    """Return a list of the names of OVS internal interfaces on the system.
++
++    These will all be strings, and are used to exclude OVS-specific interface
++    from cloud-init's network configuration handling.
++    """
++    try:
++        out, _err = subp.subp(OVS_INTERNAL_INTERFACE_LOOKUP_CMD)
++    except subp.ProcessExecutionError as exc:
++        if "database connection failed" in exc.stderr:
++            LOG.info(
++                "Open vSwitch is not yet up; no interfaces will be detected as"
++                " OVS-internal"
++            )
++            return []
++        raise
++    else:
++        return out.splitlines()
++
++
++def is_openvswitch_internal_interface(devname: str) -> bool:
++    """Returns True if this is an OVS internal interface.
++
++    If OVS is not installed or not yet running, this will return False.
++    """
++    if not openvswitch_is_installed():
++        return False
++    ovs_bridges = get_ovs_internal_interfaces()
++    if devname in ovs_bridges:
++        LOG.debug("Detected %s as an OVS interface", devname)
++        return True
++    return False
++
++
+ def is_netfailover(devname, driver=None):
+     """ netfailover driver uses 3 nics, master, primary and standby.
+         this returns True if the device is either the primary or standby
+@@ -884,6 +944,8 @@ def get_interfaces(blacklist_drivers=None) -> list:
+         # skip nics that have no mac (00:00....)
+         if name != 'lo' and mac == zero_mac[:len(mac)]:
+             continue
++        if is_openvswitch_internal_interface(name):
++            continue
+         # skip nics that have drivers blacklisted
+         driver = device_driver(name)
+         if driver in blacklist_drivers:
+diff --git a/cloudinit/net/tests/test_init.py b/cloudinit/net/tests/test_init.py
+index 0535387a..946f8ee2 100644
+--- a/cloudinit/net/tests/test_init.py
++++ b/cloudinit/net/tests/test_init.py
+@@ -391,6 +391,10 @@ class TestGetDeviceList(CiTestCase):
+         self.assertCountEqual(['eth0', 'eth1'], net.get_devicelist())
+ 
+ 
++@mock.patch(
++    "cloudinit.net.is_openvswitch_internal_interface",
++    mock.Mock(return_value=False),
++)
+ class TestGetInterfaceMAC(CiTestCase):
+ 
+     def setUp(self):
+@@ -1224,6 +1228,121 @@ class TestNetFailOver(CiTestCase):
+         self.assertFalse(net.is_netfailover(devname, driver))
+ 
+ 
++class TestOpenvswitchIsInstalled:
++    """Test cloudinit.net.openvswitch_is_installed.
++
++    Uses the ``clear_lru_cache`` local autouse fixture to allow us to test
++    despite the ``lru_cache`` decorator on the unit under test.
++    """
++
++    @pytest.fixture(autouse=True)
++    def clear_lru_cache(self):
++        net.openvswitch_is_installed.cache_clear()
++
++    @pytest.mark.parametrize(
++        "expected,which_return", [(True, "/some/path"), (False, None)]
++    )
++    @mock.patch("cloudinit.net.subp.which")
++    def test_mirrors_which_result(self, m_which, expected, which_return):
++        m_which.return_value = which_return
++        assert expected == net.openvswitch_is_installed()
++
++    @mock.patch("cloudinit.net.subp.which")
++    def test_only_calls_which_once(self, m_which):
++        net.openvswitch_is_installed()
++        net.openvswitch_is_installed()
++        assert 1 == m_which.call_count
++
++
++@mock.patch("cloudinit.net.subp.subp", return_value=("", ""))
++class TestGetOVSInternalInterfaces:
++    """Test cloudinit.net.get_ovs_internal_interfaces.
++
++    Uses the ``clear_lru_cache`` local autouse fixture to allow us to test
++    despite the ``lru_cache`` decorator on the unit under test.
++    """
++    @pytest.fixture(autouse=True)
++    def clear_lru_cache(self):
++        net.get_ovs_internal_interfaces.cache_clear()
++
++    def test_command_used(self, m_subp):
++        """Test we use the correct command when we call subp"""
++        net.get_ovs_internal_interfaces()
++
++        assert [
++            mock.call(net.OVS_INTERNAL_INTERFACE_LOOKUP_CMD)
++        ] == m_subp.call_args_list
++
++    def test_subp_contents_split_and_returned(self, m_subp):
++        """Test that the command output is appropriately mangled."""
++        stdout = "iface1\niface2\niface3\n"
++        m_subp.return_value = (stdout, "")
++
++        assert [
++            "iface1",
++            "iface2",
++            "iface3",
++        ] == net.get_ovs_internal_interfaces()
++
++    def test_database_connection_error_handled_gracefully(self, m_subp):
++        """Test that the error indicating OVS is down is handled gracefully."""
++        m_subp.side_effect = ProcessExecutionError(
++            stderr="database connection failed"
++        )
++
++        assert [] == net.get_ovs_internal_interfaces()
++
++    def test_other_errors_raised(self, m_subp):
++        """Test that only database connection errors are handled."""
++        m_subp.side_effect = ProcessExecutionError()
++
++        with pytest.raises(ProcessExecutionError):
++            net.get_ovs_internal_interfaces()
++
++    def test_only_runs_once(self, m_subp):
++        """Test that we cache the value."""
++        net.get_ovs_internal_interfaces()
++        net.get_ovs_internal_interfaces()
++
++        assert 1 == m_subp.call_count
++
++
++@mock.patch("cloudinit.net.get_ovs_internal_interfaces")
++@mock.patch("cloudinit.net.openvswitch_is_installed")
++class TestIsOpenVSwitchInternalInterface:
++    def test_false_if_ovs_not_installed(
++        self, m_openvswitch_is_installed, _m_get_ovs_internal_interfaces
++    ):
++        """Test that OVS' absence returns False."""
++        m_openvswitch_is_installed.return_value = False
++
++        assert not net.is_openvswitch_internal_interface("devname")
++
++    @pytest.mark.parametrize(
++        "detected_interfaces,devname,expected_return",
++        [
++            ([], "devname", False),
++            (["notdevname"], "devname", False),
++            (["devname"], "devname", True),
++            (["some", "other", "devices", "and", "ours"], "ours", True),
++        ],
++    )
++    def test_return_value_based_on_detected_interfaces(
++        self,
++        m_openvswitch_is_installed,
++        m_get_ovs_internal_interfaces,
++        detected_interfaces,
++        devname,
++        expected_return,
++    ):
++        """Test that the detected interfaces are used correctly."""
++        m_openvswitch_is_installed.return_value = True
++        m_get_ovs_internal_interfaces.return_value = detected_interfaces
++        assert expected_return == net.is_openvswitch_internal_interface(
++            devname
++        )
++
++
+ class TestIsIpAddress:
+     """Tests for net.is_ip_address.
+ 
+diff --git a/cloudinit/sources/helpers/tests/test_openstack.py b/cloudinit/sources/helpers/tests/test_openstack.py
+index 2bde1e3f..95fb9743 100644
+--- a/cloudinit/sources/helpers/tests/test_openstack.py
++++ b/cloudinit/sources/helpers/tests/test_openstack.py
+@@ -1,10 +1,15 @@
+ # This file is part of cloud-init. See LICENSE file for license information.
+ # ./cloudinit/sources/helpers/tests/test_openstack.py
++from unittest import mock
+ 
+ from cloudinit.sources.helpers import openstack
+ from cloudinit.tests import helpers as test_helpers
+ 
+ 
++@mock.patch(
++    "cloudinit.net.is_openvswitch_internal_interface",
++    mock.Mock(return_value=False)
++)
+ class TestConvertNetJson(test_helpers.CiTestCase):
+ 
+     def test_phy_types(self):
+diff --git a/cloudinit/sources/tests/test_oracle.py b/cloudinit/sources/tests/test_oracle.py
+index a7bbdfd9..dcf33b9b 100644
+--- a/cloudinit/sources/tests/test_oracle.py
++++ b/cloudinit/sources/tests/test_oracle.py
+@@ -173,6 +173,10 @@ class TestIsPlatformViable(test_helpers.CiTestCase):
+         m_read_dmi_data.assert_has_calls([mock.call('chassis-asset-tag')])
+ 
+ 
++@mock.patch(
++    "cloudinit.net.is_openvswitch_internal_interface",
++    mock.Mock(return_value=False)
++)
+ class TestNetworkConfigFromOpcImds:
+     def test_no_secondary_nics_does_not_mutate_input(self, oracle_ds):
+         oracle_ds._vnics_data = [{}]
+diff --git a/tests/integration_tests/bugs/test_lp1912844.py b/tests/integration_tests/bugs/test_lp1912844.py
+new file mode 100644
+index 00000000..efafae50
+--- /dev/null
++++ b/tests/integration_tests/bugs/test_lp1912844.py
+@@ -0,0 +1,103 @@
++"""Integration test for LP: #1912844
++
++cloud-init should ignore OVS-internal interfaces when performing its own
++interface determination: these interfaces are handled fully by OVS, so
++cloud-init should never need to touch them.
++
++This test is a semi-synthetic reproducer for the bug.  It uses a similar
++network configuration, tweaked slightly to DHCP in a way that will succeed even
++on "failed" boots.  The exact bug doesn't reproduce with the NoCloud
++datasource, because it runs at init-local time (whereas the MAAS datasource,
++from the report, runs only at init (network) time): this means that the
++networking code runs before OVS creates its interfaces (which happens after
++init-local but, of course, before networking is up), and so doesn't generate
++the traceback that they cause.  We work around this by calling
++``get_interfaces_by_mac` directly in the test code.
++"""
++import pytest
++
++from tests.integration_tests import random_mac_address
++
++MAC_ADDRESS = random_mac_address()
++
++NETWORK_CONFIG = """\
++bonds:
++    bond0:
++        interfaces:
++            - enp5s0
++        macaddress: {0}
++        mtu: 1500
++bridges:
++        ovs-br:
++            interfaces:
++            - bond0
++            macaddress: {0}
++            mtu: 1500
++            openvswitch: {{}}
++            dhcp4: true
++ethernets:
++    enp5s0:
++      mtu: 1500
++      set-name: enp5s0
++      match:
++          macaddress: {0}
++version: 2
++vlans:
++  ovs-br.100:
++    id: 100
++    link: ovs-br
++    mtu: 1500
++  ovs-br.200:
++    id: 200
++    link: ovs-br
++    mtu: 1500
++""".format(MAC_ADDRESS)
++
++
++SETUP_USER_DATA = """\
++#cloud-config
++packages:
++- openvswitch-switch
++"""
++
++
++@pytest.fixture
++def ovs_enabled_session_cloud(session_cloud):
++    """A session_cloud wrapper, to use an OVS-enabled image for tests.
++
++    This implementation is complicated by wanting to use ``session_cloud``s
++    snapshot cleanup/retention logic, to avoid having to reimplement that here.
++    """
++    old_snapshot_id = session_cloud.snapshot_id
++    with session_cloud.launch(
++        user_data=SETUP_USER_DATA,
++    ) as instance:
++        instance.instance.clean()
++        session_cloud.snapshot_id = instance.snapshot()
++
++    yield session_cloud
++
++    try:
++        session_cloud.delete_snapshot()
++    finally:
++        session_cloud.snapshot_id = old_snapshot_id
++
++
++@pytest.mark.lxd_vm
++def test_get_interfaces_by_mac_doesnt_traceback(ovs_enabled_session_cloud):
++    """Launch our OVS-enabled image and confirm the bug doesn't reproduce."""
++    launch_kwargs = {
++        "config_dict": {
++            "user.network-config": NETWORK_CONFIG,
++            "volatile.eth0.hwaddr": MAC_ADDRESS,
++        },
++    }
++    with ovs_enabled_session_cloud.launch(
++        launch_kwargs=launch_kwargs,
++    ) as client:
++        result = client.execute(
++            "python3 -c"
++            "'from cloudinit.net import get_interfaces_by_mac;"
++            "get_interfaces_by_mac()'"
++        )
++        assert result.ok
+diff --git a/tests/unittests/test_datasource/test_configdrive.py b/tests/unittests/test_datasource/test_configdrive.py
+index 6f830cc6..2e2b7847 100644
+--- a/tests/unittests/test_datasource/test_configdrive.py
++++ b/tests/unittests/test_datasource/test_configdrive.py
+@@ -494,6 +494,10 @@ class TestConfigDriveDataSource(CiTestCase):
+         self.assertEqual('config-disk (/dev/anything)', cfg_ds.subplatform)
+ 
+ 
++@mock.patch(
++    "cloudinit.net.is_openvswitch_internal_interface",
++    mock.Mock(return_value=False)
++)
+ class TestNetJson(CiTestCase):
+     def setUp(self):
+         super(TestNetJson, self).setUp()
+@@ -654,6 +658,10 @@ class TestNetJson(CiTestCase):
+             self.assertEqual(out_data, conv_data)
+ 
+ 
++@mock.patch(
++    "cloudinit.net.is_openvswitch_internal_interface",
++    mock.Mock(return_value=False)
++)
+ class TestConvertNetworkData(CiTestCase):
+ 
+     with_logs = True
+diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py
+index c67b5fcc..14d3462f 100644
+--- a/tests/unittests/test_net.py
++++ b/tests/unittests/test_net.py
+@@ -2908,6 +2908,10 @@ iface eth1 inet dhcp
+         self.assertEqual(0, mock_settle.call_count)
+ 
+ 
++@mock.patch(
++    "cloudinit.net.is_openvswitch_internal_interface",
++    mock.Mock(return_value=False)
++)
+ class TestRhelSysConfigRendering(CiTestCase):
+ 
+     with_logs = True
+@@ -3592,6 +3596,10 @@ USERCTL=no
+                 expected, self._render_and_read(network_config=v2data))
+ 
+ 
++@mock.patch(
++    "cloudinit.net.is_openvswitch_internal_interface",
++    mock.Mock(return_value=False)
++)
+ class TestOpenSuseSysConfigRendering(CiTestCase):
+ 
+     with_logs = True
+@@ -5009,6 +5017,10 @@ class TestNetRenderers(CiTestCase):
+             self.assertTrue(result)
+ 
+ 
++@mock.patch(
++    "cloudinit.net.is_openvswitch_internal_interface",
++    mock.Mock(return_value=False)
++)
+ class TestGetInterfaces(CiTestCase):
+     _data = {'bonds': ['bond1'],
+              'bridges': ['bridge1'],
+@@ -5158,6 +5170,10 @@ class TestInterfaceHasOwnMac(CiTestCase):
+         self.assertFalse(interface_has_own_mac("eth0"))
+ 
+ 
++@mock.patch(
++    "cloudinit.net.is_openvswitch_internal_interface",
++    mock.Mock(return_value=False)
++)
+ class TestGetInterfacesByMac(CiTestCase):
+     _data = {'bonds': ['bond1'],
+              'bridges': ['bridge1'],
+@@ -5314,6 +5330,10 @@ class TestInterfacesSorting(CiTestCase):
+             ['enp0s3', 'enp0s8', 'enp0s13', 'enp1s2', 'enp2s0', 'enp2s3'])
+ 
+ 
++@mock.patch(
++    "cloudinit.net.is_openvswitch_internal_interface",
++    mock.Mock(return_value=False)
++)
+ class TestGetIBHwaddrsByInterface(CiTestCase):
+ 
+     _ib_addr = '80:00:00:28:fe:80:00:00:00:00:00:00:00:11:22:03:00:33:44:56'
+-- 
+2.27.0
+
diff --git a/SOURCES/0009-Fix-requiring-device-number-on-EC2-derivatives-836.patch b/SOURCES/0009-Fix-requiring-device-number-on-EC2-derivatives-836.patch
new file mode 100644
index 0000000..0d474bc
--- /dev/null
+++ b/SOURCES/0009-Fix-requiring-device-number-on-EC2-derivatives-836.patch
@@ -0,0 +1,87 @@
+From bec5fb60ffae3d1137c7261e5571c2751c5dda25 Mon Sep 17 00:00:00 2001
+From: James Falcon <TheRealFalcon@users.noreply.github.com>
+Date: Mon, 8 Mar 2021 14:09:47 -0600
+Subject: Fix requiring device-number on EC2 derivatives (#836)
+
+#342 (70dbccbb) introduced the ability to determine route-metrics based on
+the `device-number` provided by the EC2 IMDS. Not all datasources that
+subclass EC2 will have this attribute, so allow the old behavior if
+`device-number` is not present.
+
+LP: #1917875
+---
+ cloudinit/sources/DataSourceEc2.py            |  3 +-
+ .../unittests/test_datasource/test_aliyun.py  | 30 +++++++++++++++++++
+ 2 files changed, 32 insertions(+), 1 deletion(-)
+
+diff --git a/cloudinit/sources/DataSourceEc2.py b/cloudinit/sources/DataSourceEc2.py
+index 1930a509..a2105dc7 100644
+--- a/cloudinit/sources/DataSourceEc2.py
++++ b/cloudinit/sources/DataSourceEc2.py
+@@ -765,13 +765,14 @@ def convert_ec2_metadata_network_config(
+         netcfg['ethernets'][nic_name] = dev_config
+         return netcfg
+     # Apply network config for all nics and any secondary IPv4/v6 addresses
++    nic_idx = 0
+     for mac, nic_name in sorted(macs_to_nics.items()):
+         nic_metadata = macs_metadata.get(mac)
+         if not nic_metadata:
+             continue  # Not a physical nic represented in metadata
+         # device-number is zero-indexed, we want it 1-indexed for the
+         # multiplication on the following line
+-        nic_idx = int(nic_metadata['device-number']) + 1
++        nic_idx = int(nic_metadata.get('device-number', nic_idx)) + 1
+         dhcp_override = {'route-metric': nic_idx * 100}
+         dev_config = {'dhcp4': True, 'dhcp4-overrides': dhcp_override,
+                       'dhcp6': False,
+diff --git a/tests/unittests/test_datasource/test_aliyun.py b/tests/unittests/test_datasource/test_aliyun.py
+index eb2828d5..cab1ac2b 100644
+--- a/tests/unittests/test_datasource/test_aliyun.py
++++ b/tests/unittests/test_datasource/test_aliyun.py
+@@ -7,6 +7,7 @@ from unittest import mock
+ 
+ from cloudinit import helpers
+ from cloudinit.sources import DataSourceAliYun as ay
++from cloudinit.sources.DataSourceEc2 import convert_ec2_metadata_network_config
+ from cloudinit.tests import helpers as test_helpers
+ 
+ DEFAULT_METADATA = {
+@@ -183,6 +184,35 @@ class TestAliYunDatasource(test_helpers.HttprettyTestCase):
+         self.assertEqual(ay.parse_public_keys(public_keys),
+                          public_keys['key-pair-0']['openssh-key'])
+ 
++    def test_route_metric_calculated_without_device_number(self):
++        """Test that route-metric code works without `device-number`
++
++        `device-number` is part of EC2 metadata, but not supported on aliyun.
++        Attempting to access it will raise a KeyError.
++
++        LP: #1917875
++        """
++        netcfg = convert_ec2_metadata_network_config(
++            {"interfaces": {"macs": {
++                "06:17:04:d7:26:09": {
++                    "interface-id": "eni-e44ef49e",
++                },
++                "06:17:04:d7:26:08": {
++                    "interface-id": "eni-e44ef49f",
++                }
++            }}},
++            macs_to_nics={
++                '06:17:04:d7:26:09': 'eth0',
++                '06:17:04:d7:26:08': 'eth1',
++            }
++        )
++
++        met0 = netcfg['ethernets']['eth0']['dhcp4-overrides']['route-metric']
++        met1 = netcfg['ethernets']['eth1']['dhcp4-overrides']['route-metric']
++
++        # route-metric numbers should be 100 apart
++        assert 100 == abs(met0 - met1)
++
+ 
+ class TestIsAliYun(test_helpers.CiTestCase):
+     ALIYUN_PRODUCT = 'Alibaba Cloud ECS'
+-- 
+2.27.0
+
diff --git a/SOURCES/ci-Add-config-modules-for-controlling-IBM-PowerVM-RMC.-.patch b/SOURCES/ci-Add-config-modules-for-controlling-IBM-PowerVM-RMC.-.patch
deleted file mode 100644
index c3f45ff..0000000
--- a/SOURCES/ci-Add-config-modules-for-controlling-IBM-PowerVM-RMC.-.patch
+++ /dev/null
@@ -1,496 +0,0 @@
-From c3a1b3a5d7abe51a1facbdae71aca4b2bca7d6aa Mon Sep 17 00:00:00 2001
-From: Eduardo Otubo <otubo@redhat.com>
-Date: Wed, 28 Oct 2020 20:43:33 +0100
-Subject: [PATCH 2/3] Add config modules for controlling IBM PowerVM RMC.
- (#584)
-
-RH-Author: Eduardo Terrell Ferrari Otubo (eterrell)
-RH-MergeRequest: 12: Support for cloud-init config modules for PowerVM Hypervisor in Red Hat cloud-init
-RH-Commit: [1/1] d175c3607a8d4f473573ba0ce42e0f311dbc31ed (eterrell/cloud-init)
-RH-Bugzilla: 1886430
-
-commit f99d4f96b00a9cfec1c721d364cbfd728674e5dc (upstream/master)
-Author: Aman306 <45781773+Aman306@users.noreply.github.com>
-Date:   Wed Oct 28 23:36:09 2020 +0530
-
-    Add config modules for controlling IBM PowerVM RMC. (#584)
-
-    Reliable Scalable Cluster Technology (RSCT) is a set of software
-    components that together provide a comprehensive clustering
-    environment(RAS features) for IBM PowerVM based virtual machines. RSCT
-    includes the Resource Monitoring and Control (RMC) subsystem. RMC is a
-    generalized framework used for managing, monitoring, and manipulating
-    resources. RMC runs as a daemon process on individual machines and needs
-    creation of unique node id and restarts during VM boot.
-
-    LP: #1895979
-
-    Co-authored-by: Scott Moser <smoser@brickies.net>
-
-Signed-off-by: Eduardo Otubo <otubo@redhat.com>
----
- cloudinit/config/cc_refresh_rmc_and_interface.py   | 159 +++++++++++++++++++++
- cloudinit/config/cc_reset_rmc.py                   | 143 ++++++++++++++++++
- config/cloud.cfg.tmpl                              |   2 +
- .../test_handler_refresh_rmc_and_interface.py      | 109 ++++++++++++++
- tools/.github-cla-signers                          |   1 +
- 5 files changed, 414 insertions(+)
- create mode 100644 cloudinit/config/cc_refresh_rmc_and_interface.py
- create mode 100644 cloudinit/config/cc_reset_rmc.py
- create mode 100644 tests/unittests/test_handler/test_handler_refresh_rmc_and_interface.py
-
-diff --git a/cloudinit/config/cc_refresh_rmc_and_interface.py b/cloudinit/config/cc_refresh_rmc_and_interface.py
-new file mode 100644
-index 0000000..146758a
---- /dev/null
-+++ b/cloudinit/config/cc_refresh_rmc_and_interface.py
-@@ -0,0 +1,159 @@
-+# (c) Copyright IBM Corp. 2020 All Rights Reserved
-+#
-+# Author: Aman Kumar Sinha <amansi26@in.ibm.com>
-+#
-+# This file is part of cloud-init. See LICENSE file for license information.
-+
-+"""
-+Refresh IPv6 interface and RMC
-+------------------------------
-+**Summary:** Ensure Network Manager is not managing IPv6 interface
-+
-+This module is IBM PowerVM Hypervisor specific
-+
-+Reliable Scalable Cluster Technology (RSCT) is a set of software components
-+that together provide a comprehensive clustering environment(RAS features)
-+for IBM PowerVM based virtual machines. RSCT includes the Resource
-+Monitoring and Control (RMC) subsystem. RMC is a generalized framework used
-+for managing, monitoring, and manipulating resources. RMC runs as a daemon
-+process on individual machines and needs creation of unique node id and
-+restarts during VM boot.
-+More details refer
-+https://www.ibm.com/support/knowledgecenter/en/SGVKBA_3.2/admin/bl503_ovrv.htm
-+
-+This module handles
-+- Refreshing RMC
-+- Disabling NetworkManager from handling IPv6 interface, as IPv6 interface
-+  is used for communication between RMC daemon and PowerVM hypervisor.
-+
-+**Internal name:** ``cc_refresh_rmc_and_interface``
-+
-+**Module frequency:** per always
-+
-+**Supported distros:** RHEL
-+
-+"""
-+
-+from cloudinit import log as logging
-+from cloudinit.settings import PER_ALWAYS
-+from cloudinit import util
-+from cloudinit import subp
-+from cloudinit import netinfo
-+
-+import errno
-+
-+frequency = PER_ALWAYS
-+
-+LOG = logging.getLogger(__name__)
-+# Ensure that /opt/rsct/bin has been added to standard PATH of the
-+# distro. The symlink to rmcctrl is /usr/sbin/rsct/bin/rmcctrl .
-+RMCCTRL = 'rmcctrl'
-+
-+
-+def handle(name, _cfg, _cloud, _log, _args):
-+    if not subp.which(RMCCTRL):
-+        LOG.debug("No '%s' in path, disabled", RMCCTRL)
-+        return
-+
-+    LOG.debug(
-+        'Making the IPv6 up explicitly. '
-+        'Ensuring IPv6 interface is not being handled by NetworkManager '
-+        'and it is  restarted to re-establish the communication with '
-+        'the hypervisor')
-+
-+    ifaces = find_ipv6_ifaces()
-+
-+    # Setting NM_CONTROLLED=no for IPv6 interface
-+    # making it down and up
-+
-+    if len(ifaces) == 0:
-+        LOG.debug("Did not find any interfaces with ipv6 addresses.")
-+    else:
-+        for iface in ifaces:
-+            refresh_ipv6(iface)
-+            disable_ipv6(sysconfig_path(iface))
-+        restart_network_manager()
-+
-+
-+def find_ipv6_ifaces():
-+    info = netinfo.netdev_info()
-+    ifaces = []
-+    for iface, data in info.items():
-+        if iface == "lo":
-+            LOG.debug('Skipping localhost interface')
-+        if len(data.get("ipv4", [])) != 0:
-+            # skip this interface, as it has ipv4 addrs
-+            continue
-+        ifaces.append(iface)
-+    return ifaces
-+
-+
-+def refresh_ipv6(interface):
-+    # IPv6 interface is explicitly brought up, subsequent to which the
-+    # RMC services are restarted to re-establish the communication with
-+    # the hypervisor.
-+    subp.subp(['ip', 'link', 'set', interface, 'down'])
-+    subp.subp(['ip', 'link', 'set', interface, 'up'])
-+
-+
-+def sysconfig_path(iface):
-+    return '/etc/sysconfig/network-scripts/ifcfg-' + iface
-+
-+
-+def restart_network_manager():
-+    subp.subp(['systemctl', 'restart', 'NetworkManager'])
-+
-+
-+def disable_ipv6(iface_file):
-+    # Ensuring that the communication b/w the hypervisor and VM is not
-+    # interrupted due to NetworkManager. For this purpose, as part of
-+    # this function, the NM_CONTROLLED is explicitly set to No for IPV6
-+    # interface and NetworkManager is restarted.
-+    try:
-+        contents = util.load_file(iface_file)
-+    except IOError as e:
-+        if e.errno == errno.ENOENT:
-+            LOG.debug("IPv6 interface file %s does not exist\n",
-+                      iface_file)
-+        else:
-+            raise e
-+
-+    if 'IPV6INIT' not in contents:
-+        LOG.debug("Interface file %s did not have IPV6INIT", iface_file)
-+        return
-+
-+    LOG.debug("Editing interface file %s ", iface_file)
-+
-+    # Dropping any NM_CONTROLLED or IPV6 lines from IPv6 interface file.
-+    lines = contents.splitlines()
-+    lines = [line for line in lines if not search(line)]
-+    lines.append("NM_CONTROLLED=no")
-+
-+    with open(iface_file, "w") as fp:
-+        fp.write("\n".join(lines) + "\n")
-+
-+
-+def search(contents):
-+    # Search for any NM_CONTROLLED or IPV6 lines in IPv6 interface file.
-+    return(
-+        contents.startswith("IPV6ADDR") or
-+        contents.startswith("IPADDR6") or
-+        contents.startswith("IPV6INIT") or
-+        contents.startswith("NM_CONTROLLED"))
-+
-+
-+def refresh_rmc():
-+    # To make a healthy connection between RMC daemon and hypervisor we
-+    # refresh RMC. With refreshing RMC we are ensuring that making IPv6
-+    # down and up shouldn't impact communication between RMC daemon and
-+    # hypervisor.
-+    # -z : stop Resource Monitoring & Control subsystem and all resource
-+    # managers, but the command does not return control to the user
-+    # until the subsystem and all resource managers are stopped.
-+    # -s : start Resource Monitoring & Control subsystem.
-+    try:
-+        subp.subp([RMCCTRL, '-z'])
-+        subp.subp([RMCCTRL, '-s'])
-+    except Exception:
-+        util.logexc(LOG, 'Failed to refresh the RMC subsystem.')
-+        raise
-diff --git a/cloudinit/config/cc_reset_rmc.py b/cloudinit/config/cc_reset_rmc.py
-new file mode 100644
-index 0000000..1cd7277
---- /dev/null
-+++ b/cloudinit/config/cc_reset_rmc.py
-@@ -0,0 +1,143 @@
-+# (c) Copyright IBM Corp. 2020 All Rights Reserved
-+#
-+# Author: Aman Kumar Sinha <amansi26@in.ibm.com>
-+#
-+# This file is part of cloud-init. See LICENSE file for license information.
-+
-+
-+"""
-+Reset RMC
-+------------
-+**Summary:** reset rsct node id
-+
-+Reset RMC module is IBM PowerVM Hypervisor specific
-+
-+Reliable Scalable Cluster Technology (RSCT) is a set of software components,
-+that  together provide a comprehensive clustering environment (RAS features)
-+for IBM PowerVM based virtual machines. RSCT includes the Resource monitoring
-+and control (RMC) subsystem. RMC is a generalized framework used for managing,
-+monitoring, and manipulating resources. RMC runs as a daemon process on
-+individual machines and needs creation of unique node id and restarts
-+during VM boot.
-+More details refer
-+https://www.ibm.com/support/knowledgecenter/en/SGVKBA_3.2/admin/bl503_ovrv.htm
-+
-+This module handles
-+- creation of the unique RSCT node id to every instance/virtual machine
-+  and ensure once set, it isn't changed subsequently by cloud-init.
-+  In order to do so, it restarts RSCT service.
-+
-+Prerequisite of using this module is to install RSCT packages.
-+
-+**Internal name:** ``cc_reset_rmc``
-+
-+**Module frequency:** per instance
-+
-+**Supported distros:** rhel, sles and ubuntu
-+
-+"""
-+import os
-+
-+from cloudinit import log as logging
-+from cloudinit.settings import PER_INSTANCE
-+from cloudinit import util
-+from cloudinit import subp
-+
-+frequency = PER_INSTANCE
-+
-+# RMCCTRL is expected to be in system PATH (/opt/rsct/bin)
-+# The symlink for RMCCTRL and RECFGCT are
-+# /usr/sbin/rsct/bin/rmcctrl and
-+# /usr/sbin/rsct/install/bin/recfgct respectively.
-+RSCT_PATH = '/opt/rsct/install/bin'
-+RMCCTRL = 'rmcctrl'
-+RECFGCT = 'recfgct'
-+
-+LOG = logging.getLogger(__name__)
-+
-+NODE_ID_FILE = '/etc/ct_node_id'
-+
-+
-+def handle(name, _cfg, cloud, _log, _args):
-+    # Ensuring node id has to be generated only once during first boot
-+    if cloud.datasource.platform_type == 'none':
-+        LOG.debug('Skipping creation of new ct_node_id node')
-+        return
-+
-+    if not os.path.isdir(RSCT_PATH):
-+        LOG.debug("module disabled, RSCT_PATH not present")
-+        return
-+
-+    orig_path = os.environ.get('PATH')
-+    try:
-+        add_path(orig_path)
-+        reset_rmc()
-+    finally:
-+        if orig_path:
-+            os.environ['PATH'] = orig_path
-+        else:
-+            del os.environ['PATH']
-+
-+
-+def reconfigure_rsct_subsystems():
-+    # Reconfigure the RSCT subsystems, which includes removing all RSCT data
-+    # under the /var/ct directory, generating a new node ID, and making it
-+    # appear as if the RSCT components were just installed
-+    try:
-+        out = subp.subp([RECFGCT])[0]
-+        LOG.debug(out.strip())
-+        return out
-+    except subp.ProcessExecutionError:
-+        util.logexc(LOG, 'Failed to reconfigure the RSCT subsystems.')
-+        raise
-+
-+
-+def get_node_id():
-+    try:
-+        fp = util.load_file(NODE_ID_FILE)
-+        node_id = fp.split('\n')[0]
-+        return node_id
-+    except Exception:
-+        util.logexc(LOG, 'Failed to get node ID from file %s.' % NODE_ID_FILE)
-+        raise
-+
-+
-+def add_path(orig_path):
-+    # Adding the RSCT_PATH to env standard path
-+    # So thet cloud init automatically find and
-+    # run RECFGCT to create new node_id.
-+    suff = ":" + orig_path if orig_path else ""
-+    os.environ['PATH'] = RSCT_PATH + suff
-+    return os.environ['PATH']
-+
-+
-+def rmcctrl():
-+    # Stop the RMC subsystem and all resource managers so that we can make
-+    # some changes to it
-+    try:
-+        return subp.subp([RMCCTRL, '-z'])
-+    except Exception:
-+        util.logexc(LOG, 'Failed to stop the RMC subsystem.')
-+        raise
-+
-+
-+def reset_rmc():
-+    LOG.debug('Attempting to reset RMC.')
-+
-+    node_id_before = get_node_id()
-+    LOG.debug('Node ID at beginning of module: %s', node_id_before)
-+
-+    # Stop the RMC subsystem and all resource managers so that we can make
-+    # some changes to it
-+    rmcctrl()
-+    reconfigure_rsct_subsystems()
-+
-+    node_id_after = get_node_id()
-+    LOG.debug('Node ID at end of module: %s', node_id_after)
-+
-+    # Check if new node ID is generated or not
-+    # by comparing old and new node ID
-+    if node_id_after == node_id_before:
-+        msg = 'New node ID did not get generated.'
-+        LOG.error(msg)
-+        raise Exception(msg)
-diff --git a/config/cloud.cfg.tmpl b/config/cloud.cfg.tmpl
-index 2beb9b0..7171aaa 100644
---- a/config/cloud.cfg.tmpl
-+++ b/config/cloud.cfg.tmpl
-@@ -135,6 +135,8 @@ cloud_final_modules:
-  - chef
-  - mcollective
-  - salt-minion
-+ - reset_rmc
-+ - refresh_rmc_and_interface
-  - rightscale_userdata
-  - scripts-vendor
-  - scripts-per-once
-diff --git a/tests/unittests/test_handler/test_handler_refresh_rmc_and_interface.py b/tests/unittests/test_handler/test_handler_refresh_rmc_and_interface.py
-new file mode 100644
-index 0000000..e13b779
---- /dev/null
-+++ b/tests/unittests/test_handler/test_handler_refresh_rmc_and_interface.py
-@@ -0,0 +1,109 @@
-+from cloudinit.config import cc_refresh_rmc_and_interface as ccrmci
-+
-+from cloudinit import util
-+
-+from cloudinit.tests import helpers as t_help
-+from cloudinit.tests.helpers import mock
-+
-+from textwrap import dedent
-+import logging
-+
-+LOG = logging.getLogger(__name__)
-+MPATH = "cloudinit.config.cc_refresh_rmc_and_interface"
-+NET_INFO = {
-+    'lo': {'ipv4': [{'ip': '127.0.0.1',
-+                    'bcast': '', 'mask': '255.0.0.0',
-+                                 'scope': 'host'}],
-+           'ipv6': [{'ip': '::1/128',
-+                     'scope6': 'host'}], 'hwaddr': '',
-+           'up': 'True'},
-+    'env2': {'ipv4': [{'ip': '8.0.0.19',
-+                       'bcast': '8.0.0.255', 'mask': '255.255.255.0',
-+                                             'scope': 'global'}],
-+             'ipv6': [{'ip': 'fe80::f896:c2ff:fe81:8220/64',
-+                       'scope6': 'link'}], 'hwaddr': 'fa:96:c2:81:82:20',
-+             'up': 'True'},
-+    'env3': {'ipv4': [{'ip': '90.0.0.14',
-+                       'bcast': '90.0.0.255', 'mask': '255.255.255.0',
-+                                              'scope': 'global'}],
-+             'ipv6': [{'ip': 'fe80::f896:c2ff:fe81:8221/64',
-+                       'scope6': 'link'}], 'hwaddr': 'fa:96:c2:81:82:21',
-+             'up': 'True'},
-+    'env4': {'ipv4': [{'ip': '9.114.23.7',
-+                       'bcast': '9.114.23.255', 'mask': '255.255.255.0',
-+                                                'scope': 'global'}],
-+             'ipv6': [{'ip': 'fe80::f896:c2ff:fe81:8222/64',
-+                       'scope6': 'link'}], 'hwaddr': 'fa:96:c2:81:82:22',
-+             'up': 'True'},
-+    'env5': {'ipv4': [],
-+             'ipv6': [{'ip': 'fe80::9c26:c3ff:fea4:62c8/64',
-+                       'scope6': 'link'}], 'hwaddr': '42:20:86:df:fa:4c',
-+             'up': 'True'}}
-+
-+
-+class TestRsctNodeFile(t_help.CiTestCase):
-+    def test_disable_ipv6_interface(self):
-+        """test parsing of iface files."""
-+        fname = self.tmp_path("iface-eth5")
-+        util.write_file(fname, dedent("""\
-+            BOOTPROTO=static
-+            DEVICE=eth5
-+            HWADDR=42:20:86:df:fa:4c
-+            IPV6INIT=yes
-+            IPADDR6=fe80::9c26:c3ff:fea4:62c8/64
-+            IPV6ADDR=fe80::9c26:c3ff:fea4:62c8/64
-+            NM_CONTROLLED=yes
-+            ONBOOT=yes
-+            STARTMODE=auto
-+            TYPE=Ethernet
-+            USERCTL=no
-+            """))
-+
-+        ccrmci.disable_ipv6(fname)
-+        self.assertEqual(dedent("""\
-+            BOOTPROTO=static
-+            DEVICE=eth5
-+            HWADDR=42:20:86:df:fa:4c
-+            ONBOOT=yes
-+            STARTMODE=auto
-+            TYPE=Ethernet
-+            USERCTL=no
-+            NM_CONTROLLED=no
-+            """), util.load_file(fname))
-+
-+    @mock.patch(MPATH + '.refresh_rmc')
-+    @mock.patch(MPATH + '.restart_network_manager')
-+    @mock.patch(MPATH + '.disable_ipv6')
-+    @mock.patch(MPATH + '.refresh_ipv6')
-+    @mock.patch(MPATH + '.netinfo.netdev_info')
-+    @mock.patch(MPATH + '.subp.which')
-+    def test_handle(self, m_refresh_rmc,
-+                    m_netdev_info, m_refresh_ipv6, m_disable_ipv6,
-+                    m_restart_nm, m_which):
-+        """Basic test of handle."""
-+        m_netdev_info.return_value = NET_INFO
-+        m_which.return_value = '/opt/rsct/bin/rmcctrl'
-+        ccrmci.handle(
-+            "refresh_rmc_and_interface", None, None, None, None)
-+        self.assertEqual(1, m_netdev_info.call_count)
-+        m_refresh_ipv6.assert_called_with('env5')
-+        m_disable_ipv6.assert_called_with(
-+            '/etc/sysconfig/network-scripts/ifcfg-env5')
-+        self.assertEqual(1, m_restart_nm.call_count)
-+        self.assertEqual(1, m_refresh_rmc.call_count)
-+
-+    @mock.patch(MPATH + '.netinfo.netdev_info')
-+    def test_find_ipv6(self, m_netdev_info):
-+        """find_ipv6_ifaces parses netdev_info returning those with ipv6"""
-+        m_netdev_info.return_value = NET_INFO
-+        found = ccrmci.find_ipv6_ifaces()
-+        self.assertEqual(['env5'], found)
-+
-+    @mock.patch(MPATH + '.subp.subp')
-+    def test_refresh_ipv6(self, m_subp):
-+        """refresh_ipv6 should ip down and up the interface."""
-+        iface = "myeth0"
-+        ccrmci.refresh_ipv6(iface)
-+        m_subp.assert_has_calls([
-+            mock.call(['ip', 'link', 'set', iface, 'down']),
-+            mock.call(['ip', 'link', 'set', iface, 'up'])])
-diff --git a/tools/.github-cla-signers b/tools/.github-cla-signers
-index c67db43..802a35b 100644
---- a/tools/.github-cla-signers
-+++ b/tools/.github-cla-signers
-@@ -1,4 +1,5 @@
- AlexBaranowski
-+Aman306
- beezly
- bipinbachhao
- BirknerAlex
--- 
-1.8.3.1
-
diff --git a/SOURCES/ci-Adding-BOOTPROTO-dhcp-to-render-sysconfig-dhcp6-stat.patch b/SOURCES/ci-Adding-BOOTPROTO-dhcp-to-render-sysconfig-dhcp6-stat.patch
deleted file mode 100644
index c31b4b2..0000000
--- a/SOURCES/ci-Adding-BOOTPROTO-dhcp-to-render-sysconfig-dhcp6-stat.patch
+++ /dev/null
@@ -1,58 +0,0 @@
-From 8a7d21fa739901bad847294004266dba76c027af Mon Sep 17 00:00:00 2001
-From: Eduardo Otubo <otubo@redhat.com>
-Date: Tue, 1 Dec 2020 15:51:47 +0100
-Subject: [PATCH 2/4] Adding BOOTPROTO = dhcp to render sysconfig dhcp6
- stateful on RHEL (#685)
-
-RH-Author: Eduardo Terrell Ferrari Otubo (eterrell)
-RH-MergeRequest: 25: Adding BOOTPROTO = dhcp to render sysconfig dhcp6 stateful on RHEL (#685)
-RH-Commit: [1/1] b7304323096b1e40287950e44cf7aa3cdb4ba99e (eterrell/cloud-init)
-RH-Bugzilla: 1859695
-
-BOOTPROTO needs to be set to 'dhcp' on RHEL so NetworkManager can
-properly acquire ipv6 address.
-
-rhbz: #1859695
-
-Signed-off-by: Eduardo Otubo <otubo@redhat.com>
-
-Co-authored-by: Daniel Watkins <oddbloke@ubuntu.com>
-Co-authored-by: Scott Moser <smoser@brickies.net>
----
- cloudinit/net/sysconfig.py  | 6 ++++++
- tests/unittests/test_net.py | 2 +-
- 2 files changed, 7 insertions(+), 1 deletion(-)
-
-diff --git a/cloudinit/net/sysconfig.py b/cloudinit/net/sysconfig.py
-index 078636a4..94801a93 100644
---- a/cloudinit/net/sysconfig.py
-+++ b/cloudinit/net/sysconfig.py
-@@ -391,6 +391,12 @@ class Renderer(renderer.Renderer):
-                         # Only IPv6 is DHCP, IPv4 may be static
-                         iface_cfg['BOOTPROTO'] = 'dhcp6'
-                     iface_cfg['DHCLIENT6_MODE'] = 'managed'
-+                # only if rhel AND dhcpv6 stateful
-+                elif (flavor == 'rhel' and
-+                        subnet_type == 'ipv6_dhcpv6-stateful'):
-+                    iface_cfg['BOOTPROTO'] = 'dhcp'
-+                    iface_cfg['DHCPV6C'] = True
-+                    iface_cfg['IPV6INIT'] = True
-                 else:
-                     iface_cfg['IPV6INIT'] = True
-                     # Configure network settings using DHCPv6
-diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py
-index c0337459..bcd261db 100644
---- a/tests/unittests/test_net.py
-+++ b/tests/unittests/test_net.py
-@@ -1359,7 +1359,7 @@ NETWORK_CONFIGS = {
-         },
-         'expected_sysconfig_rhel': {
-             'ifcfg-iface0': textwrap.dedent("""\
--            BOOTPROTO=none
-+            BOOTPROTO=dhcp
-             DEVICE=iface0
-             DHCPV6C=yes
-             IPV6INIT=yes
--- 
-2.18.4
-
diff --git a/SOURCES/ci-DataSourceAzure-update-password-for-defuser-if-exist.patch b/SOURCES/ci-DataSourceAzure-update-password-for-defuser-if-exist.patch
deleted file mode 100644
index 7a9f478..0000000
--- a/SOURCES/ci-DataSourceAzure-update-password-for-defuser-if-exist.patch
+++ /dev/null
@@ -1,60 +0,0 @@
-From bcbd6be99d8317793aff905c4222c351a1bf5c46 Mon Sep 17 00:00:00 2001
-From: Eduardo Otubo <otubo@redhat.com>
-Date: Thu, 21 Jan 2021 10:08:49 +0100
-Subject: [PATCH 1/2] DataSourceAzure: update password for defuser if exists
- (#671)
-
-RH-Author: Eduardo Terrell Ferrari Otubo (eterrell)
-RH-MergeRequest: 37: DataSourceAzure: update password for defuser if exists (#671)
-RH-Commit: [1/1] 264092a68a3771cc4ed99dad5b93f7a1433e143a (eterrell/cloud-init)
-RH-Bugzilla: 1900892
-
-commit eea754492f074e00b601cf77aa278e3623857c5a
-Author: Anh Vo <anhvo@microsoft.com>
-Date:   Thu Nov 19 00:35:46 2020 -0500
-
-    DataSourceAzure: update password for defuser if exists (#671)
-
-    cc_set_password will only update the password for the default user if
-    cfg['password'] is set. The existing code of datasource Azure will fail
-    to update the default user's password because it does not set that
-    metadata. If the default user doesn't exist in the image, the current
-    code works fine because the password is set during user create and
-    not in cc_set_password
-
-Signed-off-by: Eduardo Otubo <otubo@redhat.com>
----
- cloudinit/sources/DataSourceAzure.py          | 2 +-
- tests/unittests/test_datasource/test_azure.py | 3 +++
- 2 files changed, 4 insertions(+), 1 deletion(-)
-
-diff --git a/cloudinit/sources/DataSourceAzure.py b/cloudinit/sources/DataSourceAzure.py
-index 1c214db9..d4a2d60f 100755
---- a/cloudinit/sources/DataSourceAzure.py
-+++ b/cloudinit/sources/DataSourceAzure.py
-@@ -1231,7 +1231,7 @@ def read_azure_ovf(contents):
-     if password:
-         defuser['lock_passwd'] = False
-         if DEF_PASSWD_REDACTION != password:
--            defuser['passwd'] = encrypt_pass(password)
-+            defuser['passwd'] = cfg['password'] = encrypt_pass(password)
- 
-     if defuser:
-         cfg['system_info'] = {'default_user': defuser}
-diff --git a/tests/unittests/test_datasource/test_azure.py b/tests/unittests/test_datasource/test_azure.py
-index 47e03bd1..2059990a 100644
---- a/tests/unittests/test_datasource/test_azure.py
-+++ b/tests/unittests/test_datasource/test_azure.py
-@@ -919,6 +919,9 @@ scbus-1 on xpt0 bus 0
-                          crypt.crypt(odata['UserPassword'],
-                                      defuser['passwd'][0:pos]))
- 
-+        # the same hashed value should also be present in cfg['password']
-+        self.assertEqual(defuser['passwd'], dsrc.cfg['password'])
-+
-     def test_user_not_locked_if_password_redacted(self):
-         odata = {'HostName': "myhost", 'UserName': "myuser",
-                  'UserPassword': dsaz.DEF_PASSWD_REDACTION}
--- 
-2.18.4
-
diff --git a/SOURCES/ci-Explicit-set-IPV6_AUTOCONF-and-IPV6_FORCE_ACCEPT_RA-.patch b/SOURCES/ci-Explicit-set-IPV6_AUTOCONF-and-IPV6_FORCE_ACCEPT_RA-.patch
deleted file mode 100644
index a0d9156..0000000
--- a/SOURCES/ci-Explicit-set-IPV6_AUTOCONF-and-IPV6_FORCE_ACCEPT_RA-.patch
+++ /dev/null
@@ -1,295 +0,0 @@
-From 5ded09d5acf4d653fe2cbd54814f53063d265489 Mon Sep 17 00:00:00 2001
-From: Eduardo Otubo <otubo@redhat.com>
-Date: Thu, 29 Oct 2020 15:05:42 +0100
-Subject: [PATCH 1/3] Explicit set IPV6_AUTOCONF and IPV6_FORCE_ACCEPT_RA on
- static6 (#634)
-
-RH-Author: Eduardo Terrell Ferrari Otubo (eterrell)
-RH-MergeRequest: 13: [RHEL-8.4.0] Add support for ipv6_autoconf on cloud-init-20.3
-RH-Commit: [1/1] 41e61c35893f4487981a1ad31f9f97a9a740b397 (eterrell/cloud-init)
-RH-Bugzilla: 1889635
-
-commit b46e4a8cff667c8441622089cf7d57aeb88220cd
-Author: Eduardo Otubo <otubo@redhat.com>
-Date:   Thu Oct 29 15:05:42 2020 +0100
-
-    Explicit set IPV6_AUTOCONF and IPV6_FORCE_ACCEPT_RA on static6 (#634)
-
-    The static and static6 subnet types for network_data.json were
-    being ignored by the Openstack handler, this would cause the code to
-    break and not function properly.
-
-    As of today, if a static6 configuration is chosen, the interface will
-    still eventually be available to receive router advertisements or be set
-    from NetworkManager to wait for them and cycle the interface in negative
-    case.
-
-    It is safe to assume that if the interface is manually configured to use
-    static ipv6 address, there's no need to wait for router advertisements.
-    This patch will set automatically IPV6_AUTOCONF and IPV6_FORCE_ACCEPT_RA
-    both to "no" in this case.
-
-    This patch fixes the specific behavior only for RHEL flavor and
-    sysconfig renderer. It also introduces new unit tests for the specific
-    case as well as adjusts some existent tests to be compatible with the
-    new options. This patch also addresses this problem by assigning the
-    appropriate subnet type for each case on the openstack handler.
-
-    rhbz: #1889635
-    rhbz: #1889635
-
-    Signed-off-by: Eduardo Otubo otubo@redhat.com
-
-Signed-off-by: Eduardo Otubo otubo@redhat.com
----
- cloudinit/net/network_state.py                 |   3 +-
- cloudinit/net/sysconfig.py                     |   4 +
- cloudinit/sources/helpers/openstack.py         |   8 +-
- tests/unittests/test_distros/test_netconfig.py |   2 +
- tests/unittests/test_net.py                    | 100 +++++++++++++++++++++++++
- 5 files changed, 115 insertions(+), 2 deletions(-)
-
-diff --git a/cloudinit/net/network_state.py b/cloudinit/net/network_state.py
-index b2f7d31..d9e7fd5 100644
---- a/cloudinit/net/network_state.py
-+++ b/cloudinit/net/network_state.py
-@@ -820,7 +820,8 @@ def _normalize_subnet(subnet):
- 
-     if subnet.get('type') in ('static', 'static6'):
-         normal_subnet.update(
--            _normalize_net_keys(normal_subnet, address_keys=('address',)))
-+            _normalize_net_keys(normal_subnet, address_keys=(
-+                'address', 'ip_address',)))
-     normal_subnet['routes'] = [_normalize_route(r)
-                                for r in subnet.get('routes', [])]
- 
-diff --git a/cloudinit/net/sysconfig.py b/cloudinit/net/sysconfig.py
-index af093dd..c078898 100644
---- a/cloudinit/net/sysconfig.py
-+++ b/cloudinit/net/sysconfig.py
-@@ -451,6 +451,10 @@ class Renderer(renderer.Renderer):
-                             iface_cfg[mtu_key] = subnet['mtu']
-                     else:
-                         iface_cfg[mtu_key] = subnet['mtu']
-+
-+                if subnet_is_ipv6(subnet) and flavor == 'rhel':
-+                    iface_cfg['IPV6_FORCE_ACCEPT_RA'] = False
-+                    iface_cfg['IPV6_AUTOCONF'] = False
-             elif subnet_type == 'manual':
-                 if flavor == 'suse':
-                     LOG.debug('Unknown subnet type setting "%s"', subnet_type)
-diff --git a/cloudinit/sources/helpers/openstack.py b/cloudinit/sources/helpers/openstack.py
-index 65e020c..3e6365f 100644
---- a/cloudinit/sources/helpers/openstack.py
-+++ b/cloudinit/sources/helpers/openstack.py
-@@ -602,11 +602,17 @@ def convert_net_json(network_json=None, known_macs=None):
-             elif network['type'] in ['ipv6_slaac', 'ipv6_dhcpv6-stateless',
-                                      'ipv6_dhcpv6-stateful']:
-                 subnet.update({'type': network['type']})
--            elif network['type'] in ['ipv4', 'ipv6']:
-+            elif network['type'] in ['ipv4', 'static']:
-                 subnet.update({
-                     'type': 'static',
-                     'address': network.get('ip_address'),
-                 })
-+            elif network['type'] in ['ipv6', 'static6']:
-+                cfg.update({'accept-ra': False})
-+                subnet.update({
-+                    'type': 'static6',
-+                    'address': network.get('ip_address'),
-+                })
- 
-             # Enable accept_ra for stateful and legacy ipv6_dhcp types
-             if network['type'] in ['ipv6_dhcpv6-stateful', 'ipv6_dhcp']:
-diff --git a/tests/unittests/test_distros/test_netconfig.py b/tests/unittests/test_distros/test_netconfig.py
-index 8d7b09c..f9fc3a1 100644
---- a/tests/unittests/test_distros/test_netconfig.py
-+++ b/tests/unittests/test_distros/test_netconfig.py
-@@ -514,7 +514,9 @@ class TestNetCfgDistroRedhat(TestNetCfgDistroBase):
-                 DEVICE=eth0
-                 IPV6ADDR=2607:f0d0:1002:0011::2/64
-                 IPV6INIT=yes
-+                IPV6_AUTOCONF=no
-                 IPV6_DEFAULTGW=2607:f0d0:1002:0011::1
-+                IPV6_FORCE_ACCEPT_RA=no
-                 NM_CONTROLLED=no
-                 ONBOOT=yes
-                 TYPE=Ethernet
-diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py
-index 9985a97..d7a7a65 100644
---- a/tests/unittests/test_net.py
-+++ b/tests/unittests/test_net.py
-@@ -750,7 +750,9 @@ IPADDR=172.19.1.34
- IPV6ADDR=2001:DB8::10/64
- IPV6ADDR_SECONDARIES="2001:DB9::10/64 2001:DB10::10/64"
- IPV6INIT=yes
-+IPV6_AUTOCONF=no
- IPV6_DEFAULTGW=2001:DB8::1
-+IPV6_FORCE_ACCEPT_RA=no
- NETMASK=255.255.252.0
- ONBOOT=yes
- TYPE=Ethernet
-@@ -1022,6 +1024,8 @@ NETWORK_CONFIGS = {
-                 IPADDR=192.168.14.2
-                 IPV6ADDR=2001:1::1/64
-                 IPV6INIT=yes
-+                IPV6_AUTOCONF=no
-+                IPV6_FORCE_ACCEPT_RA=no
-                 NETMASK=255.255.255.0
-                 ONBOOT=yes
-                 TYPE=Ethernet
-@@ -1247,6 +1251,33 @@ NETWORK_CONFIGS = {
-             """),
-         },
-     },
-+    'static6': {
-+        'yaml': textwrap.dedent("""\
-+        version: 1
-+        config:
-+          - type: 'physical'
-+            name: 'iface0'
-+            accept-ra: 'no'
-+            subnets:
-+            - type: 'static6'
-+              address: 2001:1::1/64
-+    """).rstrip(' '),
-+        'expected_sysconfig_rhel': {
-+            'ifcfg-iface0': textwrap.dedent("""\
-+            BOOTPROTO=none
-+            DEVICE=iface0
-+            IPV6ADDR=2001:1::1/64
-+            IPV6INIT=yes
-+            IPV6_AUTOCONF=no
-+            IPV6_FORCE_ACCEPT_RA=no
-+            DEVICE=iface0
-+            NM_CONTROLLED=no
-+            ONBOOT=yes
-+            TYPE=Ethernet
-+            USERCTL=no
-+            """),
-+        },
-+    },
-     'dhcpv6_stateless': {
-         'expected_eni': textwrap.dedent("""\
-         auto lo
-@@ -1636,6 +1667,8 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true
-                 IPADDR=192.168.14.2
-                 IPV6ADDR=2001:1::1/64
-                 IPV6INIT=yes
-+                IPV6_AUTOCONF=no
-+                IPV6_FORCE_ACCEPT_RA=no
-                 IPV6_DEFAULTGW=2001:4800:78ff:1b::1
-                 MACADDR=bb:bb:bb:bb:bb:aa
-                 NETMASK=255.255.255.0
-@@ -2158,6 +2191,8 @@ iface bond0 inet6 static
-         IPADDR1=192.168.1.2
-         IPV6ADDR=2001:1::1/92
-         IPV6INIT=yes
-+        IPV6_AUTOCONF=no
-+        IPV6_FORCE_ACCEPT_RA=no
-         MTU=9000
-         NETMASK=255.255.255.0
-         NETMASK1=255.255.255.0
-@@ -2259,6 +2294,8 @@ iface bond0 inet6 static
-                 IPADDR1=192.168.1.2
-                 IPV6ADDR=2001:1::bbbb/96
-                 IPV6INIT=yes
-+                IPV6_AUTOCONF=no
-+                IPV6_FORCE_ACCEPT_RA=no
-                 IPV6_DEFAULTGW=2001:1::1
-                 MTU=2222
-                 NETMASK=255.255.255.0
-@@ -2341,6 +2378,9 @@ iface bond0 inet6 static
-                 HWADDR=52:54:00:12:34:00
-                 IPV6ADDR=2001:1::100/96
-                 IPV6INIT=yes
-+                IPV6_AUTOCONF=no
-+                IPV6_FORCE_ACCEPT_RA=no
-+                NM_CONTROLLED=no
-                 ONBOOT=yes
-                 TYPE=Ethernet
-                 USERCTL=no
-@@ -2352,6 +2392,9 @@ iface bond0 inet6 static
-                 HWADDR=52:54:00:12:34:01
-                 IPV6ADDR=2001:1::101/96
-                 IPV6INIT=yes
-+                IPV6_AUTOCONF=no
-+                IPV6_FORCE_ACCEPT_RA=no
-+                NM_CONTROLLED=no
-                 ONBOOT=yes
-                 TYPE=Ethernet
-                 USERCTL=no
-@@ -3151,6 +3194,61 @@ USERCTL=no
-         self._compare_files_to_expected(entry[self.expected_name], found)
-         self._assert_headers(found)
- 
-+    def test_stattic6_from_json(self):
-+        net_json = {
-+            "services": [{"type": "dns", "address": "172.19.0.12"}],
-+            "networks": [{
-+                "network_id": "dacd568d-5be6-4786-91fe-750c374b78b4",
-+                "type": "ipv4", "netmask": "255.255.252.0",
-+                "link": "tap1a81968a-79",
-+                "routes": [{
-+                    "netmask": "0.0.0.0",
-+                    "network": "0.0.0.0",
-+                    "gateway": "172.19.3.254",
-+                }, {
-+                    "netmask": "0.0.0.0",  # A second default gateway
-+                    "network": "0.0.0.0",
-+                    "gateway": "172.20.3.254",
-+                }],
-+                "ip_address": "172.19.1.34", "id": "network0"
-+            }, {
-+                "network_id": "mgmt",
-+                "netmask": "ffff:ffff:ffff:ffff::",
-+                "link": "interface1",
-+                "mode": "link-local",
-+                "routes": [],
-+                "ip_address": "fe80::c096:67ff:fe5c:6e84",
-+                "type": "static6",
-+                "id": "network1",
-+                "services": [],
-+                "accept-ra": "false"
-+            }],
-+            "links": [
-+                {
-+                    "ethernet_mac_address": "fa:16:3e:ed:9a:59",
-+                    "mtu": None, "type": "bridge", "id":
-+                    "tap1a81968a-79",
-+                    "vif_id": "1a81968a-797a-400f-8a80-567f997eb93f"
-+                },
-+            ],
-+        }
-+        macs = {'fa:16:3e:ed:9a:59': 'eth0'}
-+        render_dir = self.tmp_dir()
-+        network_cfg = openstack.convert_net_json(net_json, known_macs=macs)
-+        ns = network_state.parse_net_config_data(network_cfg,
-+                                                 skip_broken=False)
-+        renderer = self._get_renderer()
-+        with self.assertRaises(ValueError):
-+            renderer.render_network_state(ns, target=render_dir)
-+        self.assertEqual([], os.listdir(render_dir))
-+
-+    def test_static6_from_yaml(self):
-+        entry = NETWORK_CONFIGS['static6']
-+        found = self._render_and_read(network_config=yaml.load(
-+            entry['yaml']))
-+        self._compare_files_to_expected(entry[self.expected_name], found)
-+        self._assert_headers(found)
-+
-     def test_dhcpv6_reject_ra_config_v2(self):
-         entry = NETWORK_CONFIGS['dhcpv6_reject_ra']
-         found = self._render_and_read(network_config=yaml.load(
-@@ -3268,6 +3366,8 @@ USERCTL=no
-                    IPADDR=192.168.42.100
-                    IPV6ADDR=2001:db8::100/32
-                    IPV6INIT=yes
-+                   IPV6_AUTOCONF=no
-+                   IPV6_FORCE_ACCEPT_RA=no
-                    IPV6_DEFAULTGW=2001:db8::1
-                    NETMASK=255.255.255.0
-                    NM_CONTROLLED=no
--- 
-1.8.3.1
-
diff --git a/SOURCES/ci-Fix-home-permissions-modified-by-ssh-module-SC-338-9.patch b/SOURCES/ci-Fix-home-permissions-modified-by-ssh-module-SC-338-9.patch
new file mode 100644
index 0000000..6a9cfcc
--- /dev/null
+++ b/SOURCES/ci-Fix-home-permissions-modified-by-ssh-module-SC-338-9.patch
@@ -0,0 +1,262 @@
+From 71989367e7a634fdd2af8ef58473975e0ef60464 Mon Sep 17 00:00:00 2001
+From: Emanuele Giuseppe Esposito <eesposit@redhat.com>
+Date: Sat, 21 Aug 2021 13:53:27 +0200
+Subject: [PATCH] Fix home permissions modified by ssh module (SC-338) (#984)
+
+RH-Author: Emanuele Giuseppe Esposito <eesposit@redhat.com>
+RH-MergeRequest: 29: Fix home permissions modified by ssh module (SC-338) (#984)
+RH-Commit: [1/1] c409f2609b1d7e024eba77b55a196a4cafadd1d7 (eesposit/cloud-init)
+RH-Bugzilla: 1995840
+RH-Acked-by: Mohamed Gamal Morsy <mmorsy@redhat.com>
+RH-Acked-by: Eduardo Otubo <otubo@redhat.com>
+
+TESTED: By me and QA
+BREW: 39178090
+
+Fix home permissions modified by ssh module (SC-338) (#984)
+
+commit 7d3f5d750f6111c2716143364ea33486df67c927
+Author: James Falcon <therealfalcon@gmail.com>
+Date:   Fri Aug 20 17:09:49 2021 -0500
+
+    Fix home permissions modified by ssh module (SC-338) (#984)
+
+    Fix home permissions modified by ssh module
+
+    In #956, we updated the file and directory permissions for keys not in
+    the user's home directory. We also unintentionally modified the
+    permissions within the home directory as well. These should not change,
+    and this commit changes that back.
+
+    LP: #1940233
+
+Signed-off-by: Emanuele Giuseppe Esposito <eesposit@redhat.com>
+---
+ cloudinit/ssh_util.py                         |  35 ++++-
+ .../modules/test_ssh_keysfile.py              | 132 +++++++++++++++---
+ 2 files changed, 146 insertions(+), 21 deletions(-)
+
+diff --git a/cloudinit/ssh_util.py b/cloudinit/ssh_util.py
+index b8a3c8f7..9ccadf09 100644
+--- a/cloudinit/ssh_util.py
++++ b/cloudinit/ssh_util.py
+@@ -321,23 +321,48 @@ def check_create_path(username, filename, strictmodes):
+         home_folder = os.path.dirname(user_pwent.pw_dir)
+         for directory in directories:
+             parent_folder += "/" + directory
+-            if home_folder.startswith(parent_folder):
++
++            # security check, disallow symlinks in the AuthorizedKeysFile path.
++            if os.path.islink(parent_folder):
++                LOG.debug(
++                    "Invalid directory. Symlink exists in path: %s",
++                    parent_folder)
++                return False
++
++            if os.path.isfile(parent_folder):
++                LOG.debug(
++                    "Invalid directory. File exists in path: %s",
++                    parent_folder)
++                return False
++
++            if (home_folder.startswith(parent_folder) or
++                    parent_folder == user_pwent.pw_dir):
+                 continue
+ 
+-            if not os.path.isdir(parent_folder):
++            if not os.path.exists(parent_folder):
+                 # directory does not exist, and permission so far are good:
+                 # create the directory, and make it accessible by everyone
+                 # but owned by root, as it might be used by many users.
+                 with util.SeLinuxGuard(parent_folder):
+-                    os.makedirs(parent_folder, mode=0o755, exist_ok=True)
+-                    util.chownbyid(parent_folder, root_pwent.pw_uid,
+-                                   root_pwent.pw_gid)
++                    mode = 0o755
++                    uid = root_pwent.pw_uid
++                    gid = root_pwent.pw_gid
++                    if parent_folder.startswith(user_pwent.pw_dir):
++                        mode = 0o700
++                        uid = user_pwent.pw_uid
++                        gid = user_pwent.pw_gid
++                    os.makedirs(parent_folder, mode=mode, exist_ok=True)
++                    util.chownbyid(parent_folder, uid, gid)
+ 
+             permissions = check_permissions(username, parent_folder,
+                                             filename, False, strictmodes)
+             if not permissions:
+                 return False
+ 
++        if os.path.islink(filename) or os.path.isdir(filename):
++            LOG.debug("%s is not a file!", filename)
++            return False
++
+         # check the file
+         if not os.path.exists(filename):
+             # if file does not exist: we need to create it, since the
+diff --git a/tests/integration_tests/modules/test_ssh_keysfile.py b/tests/integration_tests/modules/test_ssh_keysfile.py
+index f82d7649..3159feb9 100644
+--- a/tests/integration_tests/modules/test_ssh_keysfile.py
++++ b/tests/integration_tests/modules/test_ssh_keysfile.py
+@@ -10,10 +10,10 @@ TEST_USER1_KEYS = get_test_rsa_keypair('test1')
+ TEST_USER2_KEYS = get_test_rsa_keypair('test2')
+ TEST_DEFAULT_KEYS = get_test_rsa_keypair('test3')
+ 
+-USERDATA = """\
++_USERDATA = """\
+ #cloud-config
+ bootcmd:
+- - sed -i 's;#AuthorizedKeysFile.*;AuthorizedKeysFile /etc/ssh/authorized_keys %h/.ssh/authorized_keys2;' /etc/ssh/sshd_config
++ - {bootcmd}
+ ssh_authorized_keys:
+  - {default}
+ users:
+@@ -24,27 +24,17 @@ users:
+ - name: test_user2
+   ssh_authorized_keys:
+     - {user2}
+-""".format(  # noqa: E501
++""".format(
++    bootcmd='{bootcmd}',
+     default=TEST_DEFAULT_KEYS.public_key,
+     user1=TEST_USER1_KEYS.public_key,
+     user2=TEST_USER2_KEYS.public_key,
+ )
+ 
+ 
+-@pytest.mark.ubuntu
+-@pytest.mark.user_data(USERDATA)
+-def test_authorized_keys(client: IntegrationInstance):
+-    expected_keys = [
+-        ('test_user1', '/home/test_user1/.ssh/authorized_keys2',
+-         TEST_USER1_KEYS),
+-        ('test_user2', '/home/test_user2/.ssh/authorized_keys2',
+-         TEST_USER2_KEYS),
+-        ('ubuntu', '/home/ubuntu/.ssh/authorized_keys2',
+-         TEST_DEFAULT_KEYS),
+-        ('root', '/root/.ssh/authorized_keys2', TEST_DEFAULT_KEYS),
+-    ]
+-
++def common_verify(client, expected_keys):
+     for user, filename, keys in expected_keys:
++        # Ensure key is in the key file
+         contents = client.read_from_file(filename)
+         if user in ['ubuntu', 'root']:
+             # Our personal public key gets added by pycloudlib
+@@ -83,3 +73,113 @@ def test_authorized_keys(client: IntegrationInstance):
+                     look_for_keys=False,
+                     allow_agent=False,
+                 )
++
++        # Ensure we haven't messed with any /home permissions
++        # See LP: #1940233
++        home_dir = '/home/{}'.format(user)
++        home_perms = '755'
++        if user == 'root':
++            home_dir = '/root'
++            home_perms = '700'
++        assert '{} {}'.format(user, home_perms) == client.execute(
++            'stat -c "%U %a" {}'.format(home_dir)
++        )
++        if client.execute("test -d {}/.ssh".format(home_dir)).ok:
++            assert '{} 700'.format(user) == client.execute(
++                'stat -c "%U %a" {}/.ssh'.format(home_dir)
++            )
++        assert '{} 600'.format(user) == client.execute(
++            'stat -c "%U %a" {}'.format(filename)
++        )
++
++        # Also ensure ssh-keygen works as expected
++        client.execute('mkdir {}/.ssh'.format(home_dir))
++        assert client.execute(
++            "ssh-keygen -b 2048 -t rsa -f {}/.ssh/id_rsa -q -N ''".format(
++                home_dir)
++        ).ok
++        assert client.execute('test -f {}/.ssh/id_rsa'.format(home_dir))
++        assert client.execute('test -f {}/.ssh/id_rsa.pub'.format(home_dir))
++
++    assert 'root 755' == client.execute('stat -c "%U %a" /home')
++
++
++DEFAULT_KEYS_USERDATA = _USERDATA.format(bootcmd='""')
++
++
++@pytest.mark.ubuntu
++@pytest.mark.user_data(DEFAULT_KEYS_USERDATA)
++def test_authorized_keys_default(client: IntegrationInstance):
++    expected_keys = [
++        ('test_user1', '/home/test_user1/.ssh/authorized_keys',
++         TEST_USER1_KEYS),
++        ('test_user2', '/home/test_user2/.ssh/authorized_keys',
++         TEST_USER2_KEYS),
++        ('ubuntu', '/home/ubuntu/.ssh/authorized_keys',
++         TEST_DEFAULT_KEYS),
++        ('root', '/root/.ssh/authorized_keys', TEST_DEFAULT_KEYS),
++    ]
++    common_verify(client, expected_keys)
++
++
++AUTHORIZED_KEYS2_USERDATA = _USERDATA.format(bootcmd=(
++    "sed -i 's;#AuthorizedKeysFile.*;AuthorizedKeysFile "
++    "/etc/ssh/authorized_keys %h/.ssh/authorized_keys2;' "
++    "/etc/ssh/sshd_config"))
++
++
++@pytest.mark.ubuntu
++@pytest.mark.user_data(AUTHORIZED_KEYS2_USERDATA)
++def test_authorized_keys2(client: IntegrationInstance):
++    expected_keys = [
++        ('test_user1', '/home/test_user1/.ssh/authorized_keys2',
++         TEST_USER1_KEYS),
++        ('test_user2', '/home/test_user2/.ssh/authorized_keys2',
++         TEST_USER2_KEYS),
++        ('ubuntu', '/home/ubuntu/.ssh/authorized_keys2',
++         TEST_DEFAULT_KEYS),
++        ('root', '/root/.ssh/authorized_keys2', TEST_DEFAULT_KEYS),
++    ]
++    common_verify(client, expected_keys)
++
++
++NESTED_KEYS_USERDATA = _USERDATA.format(bootcmd=(
++    "sed -i 's;#AuthorizedKeysFile.*;AuthorizedKeysFile "
++    "/etc/ssh/authorized_keys %h/foo/bar/ssh/keys;' "
++    "/etc/ssh/sshd_config"))
++
++
++@pytest.mark.ubuntu
++@pytest.mark.user_data(NESTED_KEYS_USERDATA)
++def test_nested_keys(client: IntegrationInstance):
++    expected_keys = [
++        ('test_user1', '/home/test_user1/foo/bar/ssh/keys',
++         TEST_USER1_KEYS),
++        ('test_user2', '/home/test_user2/foo/bar/ssh/keys',
++         TEST_USER2_KEYS),
++        ('ubuntu', '/home/ubuntu/foo/bar/ssh/keys',
++         TEST_DEFAULT_KEYS),
++        ('root', '/root/foo/bar/ssh/keys', TEST_DEFAULT_KEYS),
++    ]
++    common_verify(client, expected_keys)
++
++
++EXTERNAL_KEYS_USERDATA = _USERDATA.format(bootcmd=(
++    "sed -i 's;#AuthorizedKeysFile.*;AuthorizedKeysFile "
++    "/etc/ssh/authorized_keys /etc/ssh/authorized_keys/%u/keys;' "
++    "/etc/ssh/sshd_config"))
++
++
++@pytest.mark.ubuntu
++@pytest.mark.user_data(EXTERNAL_KEYS_USERDATA)
++def test_external_keys(client: IntegrationInstance):
++    expected_keys = [
++        ('test_user1', '/etc/ssh/authorized_keys/test_user1/keys',
++         TEST_USER1_KEYS),
++        ('test_user2', '/etc/ssh/authorized_keys/test_user2/keys',
++         TEST_USER2_KEYS),
++        ('ubuntu', '/etc/ssh/authorized_keys/ubuntu/keys',
++         TEST_DEFAULT_KEYS),
++        ('root', '/etc/ssh/authorized_keys/root/keys', TEST_DEFAULT_KEYS),
++    ]
++    common_verify(client, expected_keys)
+-- 
+2.27.0
+
diff --git a/SOURCES/ci-Fix-unit-failure-of-cloud-final.service-if-NetworkMa.patch b/SOURCES/ci-Fix-unit-failure-of-cloud-final.service-if-NetworkMa.patch
deleted file mode 100644
index aeaa342..0000000
--- a/SOURCES/ci-Fix-unit-failure-of-cloud-final.service-if-NetworkMa.patch
+++ /dev/null
@@ -1,61 +0,0 @@
-From d3889c4645a1319c3d677006164b618ee53f4c8b Mon Sep 17 00:00:00 2001
-From: Eduardo Otubo <otubo@redhat.com>
-Date: Mon, 7 Dec 2020 14:23:22 +0100
-Subject: [PATCH 3/4] Fix unit failure of cloud-final.service if NetworkManager
- was not present.
-
-RH-Author: Eduardo Terrell Ferrari Otubo (eterrell)
-RH-MergeRequest: 27: Fix unit failure of cloud-final.service if NetworkManager was not present.
-RH-Commit: [1/1] 3c65a2cca140fff48df1ef32919e3cb035506a2b (eterrell/cloud-init)
-RH-Bugzilla: 1898943
-
-cloud-final.service would fail if NetworkManager was not installed.
-
-journal -u cloud-final.service would show:
-
-   cloud-init[5328]: Cloud-init v. 19.4 finished at ...
-   echo[5346]: try restart NetworkManager.service
-   systemctl[5349]: Failed to reload-or-try-restart
-       NetworkManager.service: Unit not found.
-   systemd[1]: cloud-final.service: control process exited,
-       code=exited status=5
-   systemd[1]: Failed to start Execute cloud user/final scripts.
-   systemd[1]: Unit cloud-final.service entered failed state.
-   systemd[1]: cloud-final.service failed.
-
-The change here is to only attempt to restart NetworkManager if it is
-present, and its SubState is 'running'.
-
-The multi-line shell in a systemd unit is less than ideal, but I'm not
-aware of any other way of conditionally doing this.
-
-Note that both of 'try-reload-or-restart' and 'reload-or-try-restart'
-will fail if the service is not present.  So this would also affect rhel
-8 systems that do not use NetworkManager.
-
-Signed-off-by: Eduardo Otubo <otubo@redhat.com>
----
- rhel/systemd/cloud-final.service | 7 +++++--
- 1 file changed, 5 insertions(+), 2 deletions(-)
-
-diff --git a/rhel/systemd/cloud-final.service b/rhel/systemd/cloud-final.service
-index 05add077..e281c0cf 100644
---- a/rhel/systemd/cloud-final.service
-+++ b/rhel/systemd/cloud-final.service
-@@ -11,8 +11,11 @@ ExecStart=/usr/bin/cloud-init modules --mode=final
- RemainAfterExit=yes
- TimeoutSec=0
- KillMode=process
--ExecStartPost=/bin/echo "trying to reload or restart NetworkManager.service"
--ExecStartPost=/usr/bin/systemctl try-reload-or-restart NetworkManager.service
-+# Restart NetworkManager if it is present and running.
-+ExecStartPost=/bin/sh -c 'u=NetworkManager.service; \
-+ out=$(systemctl show --property=SubState $u) || exit; \
-+ [ "$out" = "SubState=running" ] || exit 0; \
-+ systemctl reload-or-try-restart $u'
- 
- # Output needs to appear in instance console output
- StandardOutput=journal+console
--- 
-2.18.4
-
diff --git a/SOURCES/ci-Missing-IPV6_AUTOCONF-no-to-render-sysconfig-dhcp6-s.patch b/SOURCES/ci-Missing-IPV6_AUTOCONF-no-to-render-sysconfig-dhcp6-s.patch
deleted file mode 100644
index 3860cd1..0000000
--- a/SOURCES/ci-Missing-IPV6_AUTOCONF-no-to-render-sysconfig-dhcp6-s.patch
+++ /dev/null
@@ -1,49 +0,0 @@
-From 15852ea6958c18e3830aa9244b36cd0decc93b95 Mon Sep 17 00:00:00 2001
-From: Eduardo Otubo <otubo@redhat.com>
-Date: Thu, 7 Jan 2021 16:51:30 +0100
-Subject: [PATCH] Missing IPV6_AUTOCONF=no to render sysconfig dhcp6 stateful
- on RHEL (#753)
-
-RH-Author: Eduardo Terrell Ferrari Otubo (eterrell)
-RH-MergeRequest: 29: Missing IPV6_AUTOCONF=no to render sysconfig dhcp6 stateful on RHEL (#753)
-RH-Commit: [1/1] 46943f83071d243bcc61f9d987b4fe7d9cf98596 (eterrell/cloud-init)
-RH-Bugzilla: 1859695
-
-IPV6_AUTOCONF needs to be set to 'no' on RHEL so NetworkManager can
-properly acquire ipv6 address.
-
-rhbz: #1859695
-
-Signed-off-by: Eduardo Otubo <otubo@redhat.com>
----
- cloudinit/net/sysconfig.py  | 1 +
- tests/unittests/test_net.py | 1 +
- 2 files changed, 2 insertions(+)
-
-diff --git a/cloudinit/net/sysconfig.py b/cloudinit/net/sysconfig.py
-index 94801a93..1793977d 100644
---- a/cloudinit/net/sysconfig.py
-+++ b/cloudinit/net/sysconfig.py
-@@ -397,6 +397,7 @@ class Renderer(renderer.Renderer):
-                     iface_cfg['BOOTPROTO'] = 'dhcp'
-                     iface_cfg['DHCPV6C'] = True
-                     iface_cfg['IPV6INIT'] = True
-+                    iface_cfg['IPV6_AUTOCONF'] = False
-                 else:
-                     iface_cfg['IPV6INIT'] = True
-                     # Configure network settings using DHCPv6
-diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py
-index bcd261db..844d5ba8 100644
---- a/tests/unittests/test_net.py
-+++ b/tests/unittests/test_net.py
-@@ -1363,6 +1363,7 @@ NETWORK_CONFIGS = {
-             DEVICE=iface0
-             DHCPV6C=yes
-             IPV6INIT=yes
-+            IPV6_AUTOCONF=no
-             IPV6_FORCE_ACCEPT_RA=yes
-             DEVICE=iface0
-             NM_CONTROLLED=no
--- 
-2.18.4
-
diff --git a/SOURCES/ci-Revert-ssh_util-handle-non-default-AuthorizedKeysFil.patch b/SOURCES/ci-Revert-ssh_util-handle-non-default-AuthorizedKeysFil.patch
deleted file mode 100644
index a7f4117..0000000
--- a/SOURCES/ci-Revert-ssh_util-handle-non-default-AuthorizedKeysFil.patch
+++ /dev/null
@@ -1,80 +0,0 @@
-From 4dde2a9bed58aba13c730bf4a7314b21038d7a31 Mon Sep 17 00:00:00 2001
-From: Eduardo Otubo <otubo@redhat.com>
-Date: Mon, 25 Jan 2021 16:24:29 +0100
-Subject: [PATCH 2/2] Revert "ssh_util: handle non-default AuthorizedKeysFile
- config (#586)" (#775)
-
-RH-Author: Eduardo Terrell Ferrari Otubo (eterrell)
-RH-MergeRequest: 38: Revert "ssh_util: handle non-default AuthorizedKeysFile config (#586)" (#775)
-RH-Commit: [1/1] aec2860c773ad1921f3949dc622543e81860c5bf (eterrell/cloud-init)
-RH-Bugzilla: 1919972
-
-commit cdc5b81f33aee0ed3ef1ae239e5cec1906d0178a
-Author: Daniel Watkins <oddbloke@ubuntu.com>
-Date:   Tue Jan 19 12:23:23 2021 -0500
-
-    Revert "ssh_util: handle non-default AuthorizedKeysFile config (#586)" (#775)
-
-    This reverts commit b0e73814db4027dba0b7dc0282e295b7f653325c.
-
-Signed-off-by: Eduardo Otubo <otubo@redhat.com>
----
- cloudinit/ssh_util.py           | 6 +++---
- tests/unittests/test_sshutil.py | 6 +++---
- 2 files changed, 6 insertions(+), 6 deletions(-)
-
-diff --git a/cloudinit/ssh_util.py b/cloudinit/ssh_util.py
-index d5113996..c08042d6 100644
---- a/cloudinit/ssh_util.py
-+++ b/cloudinit/ssh_util.py
-@@ -262,13 +262,13 @@ def extract_authorized_keys(username, sshd_cfg_file=DEF_SSHD_CFG):
- 
-         except (IOError, OSError):
-             # Give up and use a default key filename
--            auth_key_fns.append(default_authorizedkeys_file)
-+            auth_key_fns[0] = default_authorizedkeys_file
-             util.logexc(LOG, "Failed extracting 'AuthorizedKeysFile' in SSH "
-                         "config from %r, using 'AuthorizedKeysFile' file "
-                         "%r instead", DEF_SSHD_CFG, auth_key_fns[0])
- 
--    # always store all the keys in the first file configured on sshd_config
--    return (auth_key_fns[0], parse_authorized_keys(auth_key_fns))
-+    # always store all the keys in the user's private file
-+    return (default_authorizedkeys_file, parse_authorized_keys(auth_key_fns))
- 
- 
- def setup_user_keys(keys, username, options=None):
-diff --git a/tests/unittests/test_sshutil.py b/tests/unittests/test_sshutil.py
-index 88a111e3..fd1d1bac 100644
---- a/tests/unittests/test_sshutil.py
-+++ b/tests/unittests/test_sshutil.py
-@@ -593,7 +593,7 @@ class TestMultipleSshAuthorizedKeysFile(test_helpers.CiTestCase):
-             fpw.pw_name, sshd_config)
-         content = ssh_util.update_authorized_keys(auth_key_entries, [])
- 
--        self.assertEqual(authorized_keys, auth_key_fn)
-+        self.assertEqual("%s/.ssh/authorized_keys" % fpw.pw_dir, auth_key_fn)
-         self.assertTrue(VALID_CONTENT['rsa'] in content)
-         self.assertTrue(VALID_CONTENT['dsa'] in content)
- 
-@@ -610,7 +610,7 @@ class TestMultipleSshAuthorizedKeysFile(test_helpers.CiTestCase):
-         sshd_config = self.tmp_path('sshd_config')
-         util.write_file(
-             sshd_config,
--            "AuthorizedKeysFile %s %s" % (user_keys, authorized_keys)
-+            "AuthorizedKeysFile %s %s" % (authorized_keys, user_keys)
-         )
- 
-         (auth_key_fn, auth_key_entries) = ssh_util.extract_authorized_keys(
-@@ -618,7 +618,7 @@ class TestMultipleSshAuthorizedKeysFile(test_helpers.CiTestCase):
-         )
-         content = ssh_util.update_authorized_keys(auth_key_entries, [])
- 
--        self.assertEqual(user_keys, auth_key_fn)
-+        self.assertEqual("%s/.ssh/authorized_keys" % fpw.pw_dir, auth_key_fn)
-         self.assertTrue(VALID_CONTENT['rsa'] in content)
-         self.assertTrue(VALID_CONTENT['dsa'] in content)
- 
--- 
-2.18.4
-
diff --git a/SOURCES/ci-Stop-copying-ssh-system-keys-and-check-folder-permis.patch b/SOURCES/ci-Stop-copying-ssh-system-keys-and-check-folder-permis.patch
new file mode 100644
index 0000000..e46b52b
--- /dev/null
+++ b/SOURCES/ci-Stop-copying-ssh-system-keys-and-check-folder-permis.patch
@@ -0,0 +1,1385 @@
+From 3b68aff3b7b1dc567ef6721a269c2d4e054b729f Mon Sep 17 00:00:00 2001
+From: Emanuele Giuseppe Esposito <eesposit@redhat.com>
+Date: Mon, 9 Aug 2021 23:41:44 +0200
+Subject: [PATCH] Stop copying ssh system keys and check folder permissions
+ (#956)
+
+RH-Author: Emanuele Giuseppe Esposito <eesposit@redhat.com>
+RH-MergeRequest: 28: Stop copying ssh system keys and check folder permissions (#956)
+RH-Commit: [1/1] 7cada613be82f2f525ee56b86ef9f71edf40d2ef (eesposit/cloud-init)
+RH-Bugzilla: 1862967
+RH-Acked-by: Miroslav Rezanina <mrezanin@redhat.com>
+RH-Acked-by: Eduardo Otubo <otubo@redhat.com>
+
+TESTED: By me and QA
+BREW: 38818284
+
+This is a continuation of previous MR 25 and upstream PR #937.
+There were still issues when using non-standard file paths like
+/etc/ssh/userkeys/%u or /etc/ssh/authorized_keys, and the choice
+of storing the keys of all authorized_keys files into a single
+one was not ideal. This fix modifies cloudinit to support
+all different cases of authorized_keys file locations, and
+picks a user-specific file where to copy the new keys that
+complies with ssh permissions.
+
+commit 00dbaf1e9ab0e59d81662f0f3561897bef499a3f
+Author: Emanuele Giuseppe Esposito <eesposit@redhat.com>
+Date:   Mon Aug 9 16:49:56 2021 +0200
+
+    Stop copying ssh system keys and check folder permissions (#956)
+
+    In /etc/ssh/sshd_config, it is possible to define a custom
+    authorized_keys file that will contain the keys allowed to access the
+    machine via the AuthorizedKeysFile option. Cloudinit is able to add
+    user-specific keys to the existing ones, but we need to be careful on
+    which of the authorized_keys files listed to pick.
+    Chosing a file that is shared by all user will cause security
+    issues, because the owner of that key can then access also other users.
+
+    We therefore pick an authorized_keys file only if it satisfies the
+    following conditions:
+    1. it is not a "global" file, ie it must be defined in
+       AuthorizedKeysFile with %u, %h or be in  /home/<user>. This avoids
+       security issues.
+    2. it must comply with ssh permission requirements, otherwise the ssh
+       agent won't use that file.
+
+    If it doesn't meet either of those conditions, write to
+    ~/.ssh/authorized_keys
+
+    We also need to consider the case when the chosen authorized_keys file
+    does not exist. In this case, the existing behavior of cloud-init is
+    to create the new file. We therefore need to be sure that the file
+    complies with ssh permissions too, by setting:
+    - the actual file to permission 600, and owned by the user
+    - the directories in the path that do not exist must be root owned and
+      with permission 755.
+
+Signed-off-by: Emanuele Giuseppe Esposito <eesposit@redhat.com>
+---
+ cloudinit/ssh_util.py           | 133 ++++-
+ cloudinit/util.py               |  51 +-
+ tests/unittests/test_sshutil.py | 952 +++++++++++++++++++++++++-------
+ 3 files changed, 920 insertions(+), 216 deletions(-)
+
+diff --git a/cloudinit/ssh_util.py b/cloudinit/ssh_util.py
+index 89057262..b8a3c8f7 100644
+--- a/cloudinit/ssh_util.py
++++ b/cloudinit/ssh_util.py
+@@ -249,6 +249,113 @@ def render_authorizedkeysfile_paths(value, homedir, username):
+     return rendered
+ 
+ 
++# Inspired from safe_path() in openssh source code (misc.c).
++def check_permissions(username, current_path, full_path, is_file, strictmodes):
++    """Check if the file/folder in @current_path has the right permissions.
++
++    We need to check that:
++    1. If StrictMode is enabled, the owner is either root or the user
++    2. the user can access the file/folder, otherwise ssh won't use it
++    3. If StrictMode is enabled, no write permission is given to group
++       and world users (022)
++    """
++
++    # group/world can only execute the folder (access)
++    minimal_permissions = 0o711
++    if is_file:
++        # group/world can only read the file
++        minimal_permissions = 0o644
++
++    # 1. owner must be either root or the user itself
++    owner = util.get_owner(current_path)
++    if strictmodes and owner != username and owner != "root":
++        LOG.debug("Path %s in %s must be own by user %s or"
++                  " by root, but instead is own by %s. Ignoring key.",
++                  current_path, full_path, username, owner)
++        return False
++
++    parent_permission = util.get_permissions(current_path)
++    # 2. the user can access the file/folder, otherwise ssh won't use it
++    if owner == username:
++        # need only the owner permissions
++        minimal_permissions &= 0o700
++    else:
++        group_owner = util.get_group(current_path)
++        user_groups = util.get_user_groups(username)
++
++        if group_owner in user_groups:
++            # need only the group permissions
++            minimal_permissions &= 0o070
++        else:
++            # need only the world permissions
++            minimal_permissions &= 0o007
++
++    if parent_permission & minimal_permissions == 0:
++        LOG.debug("Path %s in %s must be accessible by user %s,"
++                  " check its permissions",
++                  current_path, full_path, username)
++        return False
++
++    # 3. no write permission (w) is given to group and world users (022)
++    # Group and world user can still have +rx.
++    if strictmodes and parent_permission & 0o022 != 0:
++        LOG.debug("Path %s in %s must not give write"
++                  "permission to group or world users. Ignoring key.",
++                  current_path, full_path)
++        return False
++
++    return True
++
++
++def check_create_path(username, filename, strictmodes):
++    user_pwent = users_ssh_info(username)[1]
++    root_pwent = users_ssh_info("root")[1]
++    try:
++        # check the directories first
++        directories = filename.split("/")[1:-1]
++
++        # scan in order, from root to file name
++        parent_folder = ""
++        # this is to comply also with unit tests, and
++        # strange home directories
++        home_folder = os.path.dirname(user_pwent.pw_dir)
++        for directory in directories:
++            parent_folder += "/" + directory
++            if home_folder.startswith(parent_folder):
++                continue
++
++            if not os.path.isdir(parent_folder):
++                # directory does not exist, and permission so far are good:
++                # create the directory, and make it accessible by everyone
++                # but owned by root, as it might be used by many users.
++                with util.SeLinuxGuard(parent_folder):
++                    os.makedirs(parent_folder, mode=0o755, exist_ok=True)
++                    util.chownbyid(parent_folder, root_pwent.pw_uid,
++                                   root_pwent.pw_gid)
++
++            permissions = check_permissions(username, parent_folder,
++                                            filename, False, strictmodes)
++            if not permissions:
++                return False
++
++        # check the file
++        if not os.path.exists(filename):
++            # if file does not exist: we need to create it, since the
++            # folders at this point exist and have right permissions
++            util.write_file(filename, '', mode=0o600, ensure_dir_exists=True)
++            util.chownbyid(filename, user_pwent.pw_uid, user_pwent.pw_gid)
++
++        permissions = check_permissions(username, filename,
++                                        filename, True, strictmodes)
++        if not permissions:
++            return False
++    except (IOError, OSError) as e:
++        util.logexc(LOG, str(e))
++        return False
++
++    return True
++
++
+ def extract_authorized_keys(username, sshd_cfg_file=DEF_SSHD_CFG):
+     (ssh_dir, pw_ent) = users_ssh_info(username)
+     default_authorizedkeys_file = os.path.join(ssh_dir, 'authorized_keys')
+@@ -259,6 +366,7 @@ def extract_authorized_keys(username, sshd_cfg_file=DEF_SSHD_CFG):
+             ssh_cfg = parse_ssh_config_map(sshd_cfg_file)
+             key_paths = ssh_cfg.get("authorizedkeysfile",
+                                     "%h/.ssh/authorized_keys")
++            strictmodes = ssh_cfg.get("strictmodes", "yes")
+             auth_key_fns = render_authorizedkeysfile_paths(
+                 key_paths, pw_ent.pw_dir, username)
+ 
+@@ -269,31 +377,31 @@ def extract_authorized_keys(username, sshd_cfg_file=DEF_SSHD_CFG):
+                         "config from %r, using 'AuthorizedKeysFile' file "
+                         "%r instead", DEF_SSHD_CFG, auth_key_fns[0])
+ 
+-    # check if one of the keys is the user's one
++    # check if one of the keys is the user's one and has the right permissions
+     for key_path, auth_key_fn in zip(key_paths.split(), auth_key_fns):
+         if any([
+             '%u' in key_path,
+             '%h' in key_path,
+             auth_key_fn.startswith('{}/'.format(pw_ent.pw_dir))
+         ]):
+-            user_authorizedkeys_file = auth_key_fn
++            permissions_ok = check_create_path(username, auth_key_fn,
++                                               strictmodes == "yes")
++            if permissions_ok:
++                user_authorizedkeys_file = auth_key_fn
++                break
+ 
+     if user_authorizedkeys_file != default_authorizedkeys_file:
+         LOG.debug(
+             "AuthorizedKeysFile has an user-specific authorized_keys, "
+             "using %s", user_authorizedkeys_file)
+ 
+-    # always store all the keys in the user's private file
+-    return (user_authorizedkeys_file, parse_authorized_keys(auth_key_fns))
++    return (
++        user_authorizedkeys_file,
++        parse_authorized_keys([user_authorizedkeys_file])
++    )
+ 
+ 
+ def setup_user_keys(keys, username, options=None):
+-    # Make sure the users .ssh dir is setup accordingly
+-    (ssh_dir, pwent) = users_ssh_info(username)
+-    if not os.path.isdir(ssh_dir):
+-        util.ensure_dir(ssh_dir, mode=0o700)
+-        util.chownbyid(ssh_dir, pwent.pw_uid, pwent.pw_gid)
+-
+     # Turn the 'update' keys given into actual entries
+     parser = AuthKeyLineParser()
+     key_entries = []
+@@ -302,11 +410,10 @@ def setup_user_keys(keys, username, options=None):
+ 
+     # Extract the old and make the new
+     (auth_key_fn, auth_key_entries) = extract_authorized_keys(username)
++    ssh_dir = os.path.dirname(auth_key_fn)
+     with util.SeLinuxGuard(ssh_dir, recursive=True):
+         content = update_authorized_keys(auth_key_entries, key_entries)
+-        util.ensure_dir(os.path.dirname(auth_key_fn), mode=0o700)
+-        util.write_file(auth_key_fn, content, mode=0o600)
+-        util.chownbyid(auth_key_fn, pwent.pw_uid, pwent.pw_gid)
++        util.write_file(auth_key_fn, content, preserve_mode=True)
+ 
+ 
+ class SshdConfigLine(object):
+diff --git a/cloudinit/util.py b/cloudinit/util.py
+index 4e0a72db..343976ad 100644
+--- a/cloudinit/util.py
++++ b/cloudinit/util.py
+@@ -35,6 +35,7 @@ from base64 import b64decode, b64encode
+ from errno import ENOENT
+ from functools import lru_cache
+ from urllib import parse
++from typing import List
+ 
+ from cloudinit import importer
+ from cloudinit import log as logging
+@@ -1830,6 +1831,53 @@ def chmod(path, mode):
+             os.chmod(path, real_mode)
+ 
+ 
++def get_permissions(path: str) -> int:
++    """
++    Returns the octal permissions of the file/folder pointed by the path,
++    encoded as an int.
++
++    @param path: The full path of the file/folder.
++    """
++
++    return stat.S_IMODE(os.stat(path).st_mode)
++
++
++def get_owner(path: str) -> str:
++    """
++    Returns the owner of the file/folder pointed by the path.
++
++    @param path: The full path of the file/folder.
++    """
++    st = os.stat(path)
++    return pwd.getpwuid(st.st_uid).pw_name
++
++
++def get_group(path: str) -> str:
++    """
++    Returns the group of the file/folder pointed by the path.
++
++    @param path: The full path of the file/folder.
++    """
++    st = os.stat(path)
++    return grp.getgrgid(st.st_gid).gr_name
++
++
++def get_user_groups(username: str) -> List[str]:
++    """
++    Returns a list of all groups to which the user belongs
++
++    @param username: the user we want to check
++    """
++    groups = []
++    for group in grp.getgrall():
++        if username in group.gr_mem:
++            groups.append(group.gr_name)
++
++    gid = pwd.getpwnam(username).pw_gid
++    groups.append(grp.getgrgid(gid).gr_name)
++    return groups
++
++
+ def write_file(
+     filename,
+     content,
+@@ -1856,8 +1904,7 @@ def write_file(
+ 
+     if preserve_mode:
+         try:
+-            file_stat = os.stat(filename)
+-            mode = stat.S_IMODE(file_stat.st_mode)
++            mode = get_permissions(filename)
+         except OSError:
+             pass
+ 
+diff --git a/tests/unittests/test_sshutil.py b/tests/unittests/test_sshutil.py
+index bcb8044f..a66788bf 100644
+--- a/tests/unittests/test_sshutil.py
++++ b/tests/unittests/test_sshutil.py
+@@ -1,6 +1,9 @@
+ # This file is part of cloud-init. See LICENSE file for license information.
+ 
++import os
++
+ from collections import namedtuple
++from functools import partial
+ from unittest.mock import patch
+ 
+ from cloudinit import ssh_util
+@@ -8,13 +11,48 @@ from cloudinit.tests import helpers as test_helpers
+ from cloudinit import util
+ 
+ # https://stackoverflow.com/questions/11351032/
+-FakePwEnt = namedtuple(
+-    'FakePwEnt',
+-    ['pw_dir', 'pw_gecos', 'pw_name', 'pw_passwd', 'pw_shell', 'pwd_uid'])
++FakePwEnt = namedtuple('FakePwEnt', [
++    'pw_name',
++    'pw_passwd',
++    'pw_uid',
++    'pw_gid',
++    'pw_gecos',
++    'pw_dir',
++    'pw_shell',
++])
+ FakePwEnt.__new__.__defaults__ = tuple(
+     "UNSET_%s" % n for n in FakePwEnt._fields)
+ 
+ 
++def mock_get_owner(updated_permissions, value):
++    try:
++        return updated_permissions[value][0]
++    except ValueError:
++        return util.get_owner(value)
++
++
++def mock_get_group(updated_permissions, value):
++    try:
++        return updated_permissions[value][1]
++    except ValueError:
++        return util.get_group(value)
++
++
++def mock_get_user_groups(username):
++    return username
++
++
++def mock_get_permissions(updated_permissions, value):
++    try:
++        return updated_permissions[value][2]
++    except ValueError:
++        return util.get_permissions(value)
++
++
++def mock_getpwnam(users, username):
++    return users[username]
++
++
+ # Do not use these public keys, most of them are fetched from
+ # the testdata for OpenSSH, and their private keys are available
+ # https://github.com/openssh/openssh-portable/tree/master/regress/unittests/sshkey/testdata
+@@ -552,12 +590,30 @@ class TestBasicAuthorizedKeyParse(test_helpers.CiTestCase):
+             ssh_util.render_authorizedkeysfile_paths(
+                 "/opt/%u/keys", "/home/bobby", "bobby"))
+ 
++    def test_user_file(self):
++        self.assertEqual(
++            ["/opt/bobby"],
++            ssh_util.render_authorizedkeysfile_paths(
++                "/opt/%u", "/home/bobby", "bobby"))
++
++    def test_user_file2(self):
++        self.assertEqual(
++            ["/opt/bobby/bobby"],
++            ssh_util.render_authorizedkeysfile_paths(
++                "/opt/%u/%u", "/home/bobby", "bobby"))
++
+     def test_multiple(self):
+         self.assertEqual(
+             ["/keys/path1", "/keys/path2"],
+             ssh_util.render_authorizedkeysfile_paths(
+                 "/keys/path1 /keys/path2", "/home/bobby", "bobby"))
+ 
++    def test_multiple2(self):
++        self.assertEqual(
++            ["/keys/path1", "/keys/bobby"],
++            ssh_util.render_authorizedkeysfile_paths(
++                "/keys/path1 /keys/%u", "/home/bobby", "bobby"))
++
+     def test_relative(self):
+         self.assertEqual(
+             ["/home/bobby/.secret/keys"],
+@@ -581,269 +637,763 @@ class TestBasicAuthorizedKeyParse(test_helpers.CiTestCase):
+ 
+ class TestMultipleSshAuthorizedKeysFile(test_helpers.CiTestCase):
+ 
+-    @patch("cloudinit.ssh_util.pwd.getpwnam")
+-    def test_multiple_authorizedkeys_file_order1(self, m_getpwnam):
+-        fpw = FakePwEnt(pw_name='bobby', pw_dir='/tmp/home2/bobby')
+-        m_getpwnam.return_value = fpw
+-        user_ssh_folder = "%s/.ssh" % fpw.pw_dir
+-
+-        # /tmp/home2/bobby/.ssh/authorized_keys = rsa
+-        authorized_keys = self.tmp_path('authorized_keys', dir=user_ssh_folder)
+-        util.write_file(authorized_keys, VALID_CONTENT['rsa'])
+-
+-        # /tmp/home2/bobby/.ssh/user_keys = dsa
+-        user_keys = self.tmp_path('user_keys', dir=user_ssh_folder)
+-        util.write_file(user_keys, VALID_CONTENT['dsa'])
+-
+-        # /tmp/sshd_config
++    def create_fake_users(self, names, mock_permissions,
++                          m_get_group, m_get_owner, m_get_permissions,
++                          m_getpwnam, users):
++        homes = []
++
++        root = '/tmp/root'
++        fpw = FakePwEnt(pw_name="root", pw_dir=root)
++        users["root"] = fpw
++
++        for name in names:
++            home = '/tmp/home/' + name
++            fpw = FakePwEnt(pw_name=name, pw_dir=home)
++            users[name] = fpw
++            homes.append(home)
++
++        m_get_permissions.side_effect = partial(
++            mock_get_permissions, mock_permissions)
++        m_get_owner.side_effect = partial(mock_get_owner, mock_permissions)
++        m_get_group.side_effect = partial(mock_get_group, mock_permissions)
++        m_getpwnam.side_effect = partial(mock_getpwnam, users)
++        return homes
++
++    def create_user_authorized_file(self, home, filename, content_key, keys):
++        user_ssh_folder = "%s/.ssh" % home
++        # /tmp/home/<user>/.ssh/authorized_keys = content_key
++        authorized_keys = self.tmp_path(filename, dir=user_ssh_folder)
++        util.write_file(authorized_keys, VALID_CONTENT[content_key])
++        keys[authorized_keys] = content_key
++        return authorized_keys
++
++    def create_global_authorized_file(self, filename, content_key, keys):
++        authorized_keys = self.tmp_path(filename, dir='/tmp')
++        util.write_file(authorized_keys, VALID_CONTENT[content_key])
++        keys[authorized_keys] = content_key
++        return authorized_keys
++
++    def create_sshd_config(self, authorized_keys_files):
+         sshd_config = self.tmp_path('sshd_config', dir="/tmp")
+         util.write_file(
+             sshd_config,
+-            "AuthorizedKeysFile %s %s" % (authorized_keys, user_keys)
++            "AuthorizedKeysFile " + authorized_keys_files
+         )
++        return sshd_config
+ 
++    def execute_and_check(self, user, sshd_config, solution, keys,
++                          delete_keys=True):
+         (auth_key_fn, auth_key_entries) = ssh_util.extract_authorized_keys(
+-            fpw.pw_name, sshd_config)
++            user, sshd_config)
+         content = ssh_util.update_authorized_keys(auth_key_entries, [])
+ 
+-        self.assertEqual(user_keys, auth_key_fn)
+-        self.assertTrue(VALID_CONTENT['rsa'] in content)
+-        self.assertTrue(VALID_CONTENT['dsa'] in content)
++        self.assertEqual(auth_key_fn, solution)
++        for path, key in keys.items():
++            if path == solution:
++                self.assertTrue(VALID_CONTENT[key] in content)
++            else:
++                self.assertFalse(VALID_CONTENT[key] in content)
++
++        if delete_keys and os.path.isdir("/tmp/home/"):
++            util.delete_dir_contents("/tmp/home/")
+ 
+     @patch("cloudinit.ssh_util.pwd.getpwnam")
+-    def test_multiple_authorizedkeys_file_order2(self, m_getpwnam):
+-        fpw = FakePwEnt(pw_name='suzie', pw_dir='/tmp/home/suzie')
+-        m_getpwnam.return_value = fpw
+-        user_ssh_folder = "%s/.ssh" % fpw.pw_dir
++    @patch("cloudinit.util.get_permissions")
++    @patch("cloudinit.util.get_owner")
++    @patch("cloudinit.util.get_group")
++    def test_single_user_two_local_files(
++        self, m_get_group, m_get_owner, m_get_permissions, m_getpwnam
++    ):
++        user_bobby = 'bobby'
++        keys = {}
++        users = {}
++        mock_permissions = {
++            '/tmp/home/bobby': ('bobby', 'bobby', 0o700),
++            '/tmp/home/bobby/.ssh': ('bobby', 'bobby', 0o700),
++            '/tmp/home/bobby/.ssh/user_keys': ('bobby', 'bobby', 0o600),
++            '/tmp/home/bobby/.ssh/authorized_keys': ('bobby', 'bobby', 0o600),
++        }
++
++        homes = self.create_fake_users(
++            [user_bobby], mock_permissions, m_get_group, m_get_owner,
++            m_get_permissions, m_getpwnam, users
++        )
++        home = homes[0]
+ 
+-        # /tmp/home/suzie/.ssh/authorized_keys = rsa
+-        authorized_keys = self.tmp_path('authorized_keys', dir=user_ssh_folder)
+-        util.write_file(authorized_keys, VALID_CONTENT['rsa'])
++        # /tmp/home/bobby/.ssh/authorized_keys = rsa
++        authorized_keys = self.create_user_authorized_file(
++            home, 'authorized_keys', 'rsa', keys
++        )
+ 
+-        # /tmp/home/suzie/.ssh/user_keys = dsa
+-        user_keys = self.tmp_path('user_keys', dir=user_ssh_folder)
+-        util.write_file(user_keys, VALID_CONTENT['dsa'])
++        # /tmp/home/bobby/.ssh/user_keys = dsa
++        user_keys = self.create_user_authorized_file(
++            home, 'user_keys', 'dsa', keys
++        )
+ 
+         # /tmp/sshd_config
+-        sshd_config = self.tmp_path('sshd_config', dir="/tmp")
+-        util.write_file(
+-            sshd_config,
+-            "AuthorizedKeysFile %s %s" % (user_keys, authorized_keys)
++        options = "%s %s" % (authorized_keys, user_keys)
++        sshd_config = self.create_sshd_config(options)
++
++        self.execute_and_check(user_bobby, sshd_config, authorized_keys, keys)
++
++    @patch("cloudinit.ssh_util.pwd.getpwnam")
++    @patch("cloudinit.util.get_permissions")
++    @patch("cloudinit.util.get_owner")
++    @patch("cloudinit.util.get_group")
++    def test_single_user_two_local_files_inverted(
++        self, m_get_group, m_get_owner, m_get_permissions, m_getpwnam
++    ):
++        user_bobby = 'bobby'
++        keys = {}
++        users = {}
++        mock_permissions = {
++            '/tmp/home/bobby': ('bobby', 'bobby', 0o700),
++            '/tmp/home/bobby/.ssh': ('bobby', 'bobby', 0o700),
++            '/tmp/home/bobby/.ssh/user_keys': ('bobby', 'bobby', 0o600),
++            '/tmp/home/bobby/.ssh/authorized_keys': ('bobby', 'bobby', 0o600),
++        }
++
++        homes = self.create_fake_users(
++            [user_bobby], mock_permissions, m_get_group, m_get_owner,
++            m_get_permissions, m_getpwnam, users
+         )
++        home = homes[0]
+ 
+-        (auth_key_fn, auth_key_entries) = ssh_util.extract_authorized_keys(
+-            fpw.pw_name, sshd_config)
+-        content = ssh_util.update_authorized_keys(auth_key_entries, [])
++        # /tmp/home/bobby/.ssh/authorized_keys = rsa
++        authorized_keys = self.create_user_authorized_file(
++            home, 'authorized_keys', 'rsa', keys
++        )
+ 
+-        self.assertEqual(authorized_keys, auth_key_fn)
+-        self.assertTrue(VALID_CONTENT['rsa'] in content)
+-        self.assertTrue(VALID_CONTENT['dsa'] in content)
++        # /tmp/home/bobby/.ssh/user_keys = dsa
++        user_keys = self.create_user_authorized_file(
++            home, 'user_keys', 'dsa', keys
++        )
+ 
+-    @patch("cloudinit.ssh_util.pwd.getpwnam")
+-    def test_multiple_authorizedkeys_file_local_global(self, m_getpwnam):
+-        fpw = FakePwEnt(pw_name='bobby', pw_dir='/tmp/home2/bobby')
+-        m_getpwnam.return_value = fpw
+-        user_ssh_folder = "%s/.ssh" % fpw.pw_dir
++        # /tmp/sshd_config
++        options = "%s %s" % (user_keys, authorized_keys)
++        sshd_config = self.create_sshd_config(options)
+ 
+-        # /tmp/home2/bobby/.ssh/authorized_keys = rsa
+-        authorized_keys = self.tmp_path('authorized_keys', dir=user_ssh_folder)
+-        util.write_file(authorized_keys, VALID_CONTENT['rsa'])
++        self.execute_and_check(user_bobby, sshd_config, user_keys, keys)
+ 
+-        # /tmp/home2/bobby/.ssh/user_keys = dsa
+-        user_keys = self.tmp_path('user_keys', dir=user_ssh_folder)
+-        util.write_file(user_keys, VALID_CONTENT['dsa'])
++    @patch("cloudinit.ssh_util.pwd.getpwnam")
++    @patch("cloudinit.util.get_permissions")
++    @patch("cloudinit.util.get_owner")
++    @patch("cloudinit.util.get_group")
++    def test_single_user_local_global_files(
++        self, m_get_group, m_get_owner, m_get_permissions, m_getpwnam
++    ):
++        user_bobby = 'bobby'
++        keys = {}
++        users = {}
++        mock_permissions = {
++            '/tmp/home/bobby': ('bobby', 'bobby', 0o700),
++            '/tmp/home/bobby/.ssh': ('bobby', 'bobby', 0o700),
++            '/tmp/home/bobby/.ssh/user_keys': ('bobby', 'bobby', 0o600),
++            '/tmp/home/bobby/.ssh/authorized_keys': ('bobby', 'bobby', 0o600),
++        }
++
++        homes = self.create_fake_users(
++            [user_bobby], mock_permissions, m_get_group, m_get_owner,
++            m_get_permissions, m_getpwnam, users
++        )
++        home = homes[0]
+ 
+-        # /tmp/etc/ssh/authorized_keys = ecdsa
+-        authorized_keys_global = self.tmp_path('etc/ssh/authorized_keys',
+-                                               dir="/tmp")
+-        util.write_file(authorized_keys_global, VALID_CONTENT['ecdsa'])
++        # /tmp/home/bobby/.ssh/authorized_keys = rsa
++        authorized_keys = self.create_user_authorized_file(
++            home, 'authorized_keys', 'rsa', keys
++        )
+ 
+-        # /tmp/sshd_config
+-        sshd_config = self.tmp_path('sshd_config', dir="/tmp")
+-        util.write_file(
+-            sshd_config,
+-            "AuthorizedKeysFile %s %s %s" % (authorized_keys_global,
+-                                             user_keys, authorized_keys)
++        # /tmp/home/bobby/.ssh/user_keys = dsa
++        user_keys = self.create_user_authorized_file(
++            home, 'user_keys', 'dsa', keys
+         )
+ 
+-        (auth_key_fn, auth_key_entries) = ssh_util.extract_authorized_keys(
+-            fpw.pw_name, sshd_config)
+-        content = ssh_util.update_authorized_keys(auth_key_entries, [])
++        authorized_keys_global = self.create_global_authorized_file(
++            'etc/ssh/authorized_keys', 'ecdsa', keys
++        )
+ 
+-        self.assertEqual(authorized_keys, auth_key_fn)
+-        self.assertTrue(VALID_CONTENT['rsa'] in content)
+-        self.assertTrue(VALID_CONTENT['ecdsa'] in content)
+-        self.assertTrue(VALID_CONTENT['dsa'] in content)
++        options = "%s %s %s" % (authorized_keys_global, user_keys,
++                                authorized_keys)
++        sshd_config = self.create_sshd_config(options)
+ 
+-    @patch("cloudinit.ssh_util.pwd.getpwnam")
+-    def test_multiple_authorizedkeys_file_local_global2(self, m_getpwnam):
+-        fpw = FakePwEnt(pw_name='bobby', pw_dir='/tmp/home2/bobby')
+-        m_getpwnam.return_value = fpw
+-        user_ssh_folder = "%s/.ssh" % fpw.pw_dir
++        self.execute_and_check(user_bobby, sshd_config, user_keys, keys)
+ 
+-        # /tmp/home2/bobby/.ssh/authorized_keys2 = rsa
+-        authorized_keys = self.tmp_path('authorized_keys2',
+-                                        dir=user_ssh_folder)
+-        util.write_file(authorized_keys, VALID_CONTENT['rsa'])
++    @patch("cloudinit.ssh_util.pwd.getpwnam")
++    @patch("cloudinit.util.get_permissions")
++    @patch("cloudinit.util.get_owner")
++    @patch("cloudinit.util.get_group")
++    def test_single_user_local_global_files_inverted(
++        self, m_get_group, m_get_owner, m_get_permissions, m_getpwnam
++    ):
++        user_bobby = 'bobby'
++        keys = {}
++        users = {}
++        mock_permissions = {
++            '/tmp/home/bobby': ('bobby', 'bobby', 0o700),
++            '/tmp/home/bobby/.ssh': ('bobby', 'bobby', 0o700),
++            '/tmp/home/bobby/.ssh/user_keys3': ('bobby', 'bobby', 0o600),
++            '/tmp/home/bobby/.ssh/authorized_keys2': ('bobby', 'bobby', 0o600),
++        }
++
++        homes = self.create_fake_users(
++            [user_bobby], mock_permissions, m_get_group, m_get_owner,
++            m_get_permissions, m_getpwnam, users
++        )
++        home = homes[0]
+ 
+-        # /tmp/home2/bobby/.ssh/user_keys3 = dsa
+-        user_keys = self.tmp_path('user_keys3', dir=user_ssh_folder)
+-        util.write_file(user_keys, VALID_CONTENT['dsa'])
++        # /tmp/home/bobby/.ssh/authorized_keys = rsa
++        authorized_keys = self.create_user_authorized_file(
++            home, 'authorized_keys2', 'rsa', keys
++        )
+ 
+-        # /tmp/etc/ssh/authorized_keys = ecdsa
+-        authorized_keys_global = self.tmp_path('etc/ssh/authorized_keys',
+-                                               dir="/tmp")
+-        util.write_file(authorized_keys_global, VALID_CONTENT['ecdsa'])
++        # /tmp/home/bobby/.ssh/user_keys = dsa
++        user_keys = self.create_user_authorized_file(
++            home, 'user_keys3', 'dsa', keys
++        )
+ 
+-        # /tmp/sshd_config
+-        sshd_config = self.tmp_path('sshd_config', dir="/tmp")
+-        util.write_file(
+-            sshd_config,
+-            "AuthorizedKeysFile %s %s %s" % (authorized_keys_global,
+-                                             authorized_keys, user_keys)
++        authorized_keys_global = self.create_global_authorized_file(
++            'etc/ssh/authorized_keys', 'ecdsa', keys
+         )
+ 
+-        (auth_key_fn, auth_key_entries) = ssh_util.extract_authorized_keys(
+-            fpw.pw_name, sshd_config)
+-        content = ssh_util.update_authorized_keys(auth_key_entries, [])
++        options = "%s %s %s" % (authorized_keys_global, authorized_keys,
++                                user_keys)
++        sshd_config = self.create_sshd_config(options)
+ 
+-        self.assertEqual(user_keys, auth_key_fn)
+-        self.assertTrue(VALID_CONTENT['rsa'] in content)
+-        self.assertTrue(VALID_CONTENT['ecdsa'] in content)
+-        self.assertTrue(VALID_CONTENT['dsa'] in content)
++        self.execute_and_check(user_bobby, sshd_config, authorized_keys, keys)
+ 
+     @patch("cloudinit.ssh_util.pwd.getpwnam")
+-    def test_multiple_authorizedkeys_file_global(self, m_getpwnam):
+-        fpw = FakePwEnt(pw_name='bobby', pw_dir='/tmp/home2/bobby')
+-        m_getpwnam.return_value = fpw
++    @patch("cloudinit.util.get_permissions")
++    @patch("cloudinit.util.get_owner")
++    @patch("cloudinit.util.get_group")
++    def test_single_user_global_file(
++        self, m_get_group, m_get_owner, m_get_permissions, m_getpwnam
++    ):
++        user_bobby = 'bobby'
++        keys = {}
++        users = {}
++        mock_permissions = {
++            '/tmp/home/bobby': ('bobby', 'bobby', 0o700),
++            '/tmp/home/bobby/.ssh': ('bobby', 'bobby', 0o700),
++            '/tmp/home/bobby/.ssh/authorized_keys': ('bobby', 'bobby', 0o600),
++        }
++
++        homes = self.create_fake_users(
++            [user_bobby], mock_permissions, m_get_group, m_get_owner,
++            m_get_permissions, m_getpwnam, users
++        )
++        home = homes[0]
+ 
+         # /tmp/etc/ssh/authorized_keys = rsa
+-        authorized_keys_global = self.tmp_path('etc/ssh/authorized_keys',
+-                                               dir="/tmp")
+-        util.write_file(authorized_keys_global, VALID_CONTENT['rsa'])
++        authorized_keys_global = self.create_global_authorized_file(
++            'etc/ssh/authorized_keys', 'rsa', keys
++        )
+ 
+-        # /tmp/sshd_config
+-        sshd_config = self.tmp_path('sshd_config')
+-        util.write_file(
+-            sshd_config,
+-            "AuthorizedKeysFile %s" % (authorized_keys_global)
++        options = "%s" % authorized_keys_global
++        sshd_config = self.create_sshd_config(options)
++
++        default = "%s/.ssh/authorized_keys" % home
++        self.execute_and_check(user_bobby, sshd_config, default, keys)
++
++    @patch("cloudinit.ssh_util.pwd.getpwnam")
++    @patch("cloudinit.util.get_permissions")
++    @patch("cloudinit.util.get_owner")
++    @patch("cloudinit.util.get_group")
++    def test_two_users_local_file_standard(
++        self, m_get_group, m_get_owner, m_get_permissions, m_getpwnam
++    ):
++        keys = {}
++        users = {}
++        mock_permissions = {
++            '/tmp/home/bobby': ('bobby', 'bobby', 0o700),
++            '/tmp/home/bobby/.ssh': ('bobby', 'bobby', 0o700),
++            '/tmp/home/bobby/.ssh/authorized_keys': ('bobby', 'bobby', 0o600),
++            '/tmp/home/suzie': ('suzie', 'suzie', 0o700),
++            '/tmp/home/suzie/.ssh': ('suzie', 'suzie', 0o700),
++            '/tmp/home/suzie/.ssh/authorized_keys': ('suzie', 'suzie', 0o600),
++        }
++
++        user_bobby = 'bobby'
++        user_suzie = 'suzie'
++        homes = self.create_fake_users(
++            [user_bobby, user_suzie], mock_permissions, m_get_group,
++            m_get_owner, m_get_permissions, m_getpwnam, users
+         )
++        home_bobby = homes[0]
++        home_suzie = homes[1]
+ 
+-        (auth_key_fn, auth_key_entries) = ssh_util.extract_authorized_keys(
+-            fpw.pw_name, sshd_config)
+-        content = ssh_util.update_authorized_keys(auth_key_entries, [])
++        # /tmp/home/bobby/.ssh/authorized_keys = rsa
++        authorized_keys = self.create_user_authorized_file(
++            home_bobby, 'authorized_keys', 'rsa', keys
++        )
+ 
+-        self.assertEqual("%s/.ssh/authorized_keys" % fpw.pw_dir, auth_key_fn)
+-        self.assertTrue(VALID_CONTENT['rsa'] in content)
++        # /tmp/home/suzie/.ssh/authorized_keys = rsa
++        authorized_keys2 = self.create_user_authorized_file(
++            home_suzie, 'authorized_keys', 'ssh-xmss@openssh.com', keys
++        )
++
++        options = ".ssh/authorized_keys"
++        sshd_config = self.create_sshd_config(options)
++
++        self.execute_and_check(
++            user_bobby, sshd_config, authorized_keys, keys, delete_keys=False
++        )
++        self.execute_and_check(user_suzie, sshd_config, authorized_keys2, keys)
+ 
+     @patch("cloudinit.ssh_util.pwd.getpwnam")
+-    def test_multiple_authorizedkeys_file_multiuser(self, m_getpwnam):
+-        fpw = FakePwEnt(pw_name='bobby', pw_dir='/tmp/home2/bobby')
+-        m_getpwnam.return_value = fpw
+-        user_ssh_folder = "%s/.ssh" % fpw.pw_dir
+-        # /tmp/home2/bobby/.ssh/authorized_keys2 = rsa
+-        authorized_keys = self.tmp_path('authorized_keys2',
+-                                        dir=user_ssh_folder)
+-        util.write_file(authorized_keys, VALID_CONTENT['rsa'])
+-        # /tmp/home2/bobby/.ssh/user_keys3 = dsa
+-        user_keys = self.tmp_path('user_keys3', dir=user_ssh_folder)
+-        util.write_file(user_keys, VALID_CONTENT['dsa'])
+-
+-        fpw2 = FakePwEnt(pw_name='suzie', pw_dir='/tmp/home/suzie')
+-        user_ssh_folder = "%s/.ssh" % fpw2.pw_dir
+-        # /tmp/home/suzie/.ssh/authorized_keys2 = ssh-xmss@openssh.com
+-        authorized_keys2 = self.tmp_path('authorized_keys2',
+-                                         dir=user_ssh_folder)
+-        util.write_file(authorized_keys2,
+-                        VALID_CONTENT['ssh-xmss@openssh.com'])
++    @patch("cloudinit.util.get_permissions")
++    @patch("cloudinit.util.get_owner")
++    @patch("cloudinit.util.get_group")
++    def test_two_users_local_file_custom(
++        self, m_get_group, m_get_owner, m_get_permissions, m_getpwnam
++    ):
++        keys = {}
++        users = {}
++        mock_permissions = {
++            '/tmp/home/bobby': ('bobby', 'bobby', 0o700),
++            '/tmp/home/bobby/.ssh': ('bobby', 'bobby', 0o700),
++            '/tmp/home/bobby/.ssh/authorized_keys2': ('bobby', 'bobby', 0o600),
++            '/tmp/home/suzie': ('suzie', 'suzie', 0o700),
++            '/tmp/home/suzie/.ssh': ('suzie', 'suzie', 0o700),
++            '/tmp/home/suzie/.ssh/authorized_keys2': ('suzie', 'suzie', 0o600),
++        }
++
++        user_bobby = 'bobby'
++        user_suzie = 'suzie'
++        homes = self.create_fake_users(
++            [user_bobby, user_suzie], mock_permissions, m_get_group,
++            m_get_owner, m_get_permissions, m_getpwnam, users
++        )
++        home_bobby = homes[0]
++        home_suzie = homes[1]
+ 
+-        # /tmp/etc/ssh/authorized_keys = ecdsa
+-        authorized_keys_global = self.tmp_path('etc/ssh/authorized_keys2',
+-                                               dir="/tmp")
+-        util.write_file(authorized_keys_global, VALID_CONTENT['ecdsa'])
++        # /tmp/home/bobby/.ssh/authorized_keys2 = rsa
++        authorized_keys = self.create_user_authorized_file(
++            home_bobby, 'authorized_keys2', 'rsa', keys
++        )
+ 
+-        # /tmp/sshd_config
+-        sshd_config = self.tmp_path('sshd_config', dir="/tmp")
+-        util.write_file(
+-            sshd_config,
+-            "AuthorizedKeysFile %s %%h/.ssh/authorized_keys2 %s" %
+-            (authorized_keys_global, user_keys)
++        # /tmp/home/suzie/.ssh/authorized_keys2 = rsa
++        authorized_keys2 = self.create_user_authorized_file(
++            home_suzie, 'authorized_keys2', 'ssh-xmss@openssh.com', keys
+         )
+ 
+-        # process first user
+-        (auth_key_fn, auth_key_entries) = ssh_util.extract_authorized_keys(
+-            fpw.pw_name, sshd_config)
+-        content = ssh_util.update_authorized_keys(auth_key_entries, [])
++        options = ".ssh/authorized_keys2"
++        sshd_config = self.create_sshd_config(options)
++
++        self.execute_and_check(
++            user_bobby, sshd_config, authorized_keys, keys, delete_keys=False
++        )
++        self.execute_and_check(user_suzie, sshd_config, authorized_keys2, keys)
+ 
+-        self.assertEqual(user_keys, auth_key_fn)
+-        self.assertTrue(VALID_CONTENT['rsa'] in content)
+-        self.assertTrue(VALID_CONTENT['ecdsa'] in content)
+-        self.assertTrue(VALID_CONTENT['dsa'] in content)
+-        self.assertFalse(VALID_CONTENT['ssh-xmss@openssh.com'] in content)
++    @patch("cloudinit.ssh_util.pwd.getpwnam")
++    @patch("cloudinit.util.get_permissions")
++    @patch("cloudinit.util.get_owner")
++    @patch("cloudinit.util.get_group")
++    def test_two_users_local_global_files(
++        self, m_get_group, m_get_owner, m_get_permissions, m_getpwnam
++    ):
++        keys = {}
++        users = {}
++        mock_permissions = {
++            '/tmp/home/bobby': ('bobby', 'bobby', 0o700),
++            '/tmp/home/bobby/.ssh': ('bobby', 'bobby', 0o700),
++            '/tmp/home/bobby/.ssh/authorized_keys2': ('bobby', 'bobby', 0o600),
++            '/tmp/home/bobby/.ssh/user_keys3': ('bobby', 'bobby', 0o600),
++            '/tmp/home/suzie': ('suzie', 'suzie', 0o700),
++            '/tmp/home/suzie/.ssh': ('suzie', 'suzie', 0o700),
++            '/tmp/home/suzie/.ssh/authorized_keys2': ('suzie', 'suzie', 0o600),
++            '/tmp/home/suzie/.ssh/user_keys3': ('suzie', 'suzie', 0o600),
++        }
++
++        user_bobby = 'bobby'
++        user_suzie = 'suzie'
++        homes = self.create_fake_users(
++            [user_bobby, user_suzie], mock_permissions, m_get_group,
++            m_get_owner, m_get_permissions, m_getpwnam, users
++        )
++        home_bobby = homes[0]
++        home_suzie = homes[1]
+ 
+-        m_getpwnam.return_value = fpw2
+-        # process second user
+-        (auth_key_fn, auth_key_entries) = ssh_util.extract_authorized_keys(
+-            fpw2.pw_name, sshd_config)
+-        content = ssh_util.update_authorized_keys(auth_key_entries, [])
++        # /tmp/home/bobby/.ssh/authorized_keys2 = rsa
++        self.create_user_authorized_file(
++            home_bobby, 'authorized_keys2', 'rsa', keys
++        )
++        # /tmp/home/bobby/.ssh/user_keys3 = dsa
++        user_keys = self.create_user_authorized_file(
++            home_bobby, 'user_keys3', 'dsa', keys
++        )
++
++        # /tmp/home/suzie/.ssh/authorized_keys2 = rsa
++        authorized_keys2 = self.create_user_authorized_file(
++            home_suzie, 'authorized_keys2', 'ssh-xmss@openssh.com', keys
++        )
++
++        # /tmp/etc/ssh/authorized_keys = ecdsa
++        authorized_keys_global = self.create_global_authorized_file(
++            'etc/ssh/authorized_keys2', 'ecdsa', keys
++        )
++
++        options = "%s %s %%h/.ssh/authorized_keys2" % \
++            (authorized_keys_global, user_keys)
++        sshd_config = self.create_sshd_config(options)
+ 
+-        self.assertEqual(authorized_keys2, auth_key_fn)
+-        self.assertTrue(VALID_CONTENT['ssh-xmss@openssh.com'] in content)
+-        self.assertTrue(VALID_CONTENT['ecdsa'] in content)
+-        self.assertTrue(VALID_CONTENT['dsa'] in content)
+-        self.assertFalse(VALID_CONTENT['rsa'] in content)
++        self.execute_and_check(
++            user_bobby, sshd_config, user_keys, keys, delete_keys=False
++        )
++        self.execute_and_check(user_suzie, sshd_config, authorized_keys2, keys)
+ 
++    @patch("cloudinit.util.get_user_groups")
+     @patch("cloudinit.ssh_util.pwd.getpwnam")
+-    def test_multiple_authorizedkeys_file_multiuser2(self, m_getpwnam):
+-        fpw = FakePwEnt(pw_name='bobby', pw_dir='/tmp/home/bobby')
+-        m_getpwnam.return_value = fpw
+-        user_ssh_folder = "%s/.ssh" % fpw.pw_dir
++    @patch("cloudinit.util.get_permissions")
++    @patch("cloudinit.util.get_owner")
++    @patch("cloudinit.util.get_group")
++    def test_two_users_local_global_files_badguy(
++        self, m_get_group, m_get_owner, m_get_permissions, m_getpwnam,
++        m_get_user_groups
++    ):
++        keys = {}
++        users = {}
++        mock_permissions = {
++            '/tmp/home/bobby': ('bobby', 'bobby', 0o700),
++            '/tmp/home/bobby/.ssh': ('bobby', 'bobby', 0o700),
++            '/tmp/home/bobby/.ssh/authorized_keys2': ('bobby', 'bobby', 0o600),
++            '/tmp/home/bobby/.ssh/user_keys3': ('bobby', 'bobby', 0o600),
++            '/tmp/home/badguy': ('root', 'root', 0o755),
++            '/tmp/home/badguy/home': ('root', 'root', 0o755),
++            '/tmp/home/badguy/home/bobby': ('root', 'root', 0o655),
++        }
++
++        user_bobby = 'bobby'
++        user_badguy = 'badguy'
++        home_bobby, *_ = self.create_fake_users(
++            [user_bobby, user_badguy], mock_permissions, m_get_group,
++            m_get_owner, m_get_permissions, m_getpwnam, users
++        )
++        m_get_user_groups.side_effect = mock_get_user_groups
++
+         # /tmp/home/bobby/.ssh/authorized_keys2 = rsa
+-        authorized_keys = self.tmp_path('authorized_keys2',
+-                                        dir=user_ssh_folder)
+-        util.write_file(authorized_keys, VALID_CONTENT['rsa'])
++        authorized_keys = self.create_user_authorized_file(
++            home_bobby, 'authorized_keys2', 'rsa', keys
++        )
+         # /tmp/home/bobby/.ssh/user_keys3 = dsa
+-        user_keys = self.tmp_path('user_keys3', dir=user_ssh_folder)
+-        util.write_file(user_keys, VALID_CONTENT['dsa'])
++        user_keys = self.create_user_authorized_file(
++            home_bobby, 'user_keys3', 'dsa', keys
++        )
+ 
+-        fpw2 = FakePwEnt(pw_name='badguy', pw_dir='/tmp/home/badguy')
+-        user_ssh_folder = "%s/.ssh" % fpw2.pw_dir
+         # /tmp/home/badguy/home/bobby = ""
+         authorized_keys2 = self.tmp_path('home/bobby', dir="/tmp/home/badguy")
++        util.write_file(authorized_keys2, '')
+ 
+         # /tmp/etc/ssh/authorized_keys = ecdsa
+-        authorized_keys_global = self.tmp_path('etc/ssh/authorized_keys2',
+-                                               dir="/tmp")
+-        util.write_file(authorized_keys_global, VALID_CONTENT['ecdsa'])
++        authorized_keys_global = self.create_global_authorized_file(
++            'etc/ssh/authorized_keys2', 'ecdsa', keys
++        )
+ 
+         # /tmp/sshd_config
+-        sshd_config = self.tmp_path('sshd_config', dir="/tmp")
+-        util.write_file(
+-            sshd_config,
+-            "AuthorizedKeysFile %s %%h/.ssh/authorized_keys2 %s %s" %
+-            (authorized_keys_global, user_keys, authorized_keys2)
++        options = "%s %%h/.ssh/authorized_keys2 %s %s" % \
++            (authorized_keys2, authorized_keys_global, user_keys)
++        sshd_config = self.create_sshd_config(options)
++
++        self.execute_and_check(
++            user_bobby, sshd_config, authorized_keys, keys, delete_keys=False
++        )
++        self.execute_and_check(
++            user_badguy, sshd_config, authorized_keys2, keys
+         )
+ 
+-        # process first user
+-        (auth_key_fn, auth_key_entries) = ssh_util.extract_authorized_keys(
+-            fpw.pw_name, sshd_config)
+-        content = ssh_util.update_authorized_keys(auth_key_entries, [])
++    @patch("cloudinit.util.get_user_groups")
++    @patch("cloudinit.ssh_util.pwd.getpwnam")
++    @patch("cloudinit.util.get_permissions")
++    @patch("cloudinit.util.get_owner")
++    @patch("cloudinit.util.get_group")
++    def test_two_users_unaccessible_file(
++        self, m_get_group, m_get_owner, m_get_permissions, m_getpwnam,
++        m_get_user_groups
++    ):
++        keys = {}
++        users = {}
++        mock_permissions = {
++            '/tmp/home/bobby': ('bobby', 'bobby', 0o700),
++            '/tmp/home/bobby/.ssh': ('bobby', 'bobby', 0o700),
++            '/tmp/home/bobby/.ssh/authorized_keys': ('bobby', 'bobby', 0o600),
++
++            '/tmp/etc': ('root', 'root', 0o755),
++            '/tmp/etc/ssh': ('root', 'root', 0o755),
++            '/tmp/etc/ssh/userkeys': ('root', 'root', 0o700),
++            '/tmp/etc/ssh/userkeys/bobby': ('bobby', 'bobby', 0o600),
++            '/tmp/etc/ssh/userkeys/badguy': ('badguy', 'badguy', 0o600),
++
++            '/tmp/home/badguy': ('badguy', 'badguy', 0o700),
++            '/tmp/home/badguy/.ssh': ('badguy', 'badguy', 0o700),
++            '/tmp/home/badguy/.ssh/authorized_keys':
++                ('badguy', 'badguy', 0o600),
++        }
++
++        user_bobby = 'bobby'
++        user_badguy = 'badguy'
++        homes = self.create_fake_users(
++            [user_bobby, user_badguy], mock_permissions, m_get_group,
++            m_get_owner, m_get_permissions, m_getpwnam, users
++        )
++        m_get_user_groups.side_effect = mock_get_user_groups
++        home_bobby = homes[0]
++        home_badguy = homes[1]
+ 
+-        self.assertEqual(user_keys, auth_key_fn)
+-        self.assertTrue(VALID_CONTENT['rsa'] in content)
+-        self.assertTrue(VALID_CONTENT['ecdsa'] in content)
+-        self.assertTrue(VALID_CONTENT['dsa'] in content)
++        # /tmp/home/bobby/.ssh/authorized_keys = rsa
++        authorized_keys = self.create_user_authorized_file(
++            home_bobby, 'authorized_keys', 'rsa', keys
++        )
++        # /tmp/etc/ssh/userkeys/bobby = dsa
++        # assume here that we can bypass userkeys, despite permissions
++        self.create_global_authorized_file(
++            'etc/ssh/userkeys/bobby', 'dsa', keys
++        )
+ 
+-        m_getpwnam.return_value = fpw2
+-        # process second user
+-        (auth_key_fn, auth_key_entries) = ssh_util.extract_authorized_keys(
+-            fpw2.pw_name, sshd_config)
+-        content = ssh_util.update_authorized_keys(auth_key_entries, [])
++        # /tmp/home/badguy/.ssh/authorized_keys = ssh-xmss@openssh.com
++        authorized_keys2 = self.create_user_authorized_file(
++            home_badguy, 'authorized_keys', 'ssh-xmss@openssh.com', keys
++        )
+ 
+-        # badguy should not take the key from the other user!
+-        self.assertEqual(authorized_keys2, auth_key_fn)
+-        self.assertTrue(VALID_CONTENT['ecdsa'] in content)
+-        self.assertTrue(VALID_CONTENT['dsa'] in content)
+-        self.assertFalse(VALID_CONTENT['rsa'] in content)
++        # /tmp/etc/ssh/userkeys/badguy = ecdsa
++        self.create_global_authorized_file(
++            'etc/ssh/userkeys/badguy', 'ecdsa', keys
++        )
++
++        # /tmp/sshd_config
++        options = "/tmp/etc/ssh/userkeys/%u .ssh/authorized_keys"
++        sshd_config = self.create_sshd_config(options)
++
++        self.execute_and_check(
++            user_bobby, sshd_config, authorized_keys, keys, delete_keys=False
++        )
++        self.execute_and_check(
++            user_badguy, sshd_config, authorized_keys2, keys
++        )
++
++    @patch("cloudinit.util.get_user_groups")
++    @patch("cloudinit.ssh_util.pwd.getpwnam")
++    @patch("cloudinit.util.get_permissions")
++    @patch("cloudinit.util.get_owner")
++    @patch("cloudinit.util.get_group")
++    def test_two_users_accessible_file(
++        self, m_get_group, m_get_owner, m_get_permissions, m_getpwnam,
++        m_get_user_groups
++    ):
++        keys = {}
++        users = {}
++        mock_permissions = {
++            '/tmp/home/bobby': ('bobby', 'bobby', 0o700),
++            '/tmp/home/bobby/.ssh': ('bobby', 'bobby', 0o700),
++            '/tmp/home/bobby/.ssh/authorized_keys': ('bobby', 'bobby', 0o600),
++
++            '/tmp/etc': ('root', 'root', 0o755),
++            '/tmp/etc/ssh': ('root', 'root', 0o755),
++            '/tmp/etc/ssh/userkeys': ('root', 'root', 0o755),
++            '/tmp/etc/ssh/userkeys/bobby': ('bobby', 'bobby', 0o600),
++            '/tmp/etc/ssh/userkeys/badguy': ('badguy', 'badguy', 0o600),
++
++            '/tmp/home/badguy': ('badguy', 'badguy', 0o700),
++            '/tmp/home/badguy/.ssh': ('badguy', 'badguy', 0o700),
++            '/tmp/home/badguy/.ssh/authorized_keys':
++                ('badguy', 'badguy', 0o600),
++        }
++
++        user_bobby = 'bobby'
++        user_badguy = 'badguy'
++        homes = self.create_fake_users(
++            [user_bobby, user_badguy], mock_permissions, m_get_group,
++            m_get_owner, m_get_permissions, m_getpwnam, users
++        )
++        m_get_user_groups.side_effect = mock_get_user_groups
++        home_bobby = homes[0]
++        home_badguy = homes[1]
++
++        # /tmp/home/bobby/.ssh/authorized_keys = rsa
++        self.create_user_authorized_file(
++            home_bobby, 'authorized_keys', 'rsa', keys
++        )
++        # /tmp/etc/ssh/userkeys/bobby = dsa
++        # assume here that we can bypass userkeys, despite permissions
++        authorized_keys = self.create_global_authorized_file(
++            'etc/ssh/userkeys/bobby', 'dsa', keys
++        )
++
++        # /tmp/home/badguy/.ssh/authorized_keys = ssh-xmss@openssh.com
++        self.create_user_authorized_file(
++            home_badguy, 'authorized_keys', 'ssh-xmss@openssh.com', keys
++        )
++
++        # /tmp/etc/ssh/userkeys/badguy = ecdsa
++        authorized_keys2 = self.create_global_authorized_file(
++            'etc/ssh/userkeys/badguy', 'ecdsa', keys
++        )
++
++        # /tmp/sshd_config
++        options = "/tmp/etc/ssh/userkeys/%u .ssh/authorized_keys"
++        sshd_config = self.create_sshd_config(options)
++
++        self.execute_and_check(
++            user_bobby, sshd_config, authorized_keys, keys, delete_keys=False
++        )
++        self.execute_and_check(
++            user_badguy, sshd_config, authorized_keys2, keys
++        )
++
++    @patch("cloudinit.util.get_user_groups")
++    @patch("cloudinit.ssh_util.pwd.getpwnam")
++    @patch("cloudinit.util.get_permissions")
++    @patch("cloudinit.util.get_owner")
++    @patch("cloudinit.util.get_group")
++    def test_two_users_hardcoded_single_user_file(
++        self, m_get_group, m_get_owner, m_get_permissions, m_getpwnam,
++        m_get_user_groups
++    ):
++        keys = {}
++        users = {}
++        mock_permissions = {
++            '/tmp/home/bobby': ('bobby', 'bobby', 0o700),
++            '/tmp/home/bobby/.ssh': ('bobby', 'bobby', 0o700),
++            '/tmp/home/bobby/.ssh/authorized_keys': ('bobby', 'bobby', 0o600),
++
++            '/tmp/home/suzie': ('suzie', 'suzie', 0o700),
++            '/tmp/home/suzie/.ssh': ('suzie', 'suzie', 0o700),
++            '/tmp/home/suzie/.ssh/authorized_keys': ('suzie', 'suzie', 0o600),
++        }
++
++        user_bobby = 'bobby'
++        user_suzie = 'suzie'
++        homes = self.create_fake_users(
++            [user_bobby, user_suzie], mock_permissions, m_get_group,
++            m_get_owner, m_get_permissions, m_getpwnam, users
++        )
++        home_bobby = homes[0]
++        home_suzie = homes[1]
++        m_get_user_groups.side_effect = mock_get_user_groups
++
++        # /tmp/home/bobby/.ssh/authorized_keys = rsa
++        authorized_keys = self.create_user_authorized_file(
++            home_bobby, 'authorized_keys', 'rsa', keys
++        )
++
++        # /tmp/home/suzie/.ssh/authorized_keys = ssh-xmss@openssh.com
++        self.create_user_authorized_file(
++            home_suzie, 'authorized_keys', 'ssh-xmss@openssh.com', keys
++        )
++
++        # /tmp/sshd_config
++        options = "%s" % (authorized_keys)
++        sshd_config = self.create_sshd_config(options)
++
++        self.execute_and_check(
++            user_bobby, sshd_config, authorized_keys, keys, delete_keys=False
++        )
++        default = "%s/.ssh/authorized_keys" % home_suzie
++        self.execute_and_check(user_suzie, sshd_config, default, keys)
++
++    @patch("cloudinit.util.get_user_groups")
++    @patch("cloudinit.ssh_util.pwd.getpwnam")
++    @patch("cloudinit.util.get_permissions")
++    @patch("cloudinit.util.get_owner")
++    @patch("cloudinit.util.get_group")
++    def test_two_users_hardcoded_single_user_file_inverted(
++        self, m_get_group, m_get_owner, m_get_permissions, m_getpwnam,
++        m_get_user_groups
++    ):
++        keys = {}
++        users = {}
++        mock_permissions = {
++            '/tmp/home/bobby': ('bobby', 'bobby', 0o700),
++            '/tmp/home/bobby/.ssh': ('bobby', 'bobby', 0o700),
++            '/tmp/home/bobby/.ssh/authorized_keys': ('bobby', 'bobby', 0o600),
++
++            '/tmp/home/suzie': ('suzie', 'suzie', 0o700),
++            '/tmp/home/suzie/.ssh': ('suzie', 'suzie', 0o700),
++            '/tmp/home/suzie/.ssh/authorized_keys': ('suzie', 'suzie', 0o600),
++        }
++
++        user_bobby = 'bobby'
++        user_suzie = 'suzie'
++        homes = self.create_fake_users(
++            [user_bobby, user_suzie], mock_permissions, m_get_group,
++            m_get_owner, m_get_permissions, m_getpwnam, users
++        )
++        home_bobby = homes[0]
++        home_suzie = homes[1]
++        m_get_user_groups.side_effect = mock_get_user_groups
++
++        # /tmp/home/bobby/.ssh/authorized_keys = rsa
++        self.create_user_authorized_file(
++            home_bobby, 'authorized_keys', 'rsa', keys
++        )
++
++        # /tmp/home/suzie/.ssh/authorized_keys = ssh-xmss@openssh.com
++        authorized_keys2 = self.create_user_authorized_file(
++            home_suzie, 'authorized_keys', 'ssh-xmss@openssh.com', keys
++        )
++
++        # /tmp/sshd_config
++        options = "%s" % (authorized_keys2)
++        sshd_config = self.create_sshd_config(options)
++
++        default = "%s/.ssh/authorized_keys" % home_bobby
++        self.execute_and_check(
++            user_bobby, sshd_config, default, keys, delete_keys=False
++        )
++        self.execute_and_check(user_suzie, sshd_config, authorized_keys2, keys)
++
++    @patch("cloudinit.util.get_user_groups")
++    @patch("cloudinit.ssh_util.pwd.getpwnam")
++    @patch("cloudinit.util.get_permissions")
++    @patch("cloudinit.util.get_owner")
++    @patch("cloudinit.util.get_group")
++    def test_two_users_hardcoded_user_files(
++        self, m_get_group, m_get_owner, m_get_permissions, m_getpwnam,
++        m_get_user_groups
++    ):
++        keys = {}
++        users = {}
++        mock_permissions = {
++            '/tmp/home/bobby': ('bobby', 'bobby', 0o700),
++            '/tmp/home/bobby/.ssh': ('bobby', 'bobby', 0o700),
++            '/tmp/home/bobby/.ssh/authorized_keys': ('bobby', 'bobby', 0o600),
++
++            '/tmp/home/suzie': ('suzie', 'suzie', 0o700),
++            '/tmp/home/suzie/.ssh': ('suzie', 'suzie', 0o700),
++            '/tmp/home/suzie/.ssh/authorized_keys': ('suzie', 'suzie', 0o600),
++        }
++
++        user_bobby = 'bobby'
++        user_suzie = 'suzie'
++        homes = self.create_fake_users(
++            [user_bobby, user_suzie], mock_permissions, m_get_group,
++            m_get_owner, m_get_permissions, m_getpwnam, users
++        )
++        home_bobby = homes[0]
++        home_suzie = homes[1]
++        m_get_user_groups.side_effect = mock_get_user_groups
++
++        # /tmp/home/bobby/.ssh/authorized_keys = rsa
++        authorized_keys = self.create_user_authorized_file(
++            home_bobby, 'authorized_keys', 'rsa', keys
++        )
++
++        # /tmp/home/suzie/.ssh/authorized_keys = ssh-xmss@openssh.com
++        authorized_keys2 = self.create_user_authorized_file(
++            home_suzie, 'authorized_keys', 'ssh-xmss@openssh.com', keys
++        )
++
++        # /tmp/etc/ssh/authorized_keys = ecdsa
++        authorized_keys_global = self.create_global_authorized_file(
++            'etc/ssh/authorized_keys', 'ecdsa', keys
++        )
++
++        # /tmp/sshd_config
++        options = "%s %s %s" % \
++            (authorized_keys_global, authorized_keys, authorized_keys2)
++        sshd_config = self.create_sshd_config(options)
++
++        self.execute_and_check(
++            user_bobby, sshd_config, authorized_keys, keys, delete_keys=False
++        )
++        self.execute_and_check(user_suzie, sshd_config, authorized_keys2, keys)
+ 
+ # vi: ts=4 expandtab
+-- 
+2.27.0
+
diff --git a/SOURCES/ci-fix-a-typo-in-man-page-cloud-init.1-752.patch b/SOURCES/ci-fix-a-typo-in-man-page-cloud-init.1-752.patch
deleted file mode 100644
index 0a08abf..0000000
--- a/SOURCES/ci-fix-a-typo-in-man-page-cloud-init.1-752.patch
+++ /dev/null
@@ -1,53 +0,0 @@
-From c90d5c11eb99ec25e0fd90585bad9283e60bda7e Mon Sep 17 00:00:00 2001
-From: Eduardo Otubo <otubo@redhat.com>
-Date: Tue, 26 Jan 2021 10:48:55 +0100
-Subject: [PATCH] fix a typo in man page cloud-init.1 (#752)
-
-RH-Author: Eduardo Terrell Ferrari Otubo (eterrell)
-RH-MergeRequest: 39: fix a typo in man page cloud-init.1 (#752)
-RH-Commit: [1/1] d2f7efbc63a7928ef175ac0714053dba20aab01a (eterrell/cloud-init)
-RH-Bugzilla: 1913127
-
-commit 48b2c5f16bd4ef754fef137ea19894908d4bf1db
-Author: Amy Chen <66719270+xiachen-rh@users.noreply.github.com>
-Date:   Wed Jan 6 22:37:02 2021 +0800
-
-    fix a typo in man page cloud-init.1 (#752)
-
-    1. fix a typo in cloud-init.1
-    2. add xiachen-rh as contributor
-
-Conflict: We don't really use tools/.github-cla-signers, but had to fix
-a tiny conflict of already included names on the file.
-
-Signed-off-by: Eduardo Otubo <otubo@redhat.com>
----
- doc/man/cloud-init.1      | 2 +-
- tools/.github-cla-signers | 1 +
- 2 files changed, 2 insertions(+), 1 deletion(-)
-
-diff --git a/doc/man/cloud-init.1 b/doc/man/cloud-init.1
-index 9b52dc8d..3fde4148 100644
---- a/doc/man/cloud-init.1
-+++ b/doc/man/cloud-init.1
-@@ -10,7 +10,7 @@ cloud-init \- Cloud instance initialization
- Cloud-init provides a mechanism for cloud instance initialization.
- This is done by identifying the cloud platform that is in use, reading
- provided cloud metadata and optional vendor and user
--data, and then intializing the instance as requested.
-+data, and then initializing the instance as requested.
- 
- Generally, this command is not normally meant to be run directly by
- the user. However, some subcommands may useful for development or
-diff --git a/tools/.github-cla-signers b/tools/.github-cla-signers
-index 802a35bd..e5d2b95c 100644
---- a/tools/.github-cla-signers
-+++ b/tools/.github-cla-signers
-@@ -21,3 +21,4 @@ sshedi
- TheRealFalcon
- tomponline
- tsanghan
-+xiachen-rh
--- 
-2.18.4
-
diff --git a/SOURCES/ci-network-Fix-type-and-respect-name-when-rendering-vla.patch b/SOURCES/ci-network-Fix-type-and-respect-name-when-rendering-vla.patch
deleted file mode 100644
index a2ef2dc..0000000
--- a/SOURCES/ci-network-Fix-type-and-respect-name-when-rendering-vla.patch
+++ /dev/null
@@ -1,247 +0,0 @@
-From 51a90ecbdf1f3900183d8ec641eeb4571decf6dc Mon Sep 17 00:00:00 2001
-From: Eduardo Otubo <otubo@redhat.com>
-Date: Wed, 4 Nov 2020 12:37:54 +0100
-Subject: [PATCH] network: Fix type and respect name when rendering vlan in
- sysconfig. (#541)
-
-RH-Author: Eduardo Terrell Ferrari Otubo (eterrell)
-RH-MergeRequest: 19: network: Fix type and respect name when rendering vlan in sysconfig. (#541)
-RH-Commit: [1/1] 75bea46017397082c5763125a5f35806c2f840e9 (eterrell/cloud-init)
-RH-Bugzilla: 1881462
-
-commit 8439b191ec2f336d544cab86dba2860f969cd5b8
-Author: Eduardo Otubo <otubo@redhat.com>
-Date:   Tue Sep 15 18:00:00 2020 +0200
-
-    network: Fix type and respect name when rendering vlan in sysconfig. (#541)
-
-    Prior to this change, vlans were rendered in sysconfig with
-    'TYPE=Ethernet', and incorrectly rendered the PHYSDEV based on
-    the name of the vlan device rather than the 'link' provided
-    in the network config.
-
-    The change here fixes:
-     * rendering of TYPE=Ethernet for a vlan
-     * adds a warning if the configured device name is not supported
-       per the RHEL 7 docs "11.5. Naming Scheme for VLAN Interfaces"
-
-    LP: #1788915
-    LP: #1826608
-    RHBZ: #1861871
-
-Signed-off-by: Eduardo Otubo <otubo@redhat.com>
----
- cloudinit/net/sysconfig.py                     | 32 +++++++++-
- tests/unittests/test_distros/test_netconfig.py | 81 ++++++++++++++++++++++++++
- tests/unittests/test_net.py                    |  4 --
- 3 files changed, 112 insertions(+), 5 deletions(-)
-
-diff --git a/cloudinit/net/sysconfig.py b/cloudinit/net/sysconfig.py
-index c078898..078636a 100644
---- a/cloudinit/net/sysconfig.py
-+++ b/cloudinit/net/sysconfig.py
-@@ -99,6 +99,10 @@ class ConfigMap(object):
-     def __len__(self):
-         return len(self._conf)
- 
-+    def skip_key_value(self, key, val):
-+        """Skip the pair key, value if it matches a certain rule."""
-+        return False
-+
-     def to_string(self):
-         buf = io.StringIO()
-         buf.write(_make_header())
-@@ -106,6 +110,8 @@ class ConfigMap(object):
-             buf.write("\n")
-         for key in sorted(self._conf.keys()):
-             value = self._conf[key]
-+            if self.skip_key_value(key, value):
-+                continue
-             if isinstance(value, bool):
-                 value = self._bool_map[value]
-             if not isinstance(value, str):
-@@ -214,6 +220,7 @@ class NetInterface(ConfigMap):
-         'bond': 'Bond',
-         'bridge': 'Bridge',
-         'infiniband': 'InfiniBand',
-+        'vlan': 'Vlan',
-     }
- 
-     def __init__(self, iface_name, base_sysconf_dir, templates,
-@@ -267,6 +274,11 @@ class NetInterface(ConfigMap):
-             c.routes = self.routes.copy()
-         return c
- 
-+    def skip_key_value(self, key, val):
-+        if key == 'TYPE' and val == 'Vlan':
-+            return True
-+        return False
-+
- 
- class Renderer(renderer.Renderer):
-     """Renders network information in a /etc/sysconfig format."""
-@@ -701,7 +713,16 @@ class Renderer(renderer.Renderer):
-                 iface_cfg['ETHERDEVICE'] = iface_name[:iface_name.rfind('.')]
-             else:
-                 iface_cfg['VLAN'] = True
--                iface_cfg['PHYSDEV'] = iface_name[:iface_name.rfind('.')]
-+                iface_cfg.kind = 'vlan'
-+
-+                rdev = iface['vlan-raw-device']
-+                supported = _supported_vlan_names(rdev, iface['vlan_id'])
-+                if iface_name not in supported:
-+                    LOG.info(
-+                        "Name '%s' for vlan '%s' is not officially supported"
-+                        "by RHEL. Supported: %s",
-+                        iface_name, rdev, ' '.join(supported))
-+                iface_cfg['PHYSDEV'] = rdev
- 
-             iface_subnets = iface.get("subnets", [])
-             route_cfg = iface_cfg.routes
-@@ -909,6 +930,15 @@ class Renderer(renderer.Renderer):
-                             "\n".join(netcfg) + "\n", file_mode)
- 
- 
-+def _supported_vlan_names(rdev, vid):
-+    """Return list of supported names for vlan devices per RHEL doc
-+    11.5. Naming Scheme for VLAN Interfaces."""
-+    return [
-+        v.format(rdev=rdev, vid=int(vid))
-+        for v in ("{rdev}{vid:04}", "{rdev}{vid}",
-+                  "{rdev}.{vid:04}", "{rdev}.{vid}")]
-+
-+
- def available(target=None):
-     sysconfig = available_sysconfig(target=target)
-     nm = available_nm(target=target)
-diff --git a/tests/unittests/test_distros/test_netconfig.py b/tests/unittests/test_distros/test_netconfig.py
-index f9fc3a1..a1df066 100644
---- a/tests/unittests/test_distros/test_netconfig.py
-+++ b/tests/unittests/test_distros/test_netconfig.py
-@@ -541,6 +541,87 @@ class TestNetCfgDistroRedhat(TestNetCfgDistroBase):
-                                V1_NET_CFG_IPV6,
-                                expected_cfgs=expected_cfgs.copy())
- 
-+    def test_vlan_render_unsupported(self):
-+        """Render officially unsupported vlan names."""
-+        cfg = {
-+            'version': 2,
-+            'ethernets': {
-+                'eth0': {'addresses': ["192.10.1.2/24"],
-+                         'match': {'macaddress': "00:16:3e:60:7c:df"}}},
-+            'vlans': {
-+                'infra0': {'addresses': ["10.0.1.2/16"],
-+                           'id': 1001, 'link': 'eth0'}},
-+        }
-+        expected_cfgs = {
-+            self.ifcfg_path('eth0'): dedent("""\
-+                BOOTPROTO=none
-+                DEVICE=eth0
-+                HWADDR=00:16:3e:60:7c:df
-+                IPADDR=192.10.1.2
-+                NETMASK=255.255.255.0
-+                NM_CONTROLLED=no
-+                ONBOOT=yes
-+                TYPE=Ethernet
-+                USERCTL=no
-+                """),
-+            self.ifcfg_path('infra0'): dedent("""\
-+                BOOTPROTO=none
-+                DEVICE=infra0
-+                IPADDR=10.0.1.2
-+                NETMASK=255.255.0.0
-+                NM_CONTROLLED=no
-+                ONBOOT=yes
-+                PHYSDEV=eth0
-+                USERCTL=no
-+                VLAN=yes
-+                """),
-+            self.control_path(): dedent("""\
-+                NETWORKING=yes
-+                """),
-+        }
-+        self._apply_and_verify(
-+            self.distro.apply_network_config, cfg,
-+            expected_cfgs=expected_cfgs)
-+
-+    def test_vlan_render(self):
-+        cfg = {
-+            'version': 2,
-+            'ethernets': {
-+                'eth0': {'addresses': ["192.10.1.2/24"]}},
-+            'vlans': {
-+                'eth0.1001': {'addresses': ["10.0.1.2/16"],
-+                              'id': 1001, 'link': 'eth0'}},
-+        }
-+        expected_cfgs = {
-+            self.ifcfg_path('eth0'): dedent("""\
-+                BOOTPROTO=none
-+                DEVICE=eth0
-+                IPADDR=192.10.1.2
-+                NETMASK=255.255.255.0
-+                NM_CONTROLLED=no
-+                ONBOOT=yes
-+                TYPE=Ethernet
-+                USERCTL=no
-+                """),
-+            self.ifcfg_path('eth0.1001'): dedent("""\
-+                BOOTPROTO=none
-+                DEVICE=eth0.1001
-+                IPADDR=10.0.1.2
-+                NETMASK=255.255.0.0
-+                NM_CONTROLLED=no
-+                ONBOOT=yes
-+                PHYSDEV=eth0
-+                USERCTL=no
-+                VLAN=yes
-+                """),
-+            self.control_path(): dedent("""\
-+                NETWORKING=yes
-+                """),
-+        }
-+        self._apply_and_verify(
-+            self.distro.apply_network_config, cfg,
-+            expected_cfgs=expected_cfgs)
-+
- 
- class TestNetCfgDistroOpensuse(TestNetCfgDistroBase):
- 
-diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py
-index d7a7a65..c033745 100644
---- a/tests/unittests/test_net.py
-+++ b/tests/unittests/test_net.py
-@@ -1656,7 +1656,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true
-                 DHCLIENT_SET_DEFAULT_ROUTE=no
-                 ONBOOT=yes
-                 PHYSDEV=bond0
--                TYPE=Ethernet
-                 USERCTL=no
-                 VLAN=yes"""),
-             'ifcfg-br0': textwrap.dedent("""\
-@@ -1699,7 +1698,6 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true
-                 NETMASK1=255.255.255.0
-                 ONBOOT=yes
-                 PHYSDEV=eth0
--                TYPE=Ethernet
-                 USERCTL=no
-                 VLAN=yes"""),
-             'ifcfg-eth1': textwrap.dedent("""\
-@@ -2302,7 +2300,6 @@ iface bond0 inet6 static
-                 NETMASK1=255.255.255.0
-                 ONBOOT=yes
-                 PHYSDEV=en0
--                TYPE=Ethernet
-                 USERCTL=no
-                 VLAN=yes"""),
-         },
-@@ -3409,7 +3406,6 @@ USERCTL=no
-                 NM_CONTROLLED=no
-                 ONBOOT=yes
-                 PHYSDEV=eno1
--                TYPE=Ethernet
-                 USERCTL=no
-                 VLAN=yes
-                 """)
--- 
-1.8.3.1
-
diff --git a/SOURCES/ci-rhel-cloud.cfg-remove-ssh_genkeytypes-in-settings.py.patch b/SOURCES/ci-rhel-cloud.cfg-remove-ssh_genkeytypes-in-settings.py.patch
new file mode 100644
index 0000000..be1e283
--- /dev/null
+++ b/SOURCES/ci-rhel-cloud.cfg-remove-ssh_genkeytypes-in-settings.py.patch
@@ -0,0 +1,65 @@
+From abf1adeae8211f5acd87dc63b03b2ed995047efd Mon Sep 17 00:00:00 2001
+From: Emanuele Giuseppe Esposito <eesposit@redhat.com>
+Date: Thu, 20 May 2021 08:53:55 +0200
+Subject: [PATCH 1/2] rhel/cloud.cfg: remove ssh_genkeytypes in settings.py and
+ set in cloud.cfg
+
+RH-Author: Emanuele Giuseppe Esposito <eesposit@redhat.com>
+RH-MergeRequest: 10: rhel/cloud.cfg: remove ssh_genkeytypes in settings.py and set in cloud.cfg
+RH-Commit: [1/1] 6da989423b9b6e017afbac2f1af3649b0487310f
+RH-Bugzilla: 1957532
+RH-Acked-by: Eduardo Otubo <otubo@redhat.com>
+RH-Acked-by: Cathy Avery <cavery@redhat.com>
+RH-Acked-by: Vitaly Kuznetsov <vkuznets@redhat.com>
+RH-Acked-by: Mohamed Gamal Morsy <mmorsy@redhat.com>
+
+Currently genkeytypes in cloud.cfg is set to None, so together with
+ssh_deletekeys=1 cloudinit on first boot it will just delete the existing
+keys and not generate new ones.
+
+Just removing that property in cloud.cfg is not enough, because
+settings.py provides another empty default value that will be used
+instead, resulting to no key generated even when the property is not defined.
+
+Removing genkeytypes also in settings.py will default to GENERATE_KEY_NAMES,
+but since we want only 'rsa', 'ecdsa' and 'ed25519', add back genkeytypes in
+cloud.cfg with the above defaults.
+
+Also remove ssh_deletekeys in settings.py as we always need
+to 1 (and it also defaults to 1).
+
+Signed-off-by: Emanuele Giuseppe Esposito <eesposit@redhat.com>
+---
+ cloudinit/settings.py | 2 --
+ rhel/cloud.cfg        | 2 +-
+ 2 files changed, 1 insertion(+), 3 deletions(-)
+
+diff --git a/cloudinit/settings.py b/cloudinit/settings.py
+index 43a1490c..2acf2615 100644
+--- a/cloudinit/settings.py
++++ b/cloudinit/settings.py
+@@ -49,8 +49,6 @@ CFG_BUILTIN = {
+     'def_log_file_mode': 0o600,
+     'log_cfgs': [],
+     'mount_default_fields': [None, None, 'auto', 'defaults,nofail', '0', '2'],
+-    'ssh_deletekeys': False,
+-    'ssh_genkeytypes': [],
+     'syslog_fix_perms': [],
+     'system_info': {
+         'paths': {
+diff --git a/rhel/cloud.cfg b/rhel/cloud.cfg
+index 9ecba215..cbee197a 100644
+--- a/rhel/cloud.cfg
++++ b/rhel/cloud.cfg
+@@ -7,7 +7,7 @@ ssh_pwauth:   0
+ mount_default_fields: [~, ~, 'auto', 'defaults,nofail,x-systemd.requires=cloud-init.service', '0', '2']
+ resize_rootfs_tmp: /dev
+ ssh_deletekeys:   1
+-ssh_genkeytypes:  ~
++ssh_genkeytypes:  ['rsa', 'ecdsa', 'ed25519']
+ syslog_fix_perms: ~
+ disable_vmware_customization: false
+ 
+-- 
+2.27.0
+
diff --git a/SOURCES/ci-ssh-util-allow-cloudinit-to-merge-all-ssh-keys-into-.patch b/SOURCES/ci-ssh-util-allow-cloudinit-to-merge-all-ssh-keys-into-.patch
new file mode 100644
index 0000000..bdec823
--- /dev/null
+++ b/SOURCES/ci-ssh-util-allow-cloudinit-to-merge-all-ssh-keys-into-.patch
@@ -0,0 +1,653 @@
+From aeab67600eb2d5e483812620b56ce5fb031a57d6 Mon Sep 17 00:00:00 2001
+From: Emanuele Giuseppe Esposito <eesposit@redhat.com>
+Date: Mon, 12 Jul 2021 21:47:37 +0200
+Subject: [PATCH] ssh-util: allow cloudinit to merge all ssh keys into a custom
+ user file, defined in AuthorizedKeysFile (#937)
+
+RH-Author: Emanuele Giuseppe Esposito <eesposit@redhat.com>
+RH-MergeRequest: 25: ssh-util: allow cloudinit to merge all ssh keys into a custom user file, defined in AuthorizedKeysFile (#937)
+RH-Commit: [1/1] 27bbe94f3b9dd8734865766bd30b06cff83383ab (eesposit/cloud-init)
+RH-Bugzilla: 1862967
+RH-Acked-by: Vitaly Kuznetsov <vkuznets@redhat.com>
+RH-Acked-by: Mohamed Gamal Morsy <mmorsy@redhat.com>
+
+TESTED: By me and QA
+BREW: 38030830
+
+Conflicts: upstream patch modifies tests/integration_tests/util.py, that is
+not present in RHEL.
+
+commit 9b52405c6f0de5e00d5ee9c1d13540425d8f6bf5
+Author: Emanuele Giuseppe Esposito <eesposit@redhat.com>
+Date:   Mon Jul 12 20:21:02 2021 +0200
+
+    ssh-util: allow cloudinit to merge all ssh keys into a custom user file, defined in AuthorizedKeysFile (#937)
+
+    This patch aims to fix LP1911680, by analyzing the files provided
+    in sshd_config and merge all keys into an user-specific file. Also
+    introduces additional tests to cover this specific case.
+
+    The file is picked by analyzing the path given in AuthorizedKeysFile.
+
+    If it points inside the current user folder (path is /home/user/*), it
+    means it is an user-specific file, so we can copy all user-keys there.
+    If it contains a %u or %h, it means that there will be a specific
+    authorized_keys file for each user, so we can copy all user-keys there.
+    If no path points to an user-specific file, for example when only
+    /etc/ssh/authorized_keys is given, default to ~/.ssh/authorized_keys.
+    Note that if there are more than a single user-specific file, the last
+    one will be picked.
+
+    Signed-off-by: Emanuele Giuseppe Esposito <eesposit@redhat.com>
+    Co-authored-by: James Falcon <therealfalcon@gmail.com>
+
+    LP: #1911680
+    RHBZ:1862967
+
+Signed-off-by: Emanuele Giuseppe Esposito <eesposit@redhat.com>
+---
+ cloudinit/ssh_util.py                         |  22 +-
+ .../assets/keys/id_rsa.test1                  |  38 +++
+ .../assets/keys/id_rsa.test1.pub              |   1 +
+ .../assets/keys/id_rsa.test2                  |  38 +++
+ .../assets/keys/id_rsa.test2.pub              |   1 +
+ .../assets/keys/id_rsa.test3                  |  38 +++
+ .../assets/keys/id_rsa.test3.pub              |   1 +
+ .../modules/test_ssh_keysfile.py              |  85 ++++++
+ tests/unittests/test_sshutil.py               | 246 +++++++++++++++++-
+ 9 files changed, 456 insertions(+), 14 deletions(-)
+ create mode 100644 tests/integration_tests/assets/keys/id_rsa.test1
+ create mode 100644 tests/integration_tests/assets/keys/id_rsa.test1.pub
+ create mode 100644 tests/integration_tests/assets/keys/id_rsa.test2
+ create mode 100644 tests/integration_tests/assets/keys/id_rsa.test2.pub
+ create mode 100644 tests/integration_tests/assets/keys/id_rsa.test3
+ create mode 100644 tests/integration_tests/assets/keys/id_rsa.test3.pub
+ create mode 100644 tests/integration_tests/modules/test_ssh_keysfile.py
+
+diff --git a/cloudinit/ssh_util.py b/cloudinit/ssh_util.py
+index c08042d6..89057262 100644
+--- a/cloudinit/ssh_util.py
++++ b/cloudinit/ssh_util.py
+@@ -252,13 +252,15 @@ def render_authorizedkeysfile_paths(value, homedir, username):
+ def extract_authorized_keys(username, sshd_cfg_file=DEF_SSHD_CFG):
+     (ssh_dir, pw_ent) = users_ssh_info(username)
+     default_authorizedkeys_file = os.path.join(ssh_dir, 'authorized_keys')
++    user_authorizedkeys_file = default_authorizedkeys_file
+     auth_key_fns = []
+     with util.SeLinuxGuard(ssh_dir, recursive=True):
+         try:
+             ssh_cfg = parse_ssh_config_map(sshd_cfg_file)
++            key_paths = ssh_cfg.get("authorizedkeysfile",
++                                    "%h/.ssh/authorized_keys")
+             auth_key_fns = render_authorizedkeysfile_paths(
+-                ssh_cfg.get("authorizedkeysfile", "%h/.ssh/authorized_keys"),
+-                pw_ent.pw_dir, username)
++                key_paths, pw_ent.pw_dir, username)
+ 
+         except (IOError, OSError):
+             # Give up and use a default key filename
+@@ -267,8 +269,22 @@ def extract_authorized_keys(username, sshd_cfg_file=DEF_SSHD_CFG):
+                         "config from %r, using 'AuthorizedKeysFile' file "
+                         "%r instead", DEF_SSHD_CFG, auth_key_fns[0])
+ 
++    # check if one of the keys is the user's one
++    for key_path, auth_key_fn in zip(key_paths.split(), auth_key_fns):
++        if any([
++            '%u' in key_path,
++            '%h' in key_path,
++            auth_key_fn.startswith('{}/'.format(pw_ent.pw_dir))
++        ]):
++            user_authorizedkeys_file = auth_key_fn
++
++    if user_authorizedkeys_file != default_authorizedkeys_file:
++        LOG.debug(
++            "AuthorizedKeysFile has an user-specific authorized_keys, "
++            "using %s", user_authorizedkeys_file)
++
+     # always store all the keys in the user's private file
+-    return (default_authorizedkeys_file, parse_authorized_keys(auth_key_fns))
++    return (user_authorizedkeys_file, parse_authorized_keys(auth_key_fns))
+ 
+ 
+ def setup_user_keys(keys, username, options=None):
+diff --git a/tests/integration_tests/assets/keys/id_rsa.test1 b/tests/integration_tests/assets/keys/id_rsa.test1
+new file mode 100644
+index 00000000..bd4c822e
+--- /dev/null
++++ b/tests/integration_tests/assets/keys/id_rsa.test1
+@@ -0,0 +1,38 @@
++-----BEGIN OPENSSH PRIVATE KEY-----
++b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn
++NhAAAAAwEAAQAAAYEAtRlG96aJ23URvAgO/bBsuLl+lquc350aSwV98/i8vlvOn5GVcHye
++t/rXQg4lZ4s0owG3kWyQFY8nvTk+G+UNU8fN0anAzBDi+4MzsejkF9scjTMFmXVrIpICqV
++3bYQNjPv6r+ubQdkD01du3eB9t5/zl84gtshp0hBdofyz8u1/A25s7fVU67GyI7PdKvaS+
++yvJSInZnb2e9VQzfJC+qAnN7gUZatBKjdgUtJeiUUeDaVnaS17b0aoT9iBO0sIcQtOTBlY
++lCjFt1TAMLZ64Hj3SfGZB7Yj0Z+LzFB2IWX1zzsjI68YkYPKOSL/NYhQU9e55kJQ7WnngN
++HY/2n/A7dNKSFDmgM5c9IWgeZ7fjpsfIYAoJ/CAxFIND+PEHd1gCS6xoEhaUVyh5WH/Xkw
++Kv1nx4AiZ2BFCE+75kySRLZUJ+5y0r3DU5ktMXeURzVIP7pu0R8DCul+GU+M/+THyWtAEO
++geaNJ6fYpo2ipDhbmTYt3kk2lMIapRxGBFs+37sdAAAFgGGJssNhibLDAAAAB3NzaC1yc2
++EAAAGBALUZRvemidt1EbwIDv2wbLi5fparnN+dGksFffP4vL5bzp+RlXB8nrf610IOJWeL
++NKMBt5FskBWPJ705PhvlDVPHzdGpwMwQ4vuDM7Ho5BfbHI0zBZl1ayKSAqld22EDYz7+q/
++rm0HZA9NXbt3gfbef85fOILbIadIQXaH8s/LtfwNubO31VOuxsiOz3Sr2kvsryUiJ2Z29n
++vVUM3yQvqgJze4FGWrQSo3YFLSXolFHg2lZ2kte29GqE/YgTtLCHELTkwZWJQoxbdUwDC2
++euB490nxmQe2I9Gfi8xQdiFl9c87IyOvGJGDyjki/zWIUFPXueZCUO1p54DR2P9p/wO3TS
++khQ5oDOXPSFoHme346bHyGAKCfwgMRSDQ/jxB3dYAkusaBIWlFcoeVh/15MCr9Z8eAImdg
++RQhPu+ZMkkS2VCfuctK9w1OZLTF3lEc1SD+6btEfAwrpfhlPjP/kx8lrQBDoHmjSen2KaN
++oqQ4W5k2Ld5JNpTCGqUcRgRbPt+7HQAAAAMBAAEAAAGBAJJCTOd70AC2ptEGbR0EHHqADT
++Wgefy7A94tHFEqxTy0JscGq/uCGimaY7kMdbcPXT59B4VieWeAC2cuUPP0ZHQSfS5ke7oT
++tU3N47U+0uBVbNS4rUAH7bOo2o9wptnOA5x/z+O+AARRZ6tEXQOd1oSy4gByLf2Wkh2QTi
++vP6Hln1vlFgKEzcXg6G8fN3MYWxKRhWmZM3DLERMvorlqqSBLcs5VvfZfLKcsKWTExioAq
++KgwEjYm8T9+rcpsw1xBus3j9k7wCI1Sus6PCDjq0pcYKLMYM7p8ygnU2tRYrOztdIxgWRA
++w/1oenm1Mqq2tV5xJcBCwCLOGe6SFwkIRywOYc57j5McH98Xhhg9cViyyBdXy/baF0mro+
++qPhOsWDxqwD4VKZ9UmQ6O8kPNKcc7QcIpFJhcO0g9zbp/MT0KueaWYrTKs8y4lUkTT7Xz6
+++MzlR122/JwlAbBo6Y2kWtB+y+XwBZ0BfyJsm2czDhKm7OI5KfuBNhq0tFfKwOlYBq4QAA
++AMAyvUof1R8LLISkdO3EFTKn5RGNkPPoBJmGs6LwvU7NSjjLj/wPQe4jsIBc585tvbrddp
++60h72HgkZ5tqOfdeBYOKqX0qQQBHUEvI6M+NeQTQRev8bCHMLXQ21vzpClnrwNzlja359E
++uTRfiPRwIlyPLhOUiClBDSAnBI9h82Hkk3zzsQ/xGfsPB7iOjRbW69bMRSVCRpeweCVmWC
++77DTsEOq69V2TdljhQNIXE5OcOWonIlfgPiI74cdd+dLhzc/AAAADBAO1/JXd2kYiRyNkZ
++aXTLcwiSgBQIYbobqVP3OEtTclr0P1JAvby3Y4cCaEhkenx+fBqgXAku5lKM+U1Q9AEsMk
++cjIhaDpb43rU7GPjMn4zHwgGsEKd5pC1yIQ2PlK+cHanAdsDjIg+6RR+fuvid/mBeBOYXb
++Py0sa3HyekLJmCdx4UEyNASoiNaGFLQVAqo+RACsXy6VMxFH5dqDYlvwrfUQLwxJmse9Vb
++GEuuPAsklNugZqssC2XOIujFVUpslduQAAAMEAwzVHQVtsc3icCSzEAARpDTUdTbI29OhB
++/FMBnjzS9/3SWfLuBOSm9heNCHs2jdGNb8cPdKZuY7S9Fx6KuVUPyTbSSYkjj0F4fTeC9g
++0ym4p4UWYdF67WSWwLORkaG8K0d+G/CXkz8hvKUg6gcZWKBHAE1ROrHu1nsc8v7mkiKq4I
++bnTw5Q9TgjbWcQWtgPq0wXyyl/K8S1SFdkMCTOHDD0RQ+jTV2WNGVwFTodIRHenX+Rw2g4
++CHbTWbsFrHR1qFAAAACmphbWVzQG5ld3Q=
++-----END OPENSSH PRIVATE KEY-----
+diff --git a/tests/integration_tests/assets/keys/id_rsa.test1.pub b/tests/integration_tests/assets/keys/id_rsa.test1.pub
+new file mode 100644
+index 00000000..3d2e26e1
+--- /dev/null
++++ b/tests/integration_tests/assets/keys/id_rsa.test1.pub
+@@ -0,0 +1 @@
++ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC1GUb3ponbdRG8CA79sGy4uX6Wq5zfnRpLBX3z+Ly+W86fkZVwfJ63+tdCDiVnizSjAbeRbJAVjye9OT4b5Q1Tx83RqcDMEOL7gzOx6OQX2xyNMwWZdWsikgKpXdthA2M+/qv65tB2QPTV27d4H23n/OXziC2yGnSEF2h/LPy7X8Dbmzt9VTrsbIjs90q9pL7K8lIidmdvZ71VDN8kL6oCc3uBRlq0EqN2BS0l6JRR4NpWdpLXtvRqhP2IE7SwhxC05MGViUKMW3VMAwtnrgePdJ8ZkHtiPRn4vMUHYhZfXPOyMjrxiRg8o5Iv81iFBT17nmQlDtaeeA0dj/af8Dt00pIUOaAzlz0haB5nt+Omx8hgCgn8IDEUg0P48Qd3WAJLrGgSFpRXKHlYf9eTAq/WfHgCJnYEUIT7vmTJJEtlQn7nLSvcNTmS0xd5RHNUg/um7RHwMK6X4ZT4z/5MfJa0AQ6B5o0np9imjaKkOFuZNi3eSTaUwhqlHEYEWz7fux0= test1@host
+diff --git a/tests/integration_tests/assets/keys/id_rsa.test2 b/tests/integration_tests/assets/keys/id_rsa.test2
+new file mode 100644
+index 00000000..5854d901
+--- /dev/null
++++ b/tests/integration_tests/assets/keys/id_rsa.test2
+@@ -0,0 +1,38 @@
++-----BEGIN OPENSSH PRIVATE KEY-----
++b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn
++NhAAAAAwEAAQAAAYEAvK50D2PWOc4ikyHVRJS6tDhqzjL5cKiivID4p1X8BYCVw83XAEGO
++LnItUyVXHNADlh6fpVq1NY6A2JVtygoPF6ZFx8ph7IWMmnhDdnxLLyGsbhd1M1tiXJD/R+
++3WnGHRJ4PKrQavMLgqHRrieV3QVVfjFSeo6jX/4TruP6ZmvITMZWJrXaGphxJ/pPykEdkO
++i8AmKU9FNviojyPS2nNtj9B/635IdgWvrd7Vf5Ycsw9MR55LWSidwa856RH62Yl6LpEGTH
++m1lJiMk1u88JPSqvohhaUkLKkFpcQwcB0m76W1KOyllJsmX8bNXrlZsI+WiiYI7Xl5vQm2
++17DEuNeavtPAtDMxu8HmTg2UJ55Naxehbfe2lx2k5kYGGw3i1O1OVN2pZ2/OB71LucYd/5
++qxPaz03wswcGOJYGPkNc40vdES/Scc7Yt8HsnZuzqkyOgzn0HiUCzoYUYLYTpLf+yGmwxS
++yAEY056aOfkCsboKHOKiOmlJxNaZZFQkX1evep4DAAAFgC7HMbUuxzG1AAAAB3NzaC1yc2
++EAAAGBALyudA9j1jnOIpMh1USUurQ4as4y+XCooryA+KdV/AWAlcPN1wBBji5yLVMlVxzQ
++A5Yen6VatTWOgNiVbcoKDxemRcfKYeyFjJp4Q3Z8Sy8hrG4XdTNbYlyQ/0ft1pxh0SeDyq
++0GrzC4Kh0a4nld0FVX4xUnqOo1/+E67j+mZryEzGVia12hqYcSf6T8pBHZDovAJilPRTb4
++qI8j0tpzbY/Qf+t+SHYFr63e1X+WHLMPTEeeS1koncGvOekR+tmJei6RBkx5tZSYjJNbvP
++CT0qr6IYWlJCypBaXEMHAdJu+ltSjspZSbJl/GzV65WbCPloomCO15eb0JttewxLjXmr7T
++wLQzMbvB5k4NlCeeTWsXoW33tpcdpOZGBhsN4tTtTlTdqWdvzge9S7nGHf+asT2s9N8LMH
++BjiWBj5DXONL3REv0nHO2LfB7J2bs6pMjoM59B4lAs6GFGC2E6S3/shpsMUsgBGNOemjn5
++ArG6ChziojppScTWmWRUJF9Xr3qeAwAAAAMBAAEAAAGASj/kkEHbhbfmxzujL2/P4Sfqb+
++aDXqAeGkwujbs6h/fH99vC5ejmSMTJrVSeaUo6fxLiBDIj6UWA0rpLEBzRP59BCpRL4MXV
++RNxav/+9nniD4Hb+ug0WMhMlQmsH71ZW9lPYqCpfOq7ec8GmqdgPKeaCCEspH7HMVhfYtd
++eHylwAC02lrpz1l5/h900sS5G9NaWR3uPA+xbzThDs4uZVkSidjlCNt1QZhDSSk7jA5n34
++qJ5UTGu9WQDZqyxWKND+RIyQuFAPGQyoyCC1FayHO2sEhT5qHuumL14Mn81XpzoXFoKyql
++rhBDe+pHhKArBYt92Evch0k1ABKblFxtxLXcvk4Fs7pHi+8k4+Cnazej2kcsu1kURlMZJB
++w2QT/8BV4uImbH05LtyscQuwGzpIoxqrnHrvg5VbohStmhoOjYybzqqW3/M0qhkn5JgTiy
++dJcHRJisRnAcmbmEchYtLDi6RW1e022H4I9AFXQqyr5HylBq6ugtWcFCsrcX8ibZ8xAAAA
++wQCAOPgwae6yZLkrYzRfbxZtGKNmhpI0EtNSDCHYuQQapFZJe7EFENs/VAaIiiut0yajGj
++c3aoKcwGIoT8TUM8E3GSNW6+WidUOC7H6W+/6N2OYZHRBACGz820xO+UBCl2oSk+dLBlfr
++IQzBGUWn5uVYCs0/2nxfCdFyHtMK8dMF/ypbdG+o1rXz5y9b7PVG6Mn+o1Rjsdkq7VERmy
++Pukd8hwATOIJqoKl3TuFyBeYFLqe+0e7uTeswQFw17PF31VjAAAADBAOpJRQb8c6qWqsvv
++vkve0uMuL0DfWW0G6+SxjPLcV6aTWL5xu0Grd8uBxDkkHU/CDrAwpchXyuLsvbw21Eje/u
++U5k9nLEscWZwcX7odxlK+EfAY2Bf5+Hd9bH5HMzTRJH8KkWK1EppOLPyiDxz4LZGzPLVyv
++/1PgSuvXkSWk1KIE4SvSemyxGX2tPVI6uO+URqevfnPOS1tMB7BMQlgkR6eh4bugx9UYx9
++mwlXonNa4dN0iQxZ7N4rKFBbT/uyB2bQAAAMEAzisnkD8k9Tn8uyhxpWLHwb03X4ZUUHDV
++zu15e4a8dZ+mM8nHO986913Xz5JujlJKkGwFTvgWkIiR2zqTEauZHARH7gANpaweTm6lPd
++E4p2S0M3ulY7xtp9lCFIrDhMPPkGq8SFZB6qhgucHcZSRLq6ZDou3S2IdNOzDTpBtkhRCS
++0zFcdTLh3zZweoy8HGbW36bwB6s1CIL76Pd4F64i0Ms9CCCU6b+E5ArFhYQIsXiDbgHWbD
++tZRSm2GEgnDGAvAAAACmphbWVzQG5ld3Q=
++-----END OPENSSH PRIVATE KEY-----
+diff --git a/tests/integration_tests/assets/keys/id_rsa.test2.pub b/tests/integration_tests/assets/keys/id_rsa.test2.pub
+new file mode 100644
+index 00000000..f3831a57
+--- /dev/null
++++ b/tests/integration_tests/assets/keys/id_rsa.test2.pub
+@@ -0,0 +1 @@
++ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC8rnQPY9Y5ziKTIdVElLq0OGrOMvlwqKK8gPinVfwFgJXDzdcAQY4uci1TJVcc0AOWHp+lWrU1joDYlW3KCg8XpkXHymHshYyaeEN2fEsvIaxuF3UzW2JckP9H7dacYdEng8qtBq8wuCodGuJ5XdBVV+MVJ6jqNf/hOu4/pma8hMxlYmtdoamHEn+k/KQR2Q6LwCYpT0U2+KiPI9Lac22P0H/rfkh2Ba+t3tV/lhyzD0xHnktZKJ3BrznpEfrZiXoukQZMebWUmIyTW7zwk9Kq+iGFpSQsqQWlxDBwHSbvpbUo7KWUmyZfxs1euVmwj5aKJgjteXm9CbbXsMS415q+08C0MzG7weZODZQnnk1rF6Ft97aXHaTmRgYbDeLU7U5U3alnb84HvUu5xh3/mrE9rPTfCzBwY4lgY+Q1zjS90RL9Jxzti3weydm7OqTI6DOfQeJQLOhhRgthOkt/7IabDFLIARjTnpo5+QKxugoc4qI6aUnE1plkVCRfV696ngM= test2@host
+diff --git a/tests/integration_tests/assets/keys/id_rsa.test3 b/tests/integration_tests/assets/keys/id_rsa.test3
+new file mode 100644
+index 00000000..2596c762
+--- /dev/null
++++ b/tests/integration_tests/assets/keys/id_rsa.test3
+@@ -0,0 +1,38 @@
++-----BEGIN OPENSSH PRIVATE KEY-----
++b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn
++NhAAAAAwEAAQAAAYEApPG4MdkYQKD57/qreFrh9GRC22y66qZOWZWRjC887rrbvBzO69hV
++yJpTIXleJEvpWiHYcjMR5G6NNFsnNtZ4fxDqmSc4vcFj53JsE/XNqLKq6psXadCb5vkNpG
++bxA+Z5bJlzJ969PgJIIEbgc86sei4kgR2MuPWqtZbY5GkpNCTqWuLYeFK+14oFruA2nyWH
++9MOIRDHK/d597psHy+LTMtymO7ZPhO571abKw6jvvwiSeDxVE9kV7KAQIuM9/S3gftvgQQ
++ron3GL34pgmIabdSGdbfHqGDooryJhlbquJZELBN236KgRNTCAjVvUzjjQr1eRP3xssGwV
++O6ECBGCQLl/aYogAgtwnwj9iXqtfiLK3EwlgjquU4+JQ0CVtLhG3gIZB+qoMThco0pmHTr
++jtfQCwrztsBBFunSa2/CstuV1mQ5O5ZrZ6ACo9yPRBNkns6+CiKdtMtCtzi3k2RDz9jpYm
++Pcak03Lr7IkdC1Tp6+jA+//yPHSO1o4CqW89IQzNAAAFgEUd7lZFHe5WAAAAB3NzaC1yc2
++EAAAGBAKTxuDHZGECg+e/6q3ha4fRkQttsuuqmTlmVkYwvPO6627wczuvYVciaUyF5XiRL
++6Voh2HIzEeRujTRbJzbWeH8Q6pknOL3BY+dybBP1zaiyquqbF2nQm+b5DaRm8QPmeWyZcy
++fevT4CSCBG4HPOrHouJIEdjLj1qrWW2ORpKTQk6lri2HhSvteKBa7gNp8lh/TDiEQxyv3e
++fe6bB8vi0zLcpju2T4Tue9WmysOo778Ikng8VRPZFeygECLjPf0t4H7b4EEK6J9xi9+KYJ
++iGm3UhnW3x6hg6KK8iYZW6riWRCwTdt+ioETUwgI1b1M440K9XkT98bLBsFTuhAgRgkC5f
++2mKIAILcJ8I/Yl6rX4iytxMJYI6rlOPiUNAlbS4Rt4CGQfqqDE4XKNKZh0647X0AsK87bA
++QRbp0mtvwrLbldZkOTuWa2egAqPcj0QTZJ7OvgoinbTLQrc4t5NkQ8/Y6WJj3GpNNy6+yJ
++HQtU6evowPv/8jx0jtaOAqlvPSEMzQAAAAMBAAEAAAGAGaqbdPZJNdVWzyb8g6/wtSzc0n
++Qq6dSTIJGLonq/So69HpqFAGIbhymsger24UMGvsXBfpO/1wH06w68HWZmPa+OMeLOi4iK
++WTuO4dQ/+l5DBlq32/lgKSLcIpb6LhcxEdsW9j9Mx1dnjc45owun/yMq/wRwH1/q/nLIsV
++JD3R9ZcGcYNDD8DWIm3D17gmw+qbG7hJES+0oh4n0xS2KyZpm7LFOEMDVEA8z+hE/HbryQ
++vjD1NC91n+qQWD1wKfN3WZDRwip3z1I5VHMpvXrA/spHpa9gzHK5qXNmZSz3/dfA1zHjCR
++2dHjJnrIUH8nyPfw8t+COC+sQBL3Nr0KUWEFPRM08cOcQm4ctzg17aDIZBONjlZGKlReR8
++1zfAw84Q70q2spLWLBLXSFblHkaOfijEbejIbaz2UUEQT27WD7RHAORdQlkx7eitk66T9d
++DzIq/cpYhm5Fs8KZsh3PLldp9nsHbD2Oa9J9LJyI4ryuIW0mVwRdvPSiiYi3K+mDCpAAAA
++wBe+ugEEJ+V7orb1f4Zez0Bd4FNkEc52WZL4CWbaCtM+ZBg5KnQ6xW14JdC8IS9cNi/I5P
++yLsBvG4bWPLGgQruuKY6oLueD6BFnKjqF6ACUCiSQldh4BAW1nYc2U48+FFvo3ZQyudFSy
++QEFlhHmcaNMDo0AIJY5Xnq2BG3nEX7AqdtZ8hhenHwLCRQJatDwSYBHDpSDdh9vpTnGp/2
++0jBz25Ko4UANzvSAc3sA4yN3jfpoM366TgdNf8x3g1v7yljQAAAMEA0HSQjzH5nhEwB58k
++mYYxnBYp1wb86zIuVhAyjZaeinvBQSTmLow8sXIHcCVuD3CgBezlU2SX5d9YuvRU9rcthi
++uzn4wWnbnzYy4SwzkMJXchUAkumFVD8Hq5TNPh2Z+033rLLE08EhYypSeVpuzdpFoStaS9
++3DUZA2bR/zLZI9MOVZRUcYImNegqIjOYHY8Sbj3/0QPV6+WpUJFMPvvedWhfaOsRMTA6nr
++VLG4pxkrieVl0UtuRGbzD/exXhXVi7AAAAwQDKkJj4ez/+KZFYlZQKiV0BrfUFcgS6ElFM
++2CZIEagCtu8eedrwkNqx2FUX33uxdvUTr4c9I3NvWeEEGTB9pgD4lh1x/nxfuhyGXtimFM
++GnznGV9oyz0DmKlKiKSEGwWf5G+/NiiCwwVJ7wsQQm7TqNtkQ9b8MhWWXC7xlXKUs7dmTa
++e8AqAndCCMEnbS1UQFO/R5PNcZXkFWDggLQ/eWRYKlrXgdnUgH6h0saOcViKpNJBUXb3+x
++eauhOY52PS/BcAAAAKamFtZXNAbmV3dAE=
++-----END OPENSSH PRIVATE KEY-----
+diff --git a/tests/integration_tests/assets/keys/id_rsa.test3.pub b/tests/integration_tests/assets/keys/id_rsa.test3.pub
+new file mode 100644
+index 00000000..057db632
+--- /dev/null
++++ b/tests/integration_tests/assets/keys/id_rsa.test3.pub
+@@ -0,0 +1 @@
++ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCk8bgx2RhAoPnv+qt4WuH0ZELbbLrqpk5ZlZGMLzzuutu8HM7r2FXImlMheV4kS+laIdhyMxHkbo00Wyc21nh/EOqZJzi9wWPncmwT9c2osqrqmxdp0Jvm+Q2kZvED5nlsmXMn3r0+AkggRuBzzqx6LiSBHYy49aq1ltjkaSk0JOpa4th4Ur7XigWu4DafJYf0w4hEMcr93n3umwfL4tMy3KY7tk+E7nvVpsrDqO+/CJJ4PFUT2RXsoBAi4z39LeB+2+BBCuifcYvfimCYhpt1IZ1t8eoYOiivImGVuq4lkQsE3bfoqBE1MICNW9TOONCvV5E/fGywbBU7oQIEYJAuX9piiACC3CfCP2Jeq1+IsrcTCWCOq5Tj4lDQJW0uEbeAhkH6qgxOFyjSmYdOuO19ALCvO2wEEW6dJrb8Ky25XWZDk7lmtnoAKj3I9EE2Sezr4KIp20y0K3OLeTZEPP2OliY9xqTTcuvsiR0LVOnr6MD7//I8dI7WjgKpbz0hDM0= test3@host
+diff --git a/tests/integration_tests/modules/test_ssh_keysfile.py b/tests/integration_tests/modules/test_ssh_keysfile.py
+new file mode 100644
+index 00000000..f82d7649
+--- /dev/null
++++ b/tests/integration_tests/modules/test_ssh_keysfile.py
+@@ -0,0 +1,85 @@
++import paramiko
++import pytest
++from io import StringIO
++from paramiko.ssh_exception import SSHException
++
++from tests.integration_tests.instances import IntegrationInstance
++from tests.integration_tests.util import get_test_rsa_keypair
++
++TEST_USER1_KEYS = get_test_rsa_keypair('test1')
++TEST_USER2_KEYS = get_test_rsa_keypair('test2')
++TEST_DEFAULT_KEYS = get_test_rsa_keypair('test3')
++
++USERDATA = """\
++#cloud-config
++bootcmd:
++ - sed -i 's;#AuthorizedKeysFile.*;AuthorizedKeysFile /etc/ssh/authorized_keys %h/.ssh/authorized_keys2;' /etc/ssh/sshd_config
++ssh_authorized_keys:
++ - {default}
++users:
++- default
++- name: test_user1
++  ssh_authorized_keys:
++    - {user1}
++- name: test_user2
++  ssh_authorized_keys:
++    - {user2}
++""".format(  # noqa: E501
++    default=TEST_DEFAULT_KEYS.public_key,
++    user1=TEST_USER1_KEYS.public_key,
++    user2=TEST_USER2_KEYS.public_key,
++)
++
++
++@pytest.mark.ubuntu
++@pytest.mark.user_data(USERDATA)
++def test_authorized_keys(client: IntegrationInstance):
++    expected_keys = [
++        ('test_user1', '/home/test_user1/.ssh/authorized_keys2',
++         TEST_USER1_KEYS),
++        ('test_user2', '/home/test_user2/.ssh/authorized_keys2',
++         TEST_USER2_KEYS),
++        ('ubuntu', '/home/ubuntu/.ssh/authorized_keys2',
++         TEST_DEFAULT_KEYS),
++        ('root', '/root/.ssh/authorized_keys2', TEST_DEFAULT_KEYS),
++    ]
++
++    for user, filename, keys in expected_keys:
++        contents = client.read_from_file(filename)
++        if user in ['ubuntu', 'root']:
++            # Our personal public key gets added by pycloudlib
++            lines = contents.split('\n')
++            assert len(lines) == 2
++            assert keys.public_key.strip() in contents
++        else:
++            assert contents.strip() == keys.public_key.strip()
++
++        # Ensure we can actually connect
++        ssh = paramiko.SSHClient()
++        ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
++        paramiko_key = paramiko.RSAKey.from_private_key(StringIO(
++            keys.private_key))
++
++        # Will fail with AuthenticationException if
++        # we cannot connect
++        ssh.connect(
++            client.instance.ip,
++            username=user,
++            pkey=paramiko_key,
++            look_for_keys=False,
++            allow_agent=False,
++        )
++
++        # Ensure other uses can't connect using our key
++        other_users = [u[0] for u in expected_keys if u[2] != keys]
++        for other_user in other_users:
++            with pytest.raises(SSHException):
++                print('trying to connect as {} with key from {}'.format(
++                    other_user, user))
++                ssh.connect(
++                    client.instance.ip,
++                    username=other_user,
++                    pkey=paramiko_key,
++                    look_for_keys=False,
++                    allow_agent=False,
++                )
+diff --git a/tests/unittests/test_sshutil.py b/tests/unittests/test_sshutil.py
+index fd1d1bac..bcb8044f 100644
+--- a/tests/unittests/test_sshutil.py
++++ b/tests/unittests/test_sshutil.py
+@@ -570,20 +570,33 @@ class TestBasicAuthorizedKeyParse(test_helpers.CiTestCase):
+             ssh_util.render_authorizedkeysfile_paths(
+                 "%h/.keys", "/homedirs/bobby", "bobby"))
+ 
++    def test_all(self):
++        self.assertEqual(
++            ["/homedirs/bobby/.keys", "/homedirs/bobby/.secret/keys",
++             "/keys/path1", "/opt/bobby/keys"],
++            ssh_util.render_authorizedkeysfile_paths(
++                "%h/.keys .secret/keys /keys/path1 /opt/%u/keys",
++                "/homedirs/bobby", "bobby"))
++
+ 
+ class TestMultipleSshAuthorizedKeysFile(test_helpers.CiTestCase):
+ 
+     @patch("cloudinit.ssh_util.pwd.getpwnam")
+     def test_multiple_authorizedkeys_file_order1(self, m_getpwnam):
+-        fpw = FakePwEnt(pw_name='bobby', pw_dir='/home2/bobby')
++        fpw = FakePwEnt(pw_name='bobby', pw_dir='/tmp/home2/bobby')
+         m_getpwnam.return_value = fpw
+-        authorized_keys = self.tmp_path('authorized_keys')
++        user_ssh_folder = "%s/.ssh" % fpw.pw_dir
++
++        # /tmp/home2/bobby/.ssh/authorized_keys = rsa
++        authorized_keys = self.tmp_path('authorized_keys', dir=user_ssh_folder)
+         util.write_file(authorized_keys, VALID_CONTENT['rsa'])
+ 
+-        user_keys = self.tmp_path('user_keys')
++        # /tmp/home2/bobby/.ssh/user_keys = dsa
++        user_keys = self.tmp_path('user_keys', dir=user_ssh_folder)
+         util.write_file(user_keys, VALID_CONTENT['dsa'])
+ 
+-        sshd_config = self.tmp_path('sshd_config')
++        # /tmp/sshd_config
++        sshd_config = self.tmp_path('sshd_config', dir="/tmp")
+         util.write_file(
+             sshd_config,
+             "AuthorizedKeysFile %s %s" % (authorized_keys, user_keys)
+@@ -593,33 +606,244 @@ class TestMultipleSshAuthorizedKeysFile(test_helpers.CiTestCase):
+             fpw.pw_name, sshd_config)
+         content = ssh_util.update_authorized_keys(auth_key_entries, [])
+ 
+-        self.assertEqual("%s/.ssh/authorized_keys" % fpw.pw_dir, auth_key_fn)
++        self.assertEqual(user_keys, auth_key_fn)
+         self.assertTrue(VALID_CONTENT['rsa'] in content)
+         self.assertTrue(VALID_CONTENT['dsa'] in content)
+ 
+     @patch("cloudinit.ssh_util.pwd.getpwnam")
+     def test_multiple_authorizedkeys_file_order2(self, m_getpwnam):
+-        fpw = FakePwEnt(pw_name='suzie', pw_dir='/home/suzie')
++        fpw = FakePwEnt(pw_name='suzie', pw_dir='/tmp/home/suzie')
+         m_getpwnam.return_value = fpw
+-        authorized_keys = self.tmp_path('authorized_keys')
++        user_ssh_folder = "%s/.ssh" % fpw.pw_dir
++
++        # /tmp/home/suzie/.ssh/authorized_keys = rsa
++        authorized_keys = self.tmp_path('authorized_keys', dir=user_ssh_folder)
+         util.write_file(authorized_keys, VALID_CONTENT['rsa'])
+ 
+-        user_keys = self.tmp_path('user_keys')
++        # /tmp/home/suzie/.ssh/user_keys = dsa
++        user_keys = self.tmp_path('user_keys', dir=user_ssh_folder)
+         util.write_file(user_keys, VALID_CONTENT['dsa'])
+ 
+-        sshd_config = self.tmp_path('sshd_config')
++        # /tmp/sshd_config
++        sshd_config = self.tmp_path('sshd_config', dir="/tmp")
+         util.write_file(
+             sshd_config,
+-            "AuthorizedKeysFile %s %s" % (authorized_keys, user_keys)
++            "AuthorizedKeysFile %s %s" % (user_keys, authorized_keys)
+         )
+ 
+         (auth_key_fn, auth_key_entries) = ssh_util.extract_authorized_keys(
+-            fpw.pw_name, sshd_config
++            fpw.pw_name, sshd_config)
++        content = ssh_util.update_authorized_keys(auth_key_entries, [])
++
++        self.assertEqual(authorized_keys, auth_key_fn)
++        self.assertTrue(VALID_CONTENT['rsa'] in content)
++        self.assertTrue(VALID_CONTENT['dsa'] in content)
++
++    @patch("cloudinit.ssh_util.pwd.getpwnam")
++    def test_multiple_authorizedkeys_file_local_global(self, m_getpwnam):
++        fpw = FakePwEnt(pw_name='bobby', pw_dir='/tmp/home2/bobby')
++        m_getpwnam.return_value = fpw
++        user_ssh_folder = "%s/.ssh" % fpw.pw_dir
++
++        # /tmp/home2/bobby/.ssh/authorized_keys = rsa
++        authorized_keys = self.tmp_path('authorized_keys', dir=user_ssh_folder)
++        util.write_file(authorized_keys, VALID_CONTENT['rsa'])
++
++        # /tmp/home2/bobby/.ssh/user_keys = dsa
++        user_keys = self.tmp_path('user_keys', dir=user_ssh_folder)
++        util.write_file(user_keys, VALID_CONTENT['dsa'])
++
++        # /tmp/etc/ssh/authorized_keys = ecdsa
++        authorized_keys_global = self.tmp_path('etc/ssh/authorized_keys',
++                                               dir="/tmp")
++        util.write_file(authorized_keys_global, VALID_CONTENT['ecdsa'])
++
++        # /tmp/sshd_config
++        sshd_config = self.tmp_path('sshd_config', dir="/tmp")
++        util.write_file(
++            sshd_config,
++            "AuthorizedKeysFile %s %s %s" % (authorized_keys_global,
++                                             user_keys, authorized_keys)
++        )
++
++        (auth_key_fn, auth_key_entries) = ssh_util.extract_authorized_keys(
++            fpw.pw_name, sshd_config)
++        content = ssh_util.update_authorized_keys(auth_key_entries, [])
++
++        self.assertEqual(authorized_keys, auth_key_fn)
++        self.assertTrue(VALID_CONTENT['rsa'] in content)
++        self.assertTrue(VALID_CONTENT['ecdsa'] in content)
++        self.assertTrue(VALID_CONTENT['dsa'] in content)
++
++    @patch("cloudinit.ssh_util.pwd.getpwnam")
++    def test_multiple_authorizedkeys_file_local_global2(self, m_getpwnam):
++        fpw = FakePwEnt(pw_name='bobby', pw_dir='/tmp/home2/bobby')
++        m_getpwnam.return_value = fpw
++        user_ssh_folder = "%s/.ssh" % fpw.pw_dir
++
++        # /tmp/home2/bobby/.ssh/authorized_keys2 = rsa
++        authorized_keys = self.tmp_path('authorized_keys2',
++                                        dir=user_ssh_folder)
++        util.write_file(authorized_keys, VALID_CONTENT['rsa'])
++
++        # /tmp/home2/bobby/.ssh/user_keys3 = dsa
++        user_keys = self.tmp_path('user_keys3', dir=user_ssh_folder)
++        util.write_file(user_keys, VALID_CONTENT['dsa'])
++
++        # /tmp/etc/ssh/authorized_keys = ecdsa
++        authorized_keys_global = self.tmp_path('etc/ssh/authorized_keys',
++                                               dir="/tmp")
++        util.write_file(authorized_keys_global, VALID_CONTENT['ecdsa'])
++
++        # /tmp/sshd_config
++        sshd_config = self.tmp_path('sshd_config', dir="/tmp")
++        util.write_file(
++            sshd_config,
++            "AuthorizedKeysFile %s %s %s" % (authorized_keys_global,
++                                             authorized_keys, user_keys)
++        )
++
++        (auth_key_fn, auth_key_entries) = ssh_util.extract_authorized_keys(
++            fpw.pw_name, sshd_config)
++        content = ssh_util.update_authorized_keys(auth_key_entries, [])
++
++        self.assertEqual(user_keys, auth_key_fn)
++        self.assertTrue(VALID_CONTENT['rsa'] in content)
++        self.assertTrue(VALID_CONTENT['ecdsa'] in content)
++        self.assertTrue(VALID_CONTENT['dsa'] in content)
++
++    @patch("cloudinit.ssh_util.pwd.getpwnam")
++    def test_multiple_authorizedkeys_file_global(self, m_getpwnam):
++        fpw = FakePwEnt(pw_name='bobby', pw_dir='/tmp/home2/bobby')
++        m_getpwnam.return_value = fpw
++
++        # /tmp/etc/ssh/authorized_keys = rsa
++        authorized_keys_global = self.tmp_path('etc/ssh/authorized_keys',
++                                               dir="/tmp")
++        util.write_file(authorized_keys_global, VALID_CONTENT['rsa'])
++
++        # /tmp/sshd_config
++        sshd_config = self.tmp_path('sshd_config')
++        util.write_file(
++            sshd_config,
++            "AuthorizedKeysFile %s" % (authorized_keys_global)
+         )
++
++        (auth_key_fn, auth_key_entries) = ssh_util.extract_authorized_keys(
++            fpw.pw_name, sshd_config)
+         content = ssh_util.update_authorized_keys(auth_key_entries, [])
+ 
+         self.assertEqual("%s/.ssh/authorized_keys" % fpw.pw_dir, auth_key_fn)
+         self.assertTrue(VALID_CONTENT['rsa'] in content)
++
++    @patch("cloudinit.ssh_util.pwd.getpwnam")
++    def test_multiple_authorizedkeys_file_multiuser(self, m_getpwnam):
++        fpw = FakePwEnt(pw_name='bobby', pw_dir='/tmp/home2/bobby')
++        m_getpwnam.return_value = fpw
++        user_ssh_folder = "%s/.ssh" % fpw.pw_dir
++        # /tmp/home2/bobby/.ssh/authorized_keys2 = rsa
++        authorized_keys = self.tmp_path('authorized_keys2',
++                                        dir=user_ssh_folder)
++        util.write_file(authorized_keys, VALID_CONTENT['rsa'])
++        # /tmp/home2/bobby/.ssh/user_keys3 = dsa
++        user_keys = self.tmp_path('user_keys3', dir=user_ssh_folder)
++        util.write_file(user_keys, VALID_CONTENT['dsa'])
++
++        fpw2 = FakePwEnt(pw_name='suzie', pw_dir='/tmp/home/suzie')
++        user_ssh_folder = "%s/.ssh" % fpw2.pw_dir
++        # /tmp/home/suzie/.ssh/authorized_keys2 = ssh-xmss@openssh.com
++        authorized_keys2 = self.tmp_path('authorized_keys2',
++                                         dir=user_ssh_folder)
++        util.write_file(authorized_keys2,
++                        VALID_CONTENT['ssh-xmss@openssh.com'])
++
++        # /tmp/etc/ssh/authorized_keys = ecdsa
++        authorized_keys_global = self.tmp_path('etc/ssh/authorized_keys2',
++                                               dir="/tmp")
++        util.write_file(authorized_keys_global, VALID_CONTENT['ecdsa'])
++
++        # /tmp/sshd_config
++        sshd_config = self.tmp_path('sshd_config', dir="/tmp")
++        util.write_file(
++            sshd_config,
++            "AuthorizedKeysFile %s %%h/.ssh/authorized_keys2 %s" %
++            (authorized_keys_global, user_keys)
++        )
++
++        # process first user
++        (auth_key_fn, auth_key_entries) = ssh_util.extract_authorized_keys(
++            fpw.pw_name, sshd_config)
++        content = ssh_util.update_authorized_keys(auth_key_entries, [])
++
++        self.assertEqual(user_keys, auth_key_fn)
++        self.assertTrue(VALID_CONTENT['rsa'] in content)
++        self.assertTrue(VALID_CONTENT['ecdsa'] in content)
++        self.assertTrue(VALID_CONTENT['dsa'] in content)
++        self.assertFalse(VALID_CONTENT['ssh-xmss@openssh.com'] in content)
++
++        m_getpwnam.return_value = fpw2
++        # process second user
++        (auth_key_fn, auth_key_entries) = ssh_util.extract_authorized_keys(
++            fpw2.pw_name, sshd_config)
++        content = ssh_util.update_authorized_keys(auth_key_entries, [])
++
++        self.assertEqual(authorized_keys2, auth_key_fn)
++        self.assertTrue(VALID_CONTENT['ssh-xmss@openssh.com'] in content)
++        self.assertTrue(VALID_CONTENT['ecdsa'] in content)
++        self.assertTrue(VALID_CONTENT['dsa'] in content)
++        self.assertFalse(VALID_CONTENT['rsa'] in content)
++
++    @patch("cloudinit.ssh_util.pwd.getpwnam")
++    def test_multiple_authorizedkeys_file_multiuser2(self, m_getpwnam):
++        fpw = FakePwEnt(pw_name='bobby', pw_dir='/tmp/home/bobby')
++        m_getpwnam.return_value = fpw
++        user_ssh_folder = "%s/.ssh" % fpw.pw_dir
++        # /tmp/home/bobby/.ssh/authorized_keys2 = rsa
++        authorized_keys = self.tmp_path('authorized_keys2',
++                                        dir=user_ssh_folder)
++        util.write_file(authorized_keys, VALID_CONTENT['rsa'])
++        # /tmp/home/bobby/.ssh/user_keys3 = dsa
++        user_keys = self.tmp_path('user_keys3', dir=user_ssh_folder)
++        util.write_file(user_keys, VALID_CONTENT['dsa'])
++
++        fpw2 = FakePwEnt(pw_name='badguy', pw_dir='/tmp/home/badguy')
++        user_ssh_folder = "%s/.ssh" % fpw2.pw_dir
++        # /tmp/home/badguy/home/bobby = ""
++        authorized_keys2 = self.tmp_path('home/bobby', dir="/tmp/home/badguy")
++
++        # /tmp/etc/ssh/authorized_keys = ecdsa
++        authorized_keys_global = self.tmp_path('etc/ssh/authorized_keys2',
++                                               dir="/tmp")
++        util.write_file(authorized_keys_global, VALID_CONTENT['ecdsa'])
++
++        # /tmp/sshd_config
++        sshd_config = self.tmp_path('sshd_config', dir="/tmp")
++        util.write_file(
++            sshd_config,
++            "AuthorizedKeysFile %s %%h/.ssh/authorized_keys2 %s %s" %
++            (authorized_keys_global, user_keys, authorized_keys2)
++        )
++
++        # process first user
++        (auth_key_fn, auth_key_entries) = ssh_util.extract_authorized_keys(
++            fpw.pw_name, sshd_config)
++        content = ssh_util.update_authorized_keys(auth_key_entries, [])
++
++        self.assertEqual(user_keys, auth_key_fn)
++        self.assertTrue(VALID_CONTENT['rsa'] in content)
++        self.assertTrue(VALID_CONTENT['ecdsa'] in content)
++        self.assertTrue(VALID_CONTENT['dsa'] in content)
++
++        m_getpwnam.return_value = fpw2
++        # process second user
++        (auth_key_fn, auth_key_entries) = ssh_util.extract_authorized_keys(
++            fpw2.pw_name, sshd_config)
++        content = ssh_util.update_authorized_keys(auth_key_entries, [])
++
++        # badguy should not take the key from the other user!
++        self.assertEqual(authorized_keys2, auth_key_fn)
++        self.assertTrue(VALID_CONTENT['ecdsa'] in content)
+         self.assertTrue(VALID_CONTENT['dsa'] in content)
++        self.assertFalse(VALID_CONTENT['rsa'] in content)
+ 
+ # vi: ts=4 expandtab
+-- 
+2.27.0
+
diff --git a/SOURCES/ci-ssh_util-handle-non-default-AuthorizedKeysFile-confi.patch b/SOURCES/ci-ssh_util-handle-non-default-AuthorizedKeysFile-confi.patch
deleted file mode 100644
index 5fbcb0c..0000000
--- a/SOURCES/ci-ssh_util-handle-non-default-AuthorizedKeysFile-confi.patch
+++ /dev/null
@@ -1,98 +0,0 @@
-From b84a1e6d246bbb758f0530038612bd18eff71767 Mon Sep 17 00:00:00 2001
-From: Eduardo Otubo <otubo@redhat.com>
-Date: Tue, 8 Dec 2020 13:27:22 +0100
-Subject: [PATCH 4/4] ssh_util: handle non-default AuthorizedKeysFile config
- (#586)
-
-RH-Author: Eduardo Terrell Ferrari Otubo (eterrell)
-RH-MergeRequest: 28: ssh_util: handle non-default AuthorizedKeysFile config (#586)
-RH-Commit: [1/1] f7ce396e3002c53a3504e653b58810efb956aa26 (eterrell/cloud-init)
-RH-Bugzilla: 1862967
-
-commit b0e73814db4027dba0b7dc0282e295b7f653325c
-Author: Eduardo Otubo <otubo@redhat.com>
-Date:   Tue Oct 20 18:04:59 2020 +0200
-
-    ssh_util: handle non-default AuthorizedKeysFile config (#586)
-
-    The following commit merged all ssh keys into a default user file
-    `~/.ssh/authorized_keys` in sshd_config had multiple files configured for
-    AuthorizedKeysFile:
-
-    commit f1094b1a539044c0193165a41501480de0f8df14
-    Author: Eduardo Otubo <otubo@redhat.com>
-    Date:   Thu Dec 5 17:37:35 2019 +0100
-
-        Multiple file fix for AuthorizedKeysFile config (#60)
-
-    This commit ignored the case when sshd_config would have a single file for
-    AuthorizedKeysFile, but a non default configuration, for example
-    `~/.ssh/authorized_keys_foobar`. In this case cloud-init would grab all keys
-    from this file and write a new one, the default `~/.ssh/authorized_keys`
-    causing the bug.
-
-    rhbz: #1862967
-
-    Signed-off-by: Eduardo Otubo <otubo@redhat.com>
-
-Signed-off-by: Eduardo Otubo <otubo@redhat.com>
----
- cloudinit/ssh_util.py           | 6 +++---
- tests/unittests/test_sshutil.py | 6 +++---
- 2 files changed, 6 insertions(+), 6 deletions(-)
-
-diff --git a/cloudinit/ssh_util.py b/cloudinit/ssh_util.py
-index c08042d6..d5113996 100644
---- a/cloudinit/ssh_util.py
-+++ b/cloudinit/ssh_util.py
-@@ -262,13 +262,13 @@ def extract_authorized_keys(username, sshd_cfg_file=DEF_SSHD_CFG):
- 
-         except (IOError, OSError):
-             # Give up and use a default key filename
--            auth_key_fns[0] = default_authorizedkeys_file
-+            auth_key_fns.append(default_authorizedkeys_file)
-             util.logexc(LOG, "Failed extracting 'AuthorizedKeysFile' in SSH "
-                         "config from %r, using 'AuthorizedKeysFile' file "
-                         "%r instead", DEF_SSHD_CFG, auth_key_fns[0])
- 
--    # always store all the keys in the user's private file
--    return (default_authorizedkeys_file, parse_authorized_keys(auth_key_fns))
-+    # always store all the keys in the first file configured on sshd_config
-+    return (auth_key_fns[0], parse_authorized_keys(auth_key_fns))
- 
- 
- def setup_user_keys(keys, username, options=None):
-diff --git a/tests/unittests/test_sshutil.py b/tests/unittests/test_sshutil.py
-index fd1d1bac..88a111e3 100644
---- a/tests/unittests/test_sshutil.py
-+++ b/tests/unittests/test_sshutil.py
-@@ -593,7 +593,7 @@ class TestMultipleSshAuthorizedKeysFile(test_helpers.CiTestCase):
-             fpw.pw_name, sshd_config)
-         content = ssh_util.update_authorized_keys(auth_key_entries, [])
- 
--        self.assertEqual("%s/.ssh/authorized_keys" % fpw.pw_dir, auth_key_fn)
-+        self.assertEqual(authorized_keys, auth_key_fn)
-         self.assertTrue(VALID_CONTENT['rsa'] in content)
-         self.assertTrue(VALID_CONTENT['dsa'] in content)
- 
-@@ -610,7 +610,7 @@ class TestMultipleSshAuthorizedKeysFile(test_helpers.CiTestCase):
-         sshd_config = self.tmp_path('sshd_config')
-         util.write_file(
-             sshd_config,
--            "AuthorizedKeysFile %s %s" % (authorized_keys, user_keys)
-+            "AuthorizedKeysFile %s %s" % (user_keys, authorized_keys)
-         )
- 
-         (auth_key_fn, auth_key_entries) = ssh_util.extract_authorized_keys(
-@@ -618,7 +618,7 @@ class TestMultipleSshAuthorizedKeysFile(test_helpers.CiTestCase):
-         )
-         content = ssh_util.update_authorized_keys(auth_key_entries, [])
- 
--        self.assertEqual("%s/.ssh/authorized_keys" % fpw.pw_dir, auth_key_fn)
-+        self.assertEqual(user_keys, auth_key_fn)
-         self.assertTrue(VALID_CONTENT['rsa'] in content)
-         self.assertTrue(VALID_CONTENT['dsa'] in content)
- 
--- 
-2.18.4
-
diff --git a/SOURCES/ci-write-passwords-only-to-serial-console-lock-down-clo.patch b/SOURCES/ci-write-passwords-only-to-serial-console-lock-down-clo.patch
new file mode 100644
index 0000000..5cf4671
--- /dev/null
+++ b/SOURCES/ci-write-passwords-only-to-serial-console-lock-down-clo.patch
@@ -0,0 +1,369 @@
+From 769b9f8c9b1ecc294a197575108ae7cb54ad7f4b Mon Sep 17 00:00:00 2001
+From: Eduardo Otubo <otubo@redhat.com>
+Date: Mon, 5 Jul 2021 14:13:45 +0200
+Subject: [PATCH] write passwords only to serial console, lock down
+ cloud-init-output.log (#847)
+
+RH-Author: Eduardo Otubo <otubo@redhat.com>
+RH-MergeRequest: 21: write passwords only to serial console, lock down cloud-init-output.log (#847)
+RH-Commit: [1/1] 8f30f2b7d0d6f9dca19994dbd0827b44e998f238 (otubo/cloud-init)
+RH-Bugzilla: 1945891
+RH-Acked-by: Emanuele Giuseppe Esposito <eesposit@redhat.com>
+RH-Acked-by: Mohamed Gamal Morsy <mmorsy@redhat.com>
+
+commit b794d426b9ab43ea9d6371477466070d86e10668
+Author: Daniel Watkins <oddbloke@ubuntu.com>
+Date:   Fri Mar 19 10:06:42 2021 -0400
+
+    write passwords only to serial console, lock down cloud-init-output.log (#847)
+
+    Prior to this commit, when a user specified configuration which would
+    generate random passwords for users, cloud-init would cause those
+    passwords to be written to the serial console by emitting them on
+    stderr.  In the default configuration, any stdout or stderr emitted by
+    cloud-init is also written to `/var/log/cloud-init-output.log`.  This
+    file is world-readable, meaning that those randomly-generated passwords
+    were available to be read by any user with access to the system.  This
+    presents an obvious security issue.
+
+    This commit responds to this issue in two ways:
+
+    * We address the direct issue by moving from writing the passwords to
+      sys.stderr to writing them directly to /dev/console (via
+      util.multi_log); this means that the passwords will never end up in
+      cloud-init-output.log
+    * To avoid future issues like this, we also modify the logging code so
+      that any files created in a log sink subprocess will only be
+      owner/group readable and, if it exists, will be owned by the adm
+      group.  This results in `/var/log/cloud-init-output.log` no longer
+      being world-readable, meaning that if there are other parts of the
+      codebase that are emitting sensitive data intended for the serial
+      console, that data is no longer available to all users of the system.
+
+    LP: #1918303
+
+Signed-off-by: Eduardo Otubo <otubo@redhat.com>
+---
+ cloudinit/config/cc_set_passwords.py          |  5 +-
+ cloudinit/config/tests/test_set_passwords.py  | 40 +++++++++----
+ cloudinit/tests/test_util.py                  | 56 +++++++++++++++++++
+ cloudinit/util.py                             | 38 +++++++++++--
+ .../modules/test_set_password.py              | 24 ++++++++
+ tests/integration_tests/test_logging.py       | 22 ++++++++
+ tests/unittests/test_util.py                  |  4 ++
+ 7 files changed, 173 insertions(+), 16 deletions(-)
+ create mode 100644 tests/integration_tests/test_logging.py
+
+diff --git a/cloudinit/config/cc_set_passwords.py b/cloudinit/config/cc_set_passwords.py
+index d6b5682d..433de751 100755
+--- a/cloudinit/config/cc_set_passwords.py
++++ b/cloudinit/config/cc_set_passwords.py
+@@ -78,7 +78,6 @@ password.
+ """
+ 
+ import re
+-import sys
+ 
+ from cloudinit.distros import ug_util
+ from cloudinit import log as logging
+@@ -214,7 +213,9 @@ def handle(_name, cfg, cloud, log, args):
+         if len(randlist):
+             blurb = ("Set the following 'random' passwords\n",
+                      '\n'.join(randlist))
+-            sys.stderr.write("%s\n%s\n" % blurb)
++            util.multi_log(
++                "%s\n%s\n" % blurb, stderr=False, fallback_to_stdout=False
++            )
+ 
+         if expire:
+             expired_users = []
+diff --git a/cloudinit/config/tests/test_set_passwords.py b/cloudinit/config/tests/test_set_passwords.py
+index daa1ef51..bbe2ee8f 100644
+--- a/cloudinit/config/tests/test_set_passwords.py
++++ b/cloudinit/config/tests/test_set_passwords.py
+@@ -74,10 +74,6 @@ class TestSetPasswordsHandle(CiTestCase):
+ 
+     with_logs = True
+ 
+-    def setUp(self):
+-        super(TestSetPasswordsHandle, self).setUp()
+-        self.add_patch('cloudinit.config.cc_set_passwords.sys.stderr', 'm_err')
+-
+     def test_handle_on_empty_config(self, *args):
+         """handle logs that no password has changed when config is empty."""
+         cloud = self.tmp_cloud(distro='ubuntu')
+@@ -129,10 +125,12 @@ class TestSetPasswordsHandle(CiTestCase):
+             mock.call(['pw', 'usermod', 'ubuntu', '-p', '01-Jan-1970'])],
+             m_subp.call_args_list)
+ 
++    @mock.patch(MODPATH + "util.multi_log")
+     @mock.patch(MODPATH + "util.is_BSD")
+     @mock.patch(MODPATH + "subp.subp")
+-    def test_handle_on_chpasswd_list_creates_random_passwords(self, m_subp,
+-                                                              m_is_bsd):
++    def test_handle_on_chpasswd_list_creates_random_passwords(
++        self, m_subp, m_is_bsd, m_multi_log
++    ):
+         """handle parses command set random passwords."""
+         m_is_bsd.return_value = False
+         cloud = self.tmp_cloud(distro='ubuntu')
+@@ -146,10 +144,32 @@ class TestSetPasswordsHandle(CiTestCase):
+         self.assertIn(
+             'DEBUG: Handling input for chpasswd as list.',
+             self.logs.getvalue())
+-        self.assertNotEqual(
+-            [mock.call(['chpasswd'],
+-             '\n'.join(valid_random_pwds) + '\n')],
+-            m_subp.call_args_list)
++
++        self.assertEqual(1, m_subp.call_count)
++        args, _kwargs = m_subp.call_args
++        self.assertEqual(["chpasswd"], args[0])
++
++        stdin = args[1]
++        user_pass = {
++            user: password
++            for user, password
++            in (line.split(":") for line in stdin.splitlines())
++        }
++
++        self.assertEqual(1, m_multi_log.call_count)
++        self.assertEqual(
++            mock.call(mock.ANY, stderr=False, fallback_to_stdout=False),
++            m_multi_log.call_args
++        )
++
++        self.assertEqual(set(["root", "ubuntu"]), set(user_pass.keys()))
++        written_lines = m_multi_log.call_args[0][0].splitlines()
++        for password in user_pass.values():
++            for line in written_lines:
++                if password in line:
++                    break
++            else:
++                self.fail("Password not emitted to console")
+ 
+ 
+ # vi: ts=4 expandtab
+diff --git a/cloudinit/tests/test_util.py b/cloudinit/tests/test_util.py
+index b7a302f1..e811917e 100644
+--- a/cloudinit/tests/test_util.py
++++ b/cloudinit/tests/test_util.py
+@@ -851,4 +851,60 @@ class TestEnsureFile:
+         assert "ab" == kwargs["omode"]
+ 
+ 
++@mock.patch("cloudinit.util.grp.getgrnam")
++@mock.patch("cloudinit.util.os.setgid")
++@mock.patch("cloudinit.util.os.umask")
++class TestRedirectOutputPreexecFn:
++    """This tests specifically the preexec_fn used in redirect_output."""
++
++    @pytest.fixture(params=["outfmt", "errfmt"])
++    def preexec_fn(self, request):
++        """A fixture to gather the preexec_fn used by redirect_output.
++
++        This enables simpler direct testing of it, and parameterises any tests
++        using it to cover both the stdout and stderr code paths.
++        """
++        test_string = "| piped output to invoke subprocess"
++        if request.param == "outfmt":
++            args = (test_string, None)
++        elif request.param == "errfmt":
++            args = (None, test_string)
++        with mock.patch("cloudinit.util.subprocess.Popen") as m_popen:
++            util.redirect_output(*args)
++
++        assert 1 == m_popen.call_count
++        _args, kwargs = m_popen.call_args
++        assert "preexec_fn" in kwargs, "preexec_fn not passed to Popen"
++        return kwargs["preexec_fn"]
++
++    def test_preexec_fn_sets_umask(
++        self, m_os_umask, _m_setgid, _m_getgrnam, preexec_fn
++    ):
++        """preexec_fn should set a mask that avoids world-readable files."""
++        preexec_fn()
++
++        assert [mock.call(0o037)] == m_os_umask.call_args_list
++
++    def test_preexec_fn_sets_group_id_if_adm_group_present(
++        self, _m_os_umask, m_setgid, m_getgrnam, preexec_fn
++    ):
++        """We should setgrp to adm if present, so files are owned by them."""
++        fake_group = mock.Mock(gr_gid=mock.sentinel.gr_gid)
++        m_getgrnam.return_value = fake_group
++
++        preexec_fn()
++
++        assert [mock.call("adm")] == m_getgrnam.call_args_list
++        assert [mock.call(mock.sentinel.gr_gid)] == m_setgid.call_args_list
++
++    def test_preexec_fn_handles_absent_adm_group_gracefully(
++        self, _m_os_umask, m_setgid, m_getgrnam, preexec_fn
++    ):
++        """We should handle an absent adm group gracefully."""
++        m_getgrnam.side_effect = KeyError("getgrnam(): name not found: 'adm'")
++
++        preexec_fn()
++
++        assert 0 == m_setgid.call_count
++
+ # vi: ts=4 expandtab
+diff --git a/cloudinit/util.py b/cloudinit/util.py
+index 769f3425..4e0a72db 100644
+--- a/cloudinit/util.py
++++ b/cloudinit/util.py
+@@ -359,7 +359,7 @@ def find_modules(root_dir):
+ 
+ 
+ def multi_log(text, console=True, stderr=True,
+-              log=None, log_level=logging.DEBUG):
++              log=None, log_level=logging.DEBUG, fallback_to_stdout=True):
+     if stderr:
+         sys.stderr.write(text)
+     if console:
+@@ -368,7 +368,7 @@ def multi_log(text, console=True, stderr=True,
+             with open(conpath, 'w') as wfh:
+                 wfh.write(text)
+                 wfh.flush()
+-        else:
++        elif fallback_to_stdout:
+             # A container may lack /dev/console (arguably a container bug).  If
+             # it does not exist, then write output to stdout.  this will result
+             # in duplicate stderr and stdout messages if stderr was True.
+@@ -623,6 +623,26 @@ def redirect_output(outfmt, errfmt, o_out=None, o_err=None):
+     if not o_err:
+         o_err = sys.stderr
+ 
++    # pylint: disable=subprocess-popen-preexec-fn
++    def set_subprocess_umask_and_gid():
++        """Reconfigure umask and group ID to create output files securely.
++
++        This is passed to subprocess.Popen as preexec_fn, so it is executed in
++        the context of the newly-created process.  It:
++
++        * sets the umask of the process so created files aren't world-readable
++        * if an adm group exists in the system, sets that as the process' GID
++          (so that the created file(s) are owned by root:adm)
++        """
++        os.umask(0o037)
++        try:
++            group_id = grp.getgrnam("adm").gr_gid
++        except KeyError:
++            # No adm group, don't set a group
++            pass
++        else:
++            os.setgid(group_id)
++
+     if outfmt:
+         LOG.debug("Redirecting %s to %s", o_out, outfmt)
+         (mode, arg) = outfmt.split(" ", 1)
+@@ -632,7 +652,12 @@ def redirect_output(outfmt, errfmt, o_out=None, o_err=None):
+                 owith = "wb"
+             new_fp = open(arg, owith)
+         elif mode == "|":
+-            proc = subprocess.Popen(arg, shell=True, stdin=subprocess.PIPE)
++            proc = subprocess.Popen(
++                arg,
++                shell=True,
++                stdin=subprocess.PIPE,
++                preexec_fn=set_subprocess_umask_and_gid,
++            )
+             new_fp = proc.stdin
+         else:
+             raise TypeError("Invalid type for output format: %s" % outfmt)
+@@ -654,7 +679,12 @@ def redirect_output(outfmt, errfmt, o_out=None, o_err=None):
+                 owith = "wb"
+             new_fp = open(arg, owith)
+         elif mode == "|":
+-            proc = subprocess.Popen(arg, shell=True, stdin=subprocess.PIPE)
++            proc = subprocess.Popen(
++                arg,
++                shell=True,
++                stdin=subprocess.PIPE,
++                preexec_fn=set_subprocess_umask_and_gid,
++            )
+             new_fp = proc.stdin
+         else:
+             raise TypeError("Invalid type for error format: %s" % errfmt)
+diff --git a/tests/integration_tests/modules/test_set_password.py b/tests/integration_tests/modules/test_set_password.py
+index b13f76fb..d7cf91a5 100644
+--- a/tests/integration_tests/modules/test_set_password.py
++++ b/tests/integration_tests/modules/test_set_password.py
+@@ -116,6 +116,30 @@ class Mixin:
+         # Which are not the same
+         assert shadow_users["harry"] != shadow_users["dick"]
+ 
++    def test_random_passwords_not_stored_in_cloud_init_output_log(
++        self, class_client
++    ):
++        """We should not emit passwords to the in-instance log file.
++
++        LP: #1918303
++        """
++        cloud_init_output = class_client.read_from_file(
++            "/var/log/cloud-init-output.log"
++        )
++        assert "dick:" not in cloud_init_output
++        assert "harry:" not in cloud_init_output
++
++    def test_random_passwords_emitted_to_serial_console(self, class_client):
++        """We should emit passwords to the serial console. (LP: #1918303)"""
++        try:
++            console_log = class_client.instance.console_log()
++        except NotImplementedError:
++            # Assume that an exception here means that we can't use the console
++            # log
++            pytest.skip("NotImplementedError when requesting console log")
++        assert "dick:" in console_log
++        assert "harry:" in console_log
++
+     def test_explicit_password_set_correctly(self, class_client):
+         """Test that an explicitly-specified password is set correctly."""
+         shadow_users, _ = self._fetch_and_parse_etc_shadow(class_client)
+diff --git a/tests/integration_tests/test_logging.py b/tests/integration_tests/test_logging.py
+new file mode 100644
+index 00000000..b31a0434
+--- /dev/null
++++ b/tests/integration_tests/test_logging.py
+@@ -0,0 +1,22 @@
++"""Integration tests relating to cloud-init's logging."""
++
++
++class TestVarLogCloudInitOutput:
++    """Integration tests relating to /var/log/cloud-init-output.log."""
++
++    def test_var_log_cloud_init_output_not_world_readable(self, client):
++        """
++        The log can contain sensitive data, it shouldn't be world-readable.
++
++        LP: #1918303
++        """
++        # Check the file exists
++        assert client.execute("test -f /var/log/cloud-init-output.log").ok
++
++        # Check its permissions are as we expect
++        perms, user, group = client.execute(
++            "stat -c %a:%U:%G /var/log/cloud-init-output.log"
++        ).split(":")
++        assert "640" == perms
++        assert "root" == user
++        assert "adm" == group
+diff --git a/tests/unittests/test_util.py b/tests/unittests/test_util.py
+index 857629f1..e5292001 100644
+--- a/tests/unittests/test_util.py
++++ b/tests/unittests/test_util.py
+@@ -572,6 +572,10 @@ class TestMultiLog(helpers.FilesystemMockingTestCase):
+         util.multi_log(logged_string)
+         self.assertEqual(logged_string, self.stdout.getvalue())
+ 
++    def test_logs_dont_go_to_stdout_if_fallback_to_stdout_is_false(self):
++        util.multi_log('something', fallback_to_stdout=False)
++        self.assertEqual('', self.stdout.getvalue())
++
+     def test_logs_go_to_log_if_given(self):
+         log = mock.MagicMock()
+         logged_string = 'something very important'
+-- 
+2.27.0
+
diff --git a/SPECS/cloud-init.spec b/SPECS/cloud-init.spec
index cedad04..6671452 100644
--- a/SPECS/cloud-init.spec
+++ b/SPECS/cloud-init.spec
@@ -5,8 +5,8 @@
 %global debug_package %{nil}
 
 Name:           cloud-init
-Version:        20.3
-Release:        10%{?dist}
+Version:        21.1
+Release:        7%{?dist}
 Summary:        Cloud instance init scripts
 
 Group:          System Environment/Base
@@ -22,24 +22,18 @@ Patch0004: 0004-sysconfig-Don-t-write-BOOTPROTO-dhcp-for-ipv6-dhcp.patch
 Patch0005: 0005-DataSourceAzure.py-use-hostnamectl-to-set-hostname.patch
 Patch0006: 0006-include-NOZEROCONF-yes-in-etc-sysconfig-network.patch
 Patch0007: 0007-Remove-race-condition-between-cloud-init-and-Network.patch
-Patch8: ci-Explicit-set-IPV6_AUTOCONF-and-IPV6_FORCE_ACCEPT_RA-.patch
-Patch9: ci-Add-config-modules-for-controlling-IBM-PowerVM-RMC.-.patch
-# For bz#1881462 - [rhel8][cloud-init] ifup bond0.504 Error: Connection activation failed: No suitable device found for this connection
-Patch10: ci-network-Fix-type-and-respect-name-when-rendering-vla.patch
-# For bz#1859695 - [Cloud-init] DHCPv6 assigned address is not added to VM's interface
-Patch11: ci-Adding-BOOTPROTO-dhcp-to-render-sysconfig-dhcp6-stat.patch
-# For bz#1898943 - [rhel-8]cloud-final.service fails if NetworkManager not installed.
-Patch12: ci-Fix-unit-failure-of-cloud-final.service-if-NetworkMa.patch
+Patch0008: 0008-net-exclude-OVS-internal-interfaces-in-get_interface.patch
+Patch0009: 0009-Fix-requiring-device-number-on-EC2-derivatives-836.patch
+# For bz#1957532 - [cloud-init] From RHEL 82+ cloud-init no longer displays sshd keys fingerprints from instance launched from a backup image
+Patch10: ci-rhel-cloud.cfg-remove-ssh_genkeytypes-in-settings.py.patch
+# For bz#1945891 - CVE-2021-3429 cloud-init: randomly generated passwords logged in clear-text to world-readable file [rhel-8]
+Patch11: ci-write-passwords-only-to-serial-console-lock-down-clo.patch
 # For bz#1862967 - [cloud-init]Customize ssh AuthorizedKeysFile causes login failure
-Patch13: ci-ssh_util-handle-non-default-AuthorizedKeysFile-confi.patch
-# For bz#1859695 - [Cloud-init] DHCPv6 assigned address is not added to VM's interface
-Patch14: ci-Missing-IPV6_AUTOCONF-no-to-render-sysconfig-dhcp6-s.patch
-# For bz#1900892 - [Azure] Update existing user password RHEL8x
-Patch15: ci-DataSourceAzure-update-password-for-defuser-if-exist.patch
-# For bz#1919972 - [RHEL-8.4] ssh keys can be shared across users giving potential root access
-Patch16: ci-Revert-ssh_util-handle-non-default-AuthorizedKeysFil.patch
-# For bz#1913127 - A typo in cloud-init man page
-Patch17: ci-fix-a-typo-in-man-page-cloud-init.1-752.patch
+Patch12: ci-ssh-util-allow-cloudinit-to-merge-all-ssh-keys-into-.patch
+# For bz#1862967 - [cloud-init]Customize ssh AuthorizedKeysFile causes login failure
+Patch13: ci-Stop-copying-ssh-system-keys-and-check-folder-permis.patch
+# For bz#1995840 - [cloudinit]  Fix home permissions modified by ssh module
+Patch14: ci-Fix-home-permissions-modified-by-ssh-module-SC-338-9.patch
 
 BuildArch:      noarch
 
@@ -87,6 +81,7 @@ Requires:       python3-six
 Requires:       shadow-utils
 Requires:       util-linux
 Requires:       xfsprogs
+Requires:       dhcp-client
 
 %{?systemd_requires}
 
@@ -193,8 +188,7 @@ if [ $1 -eq 0 ] ; then
 fi
 
 %postun
-%systemd_postun
-
+%systemd_postun cloud-config.service cloud-config.target cloud-final.service cloud-init.service cloud-init.target cloud-init-local.service
 
 %files
 %license LICENSE
@@ -231,6 +225,44 @@ fi
 %config(noreplace) %{_sysconfdir}/rsyslog.d/21-cloudinit.conf
 
 %changelog
+* Fri Aug 27 2021 Miroslav Rezanina <mrezanin@redhat.com> - 21.1-7
+- ci-Fix-home-permissions-modified-by-ssh-module-SC-338-9.patch [bz#1995840]
+- Resolves: bz#1995840
+  ([cloudinit]  Fix home permissions modified by ssh module)
+
+* Wed Aug 11 2021 Miroslav Rezanina <mrezanin@redhat.com> - 21.1-6
+- ci-Stop-copying-ssh-system-keys-and-check-folder-permis.patch [bz#1862967]
+- Resolves: bz#1862967
+  ([cloud-init]Customize ssh AuthorizedKeysFile causes login failure)
+
+* Fri Aug 06 2021 Miroslav Rezanina <mrezanin@redhat.com> - 21.1-5
+- ci-Add-dhcp-client-as-a-dependency.patch [bz#1977385]
+- Resolves: bz#1977385
+  ([Azure][RHEL-8] cloud-init must require dhcp-client on Azure)
+
+* Mon Jul 19 2021 Miroslav Rezanina <mrezanin@redhat.com> - 21.1-4
+- ci-ssh-util-allow-cloudinit-to-merge-all-ssh-keys-into-.patch [bz#1862967]
+- Resolves: bz#1862967
+  ([cloud-init]Customize ssh AuthorizedKeysFile causes login failure)
+
+* Mon Jul 12 2021 Miroslav Rezanina <mrezanin@redhat.com> - 21.1-3
+- ci-write-passwords-only-to-serial-console-lock-down-clo.patch [bz#1945891]
+- Resolves: bz#1945891
+  (CVE-2021-3429 cloud-init: randomly generated passwords logged in clear-text to world-readable file [rhel-8])
+
+* Fri Jun 11 2021 Miroslav Rezanina <mrezanin@redhat.com> - 21.1-2
+- ci-rhel-cloud.cfg-remove-ssh_genkeytypes-in-settings.py.patch [bz#1957532]
+- ci-cloud-init.spec.template-update-systemd_postun-param.patch [bz#1952089]
+- Resolves: bz#1957532
+  ([cloud-init] From RHEL 82+ cloud-init no longer displays sshd keys fingerprints from instance launched from a backup image)
+- Resolves: bz#1952089
+  (cloud-init brew build fails on Fedora 33)
+
+* Thu May 27 2021 Miroslav Rezanina <mrezanin@redhat.com> - 21.1-1.el8
+- Rebaes to 21.1 [bz#1958174]
+- Resolves: bz#1958174
+  ([RHEL-8.5.0] Rebase cloud-init to 21.1)
+
 * Tue Feb 02 2021 Miroslav Rezanina <mrezanin@redhat.com> - 20.3-10.el8
 - ci-fix-a-typo-in-man-page-cloud-init.1-752.patch [bz#1913127]
 - Resolves: bz#1913127