From 2fb656fd991d788ed54e098815d93458e46f069e Mon Sep 17 00:00:00 2001
From: Brett Holman <brett.holman@canonical.com>
Date: Fri, 24 Nov 2023 15:54:09 +0000
Subject: [PATCH] fix: Don't loosen the permissions of the log file (#4628)

Previous implementations loosened permissions in non-default scenarios.

Fixes GH-4243
---
 cloudinit/stages.py            | 15 +++++++++++-
 tests/unittests/test_stages.py | 45 +++++++++++++++++++++++++---------
 2 files changed, 47 insertions(+), 13 deletions(-)

diff --git a/cloudinit/stages.py b/cloudinit/stages.py
index 509d8f7..79072e7 100644
--- a/cloudinit/stages.py
+++ b/cloudinit/stages.py
@@ -8,6 +8,7 @@ import copy
 import os
 import sys
 from collections import namedtuple
+from contextlib import suppress
 from typing import Dict, Iterable, List, Optional, Set
 
 from cloudinit import cloud, distros, handlers, helpers, importer
@@ -199,13 +200,25 @@ class Init:
     def initialize(self):
         self._initialize_filesystem()
 
+    @staticmethod
+    def _get_strictest_mode(mode_1: int, mode_2: int) -> int:
+        return mode_1 & mode_2
+
     def _initialize_filesystem(self):
+        mode = 0o640
+
         util.ensure_dirs(self._initial_subdirs())
         log_file = util.get_cfg_option_str(self.cfg, "def_log_file")
         if log_file:
             # At this point the log file should have already been created
             # in the setupLogging function of log.py
-            util.ensure_file(log_file, mode=0o640, preserve_mode=False)
+            with suppress(OSError):
+                mode = self._get_strictest_mode(
+                    0o640, util.get_permissions(log_file)
+                )
+
+            # set file mode to the strictest of 0o640 and the current mode
+            util.ensure_file(log_file, mode, preserve_mode=False)
             perms = self.cfg.get("syslog_fix_perms")
             if not perms:
                 perms = {}
diff --git a/tests/unittests/test_stages.py b/tests/unittests/test_stages.py
index a61f9df..2f62eb7 100644
--- a/tests/unittests/test_stages.py
+++ b/tests/unittests/test_stages.py
@@ -606,23 +606,44 @@ class TestInit_InitializeFilesystem:
         # Assert we create it 0o640  by default if it doesn't already exist
         assert 0o640 == stat.S_IMODE(log_file.stat().mode)
 
-    def test_existing_file_permissions(self, init, tmpdir):
+    @pytest.mark.parametrize(
+        "input, expected",
+        [
+            (0o777, 0o640),
+            (0o640, 0o640),
+            (0o606, 0o600),
+            (0o501, 0o400),
+        ],
+    )
+    def test_existing_file_permissions(self, init, tmpdir, input, expected):
         """Test file permissions are set as expected.
 
-        CIS Hardening requires 640 permissions. These permissions are
-        currently hardcoded on every boot, but if there's ever a reason
-        to change this, we need to then ensure that they
-        are *not* set every boot.
+        CIS Hardening requires file mode 0o640 or stricter. Set the
+        permissions to the subset of 0o640 and the current
+        mode.
 
         See https://bugs.launchpad.net/cloud-init/+bug/1900837.
         """
         log_file = tmpdir.join("cloud-init.log")
         log_file.ensure()
-        # Use a mode that will never be made the default so this test will
-        # always be valid
-        log_file.chmod(0o606)
+        log_file.chmod(input)
         init._cfg = {"def_log_file": str(log_file)}
-
-        init._initialize_filesystem()
-
-        assert 0o640 == stat.S_IMODE(log_file.stat().mode)
+        with mock.patch.object(stages.util, "ensure_file") as ensure:
+            init._initialize_filesystem()
+            assert expected == ensure.call_args[0][1]
+
+
+@pytest.mark.parametrize(
+    "mode_1, mode_2, expected",
+    [
+        (0o777, 0o640, 0o640),
+        (0o640, 0o777, 0o640),
+        (0o640, 0o541, 0o440),
+        (0o111, 0o050, 0o010),
+        (0o631, 0o640, 0o600),
+        (0o661, 0o640, 0o640),
+        (0o453, 0o611, 0o411),
+    ],
+)
+def test_strictest_permissions(mode_1, mode_2, expected):
+    assert expected == stages.Init._get_strictest_mode(mode_1, mode_2)
-- 
2.27.0

