c36ff1
From 5bfe2ee2b063d87e6fd255d6c5e63123aa3f6de0 Mon Sep 17 00:00:00 2001
c36ff1
From: Emanuele Giuseppe Esposito <eesposit@redhat.com>
c36ff1
Date: Sat, 21 Aug 2021 13:55:53 +0200
c36ff1
Subject: [PATCH] Fix home permissions modified by ssh module (SC-338) (#984)
c36ff1
c36ff1
RH-Author: Emanuele Giuseppe Esposito <eesposit@redhat.com>
c36ff1
RH-MergeRequest: 9: Fix home permissions modified by ssh module
c36ff1
RH-Commit: [1/1] ab55db88aa1bf2f77acaca5e76ffabbab72b1fb2 (eesposit/cloud-init-centos-)
c36ff1
RH-Bugzilla: 1995843
c36ff1
RH-Acked-by: Mohamed Gamal Morsy <mmorsy@redhat.com>
c36ff1
RH-Acked-by: Eduardo Otubo <otubo@redhat.com>
c36ff1
c36ff1
TESTED: By me and QA
c36ff1
BREW: 39178085
c36ff1
c36ff1
Fix home permissions modified by ssh module (SC-338) (#984)
c36ff1
c36ff1
commit 7d3f5d750f6111c2716143364ea33486df67c927
c36ff1
Author: James Falcon <therealfalcon@gmail.com>
c36ff1
Date:   Fri Aug 20 17:09:49 2021 -0500
c36ff1
c36ff1
    Fix home permissions modified by ssh module (SC-338) (#984)
c36ff1
c36ff1
    Fix home permissions modified by ssh module
c36ff1
c36ff1
    In #956, we updated the file and directory permissions for keys not in
c36ff1
    the user's home directory. We also unintentionally modified the
c36ff1
    permissions within the home directory as well. These should not change,
c36ff1
    and this commit changes that back.
c36ff1
c36ff1
    LP: #1940233
c36ff1
c36ff1
Signed-off-by: Emanuele Giuseppe Esposito <eesposit@redhat.com>
c36ff1
---
c36ff1
 cloudinit/ssh_util.py                         |  35 ++++-
c36ff1
 .../modules/test_ssh_keysfile.py              | 132 +++++++++++++++---
c36ff1
 2 files changed, 146 insertions(+), 21 deletions(-)
c36ff1
c36ff1
diff --git a/cloudinit/ssh_util.py b/cloudinit/ssh_util.py
c36ff1
index b8a3c8f7..9ccadf09 100644
c36ff1
--- a/cloudinit/ssh_util.py
c36ff1
+++ b/cloudinit/ssh_util.py
c36ff1
@@ -321,23 +321,48 @@ def check_create_path(username, filename, strictmodes):
c36ff1
         home_folder = os.path.dirname(user_pwent.pw_dir)
c36ff1
         for directory in directories:
c36ff1
             parent_folder += "/" + directory
c36ff1
-            if home_folder.startswith(parent_folder):
c36ff1
+
c36ff1
+            # security check, disallow symlinks in the AuthorizedKeysFile path.
c36ff1
+            if os.path.islink(parent_folder):
c36ff1
+                LOG.debug(
c36ff1
+                    "Invalid directory. Symlink exists in path: %s",
c36ff1
+                    parent_folder)
c36ff1
+                return False
c36ff1
+
c36ff1
+            if os.path.isfile(parent_folder):
c36ff1
+                LOG.debug(
c36ff1
+                    "Invalid directory. File exists in path: %s",
c36ff1
+                    parent_folder)
c36ff1
+                return False
c36ff1
+
c36ff1
+            if (home_folder.startswith(parent_folder) or
c36ff1
+                    parent_folder == user_pwent.pw_dir):
c36ff1
                 continue
c36ff1
 
c36ff1
-            if not os.path.isdir(parent_folder):
c36ff1
+            if not os.path.exists(parent_folder):
c36ff1
                 # directory does not exist, and permission so far are good:
c36ff1
                 # create the directory, and make it accessible by everyone
c36ff1
                 # but owned by root, as it might be used by many users.
c36ff1
                 with util.SeLinuxGuard(parent_folder):
c36ff1
-                    os.makedirs(parent_folder, mode=0o755, exist_ok=True)
c36ff1
-                    util.chownbyid(parent_folder, root_pwent.pw_uid,
c36ff1
-                                   root_pwent.pw_gid)
c36ff1
+                    mode = 0o755
c36ff1
+                    uid = root_pwent.pw_uid
c36ff1
+                    gid = root_pwent.pw_gid
c36ff1
+                    if parent_folder.startswith(user_pwent.pw_dir):
c36ff1
+                        mode = 0o700
c36ff1
+                        uid = user_pwent.pw_uid
c36ff1
+                        gid = user_pwent.pw_gid
c36ff1
+                    os.makedirs(parent_folder, mode=mode, exist_ok=True)
c36ff1
+                    util.chownbyid(parent_folder, uid, gid)
c36ff1
 
c36ff1
             permissions = check_permissions(username, parent_folder,
c36ff1
                                             filename, False, strictmodes)
c36ff1
             if not permissions:
c36ff1
                 return False
c36ff1
 
c36ff1
+        if os.path.islink(filename) or os.path.isdir(filename):
c36ff1
+            LOG.debug("%s is not a file!", filename)
c36ff1
+            return False
c36ff1
+
c36ff1
         # check the file
c36ff1
         if not os.path.exists(filename):
c36ff1
             # if file does not exist: we need to create it, since the
c36ff1
diff --git a/tests/integration_tests/modules/test_ssh_keysfile.py b/tests/integration_tests/modules/test_ssh_keysfile.py
c36ff1
index f82d7649..3159feb9 100644
c36ff1
--- a/tests/integration_tests/modules/test_ssh_keysfile.py
c36ff1
+++ b/tests/integration_tests/modules/test_ssh_keysfile.py
c36ff1
@@ -10,10 +10,10 @@ TEST_USER1_KEYS = get_test_rsa_keypair('test1')
c36ff1
 TEST_USER2_KEYS = get_test_rsa_keypair('test2')
c36ff1
 TEST_DEFAULT_KEYS = get_test_rsa_keypair('test3')
c36ff1
 
c36ff1
-USERDATA = """\
c36ff1
+_USERDATA = """\
c36ff1
 #cloud-config
c36ff1
 bootcmd:
c36ff1
- - sed -i 's;#AuthorizedKeysFile.*;AuthorizedKeysFile /etc/ssh/authorized_keys %h/.ssh/authorized_keys2;' /etc/ssh/sshd_config
c36ff1
+ - {bootcmd}
c36ff1
 ssh_authorized_keys:
c36ff1
  - {default}
c36ff1
 users:
c36ff1
@@ -24,27 +24,17 @@ users:
c36ff1
 - name: test_user2
c36ff1
   ssh_authorized_keys:
c36ff1
     - {user2}
c36ff1
-""".format(  # noqa: E501
c36ff1
+""".format(
c36ff1
+    bootcmd='{bootcmd}',
c36ff1
     default=TEST_DEFAULT_KEYS.public_key,
c36ff1
     user1=TEST_USER1_KEYS.public_key,
c36ff1
     user2=TEST_USER2_KEYS.public_key,
c36ff1
 )
c36ff1
 
c36ff1
 
c36ff1
-@pytest.mark.ubuntu
c36ff1
-@pytest.mark.user_data(USERDATA)
c36ff1
-def test_authorized_keys(client: IntegrationInstance):
c36ff1
-    expected_keys = [
c36ff1
-        ('test_user1', '/home/test_user1/.ssh/authorized_keys2',
c36ff1
-         TEST_USER1_KEYS),
c36ff1
-        ('test_user2', '/home/test_user2/.ssh/authorized_keys2',
c36ff1
-         TEST_USER2_KEYS),
c36ff1
-        ('ubuntu', '/home/ubuntu/.ssh/authorized_keys2',
c36ff1
-         TEST_DEFAULT_KEYS),
c36ff1
-        ('root', '/root/.ssh/authorized_keys2', TEST_DEFAULT_KEYS),
c36ff1
-    ]
c36ff1
-
c36ff1
+def common_verify(client, expected_keys):
c36ff1
     for user, filename, keys in expected_keys:
c36ff1
+        # Ensure key is in the key file
c36ff1
         contents = client.read_from_file(filename)
c36ff1
         if user in ['ubuntu', 'root']:
c36ff1
             # Our personal public key gets added by pycloudlib
c36ff1
@@ -83,3 +73,113 @@ def test_authorized_keys(client: IntegrationInstance):
c36ff1
                     look_for_keys=False,
c36ff1
                     allow_agent=False,
c36ff1
                 )
c36ff1
+
c36ff1
+        # Ensure we haven't messed with any /home permissions
c36ff1
+        # See LP: #1940233
c36ff1
+        home_dir = '/home/{}'.format(user)
c36ff1
+        home_perms = '755'
c36ff1
+        if user == 'root':
c36ff1
+            home_dir = '/root'
c36ff1
+            home_perms = '700'
c36ff1
+        assert '{} {}'.format(user, home_perms) == client.execute(
c36ff1
+            'stat -c "%U %a" {}'.format(home_dir)
c36ff1
+        )
c36ff1
+        if client.execute("test -d {}/.ssh".format(home_dir)).ok:
c36ff1
+            assert '{} 700'.format(user) == client.execute(
c36ff1
+                'stat -c "%U %a" {}/.ssh'.format(home_dir)
c36ff1
+            )
c36ff1
+        assert '{} 600'.format(user) == client.execute(
c36ff1
+            'stat -c "%U %a" {}'.format(filename)
c36ff1
+        )
c36ff1
+
c36ff1
+        # Also ensure ssh-keygen works as expected
c36ff1
+        client.execute('mkdir {}/.ssh'.format(home_dir))
c36ff1
+        assert client.execute(
c36ff1
+            "ssh-keygen -b 2048 -t rsa -f {}/.ssh/id_rsa -q -N ''".format(
c36ff1
+                home_dir)
c36ff1
+        ).ok
c36ff1
+        assert client.execute('test -f {}/.ssh/id_rsa'.format(home_dir))
c36ff1
+        assert client.execute('test -f {}/.ssh/id_rsa.pub'.format(home_dir))
c36ff1
+
c36ff1
+    assert 'root 755' == client.execute('stat -c "%U %a" /home')
c36ff1
+
c36ff1
+
c36ff1
+DEFAULT_KEYS_USERDATA = _USERDATA.format(bootcmd='""')
c36ff1
+
c36ff1
+
c36ff1
+@pytest.mark.ubuntu
c36ff1
+@pytest.mark.user_data(DEFAULT_KEYS_USERDATA)
c36ff1
+def test_authorized_keys_default(client: IntegrationInstance):
c36ff1
+    expected_keys = [
c36ff1
+        ('test_user1', '/home/test_user1/.ssh/authorized_keys',
c36ff1
+         TEST_USER1_KEYS),
c36ff1
+        ('test_user2', '/home/test_user2/.ssh/authorized_keys',
c36ff1
+         TEST_USER2_KEYS),
c36ff1
+        ('ubuntu', '/home/ubuntu/.ssh/authorized_keys',
c36ff1
+         TEST_DEFAULT_KEYS),
c36ff1
+        ('root', '/root/.ssh/authorized_keys', TEST_DEFAULT_KEYS),
c36ff1
+    ]
c36ff1
+    common_verify(client, expected_keys)
c36ff1
+
c36ff1
+
c36ff1
+AUTHORIZED_KEYS2_USERDATA = _USERDATA.format(bootcmd=(
c36ff1
+    "sed -i 's;#AuthorizedKeysFile.*;AuthorizedKeysFile "
c36ff1
+    "/etc/ssh/authorized_keys %h/.ssh/authorized_keys2;' "
c36ff1
+    "/etc/ssh/sshd_config"))
c36ff1
+
c36ff1
+
c36ff1
+@pytest.mark.ubuntu
c36ff1
+@pytest.mark.user_data(AUTHORIZED_KEYS2_USERDATA)
c36ff1
+def test_authorized_keys2(client: IntegrationInstance):
c36ff1
+    expected_keys = [
c36ff1
+        ('test_user1', '/home/test_user1/.ssh/authorized_keys2',
c36ff1
+         TEST_USER1_KEYS),
c36ff1
+        ('test_user2', '/home/test_user2/.ssh/authorized_keys2',
c36ff1
+         TEST_USER2_KEYS),
c36ff1
+        ('ubuntu', '/home/ubuntu/.ssh/authorized_keys2',
c36ff1
+         TEST_DEFAULT_KEYS),
c36ff1
+        ('root', '/root/.ssh/authorized_keys2', TEST_DEFAULT_KEYS),
c36ff1
+    ]
c36ff1
+    common_verify(client, expected_keys)
c36ff1
+
c36ff1
+
c36ff1
+NESTED_KEYS_USERDATA = _USERDATA.format(bootcmd=(
c36ff1
+    "sed -i 's;#AuthorizedKeysFile.*;AuthorizedKeysFile "
c36ff1
+    "/etc/ssh/authorized_keys %h/foo/bar/ssh/keys;' "
c36ff1
+    "/etc/ssh/sshd_config"))
c36ff1
+
c36ff1
+
c36ff1
+@pytest.mark.ubuntu
c36ff1
+@pytest.mark.user_data(NESTED_KEYS_USERDATA)
c36ff1
+def test_nested_keys(client: IntegrationInstance):
c36ff1
+    expected_keys = [
c36ff1
+        ('test_user1', '/home/test_user1/foo/bar/ssh/keys',
c36ff1
+         TEST_USER1_KEYS),
c36ff1
+        ('test_user2', '/home/test_user2/foo/bar/ssh/keys',
c36ff1
+         TEST_USER2_KEYS),
c36ff1
+        ('ubuntu', '/home/ubuntu/foo/bar/ssh/keys',
c36ff1
+         TEST_DEFAULT_KEYS),
c36ff1
+        ('root', '/root/foo/bar/ssh/keys', TEST_DEFAULT_KEYS),
c36ff1
+    ]
c36ff1
+    common_verify(client, expected_keys)
c36ff1
+
c36ff1
+
c36ff1
+EXTERNAL_KEYS_USERDATA = _USERDATA.format(bootcmd=(
c36ff1
+    "sed -i 's;#AuthorizedKeysFile.*;AuthorizedKeysFile "
c36ff1
+    "/etc/ssh/authorized_keys /etc/ssh/authorized_keys/%u/keys;' "
c36ff1
+    "/etc/ssh/sshd_config"))
c36ff1
+
c36ff1
+
c36ff1
+@pytest.mark.ubuntu
c36ff1
+@pytest.mark.user_data(EXTERNAL_KEYS_USERDATA)
c36ff1
+def test_external_keys(client: IntegrationInstance):
c36ff1
+    expected_keys = [
c36ff1
+        ('test_user1', '/etc/ssh/authorized_keys/test_user1/keys',
c36ff1
+         TEST_USER1_KEYS),
c36ff1
+        ('test_user2', '/etc/ssh/authorized_keys/test_user2/keys',
c36ff1
+         TEST_USER2_KEYS),
c36ff1
+        ('ubuntu', '/etc/ssh/authorized_keys/ubuntu/keys',
c36ff1
+         TEST_DEFAULT_KEYS),
c36ff1
+        ('root', '/etc/ssh/authorized_keys/root/keys', TEST_DEFAULT_KEYS),
c36ff1
+    ]
c36ff1
+    common_verify(client, expected_keys)
c36ff1
-- 
c36ff1
2.27.0
c36ff1