Implement OWASP security logging.#6689
Conversation
…estart Implement OWASP structured logs in /var/log/cloud-init-security.log. Add security-related operations performed by cloud-init on behalf of user-data or platform meta-data: - user creation - user password change - system restart - system shutdown Default security log file can be changed by setting an alternative value for security_log_file in /etc/cloud/cloud.cfg(.d/*.cfg).
|
cc: @dbungert |
holmanb
left a comment
There was a problem hiding this comment.
Thanks for this @blackboxsw. First pass.
If the goal is for this to be cross-distro, then implementing this in the distro class doesn't seem optimal because distro methods are expected to be (and in this case are) overridden.
Also, is there a reason that the logger package cannot be used? Manually writing to files seems undesirable - I would have expected this to define a custom logger for this purpose.
Thank you for the type annotations. Besides just annotating the types, I'd like to see if we can also avoid unnecessary complexity - something that annotations can assist with. For example rather than checking an Optional parameter for trutheyness, we could instead avoid the possibility of None and pass a falsey value instead.
cloudinit/log/security_event_log.py
Outdated
| # Create file with restricted permissions if it doesn't exist | ||
| if not os.path.exists(log_file): | ||
| util.ensure_file(log_file, mode=0o600, preserve_mode=False) | ||
|
|
||
| util.append_file(log_file, json_line, disable_logging=True) |
There was a problem hiding this comment.
This doesn't seem ideal. Was Python's builtin logger considered?
This complicates code in util and makes for a bunch of otherwise unnecessary logger file parameters.
There was a problem hiding this comment.
I've adapted this to use python logging and a new SECURITY level log with custom handler which writes to
/var/log/cloud-init-security.log
cloudinit/distros/__init__.py
Outdated
| sec_log_user_created( | ||
| userid="cloud-init", | ||
| new_userid=name, | ||
| attributes={"snapuser": True, "sudo": True}, | ||
| ) |
There was a problem hiding this comment.
Intermixing code of different different purposes like this will lead to difficulty maintaining and auditing the code. I would prefer if we could find a cleaner design.
There was a problem hiding this comment.
good point, let's go decorator route instead,
cloudinit/log/security_event_log.py
Outdated
| LOG = logging.getLogger(__name__) | ||
|
|
||
| # Hard-coded application identifier per spec | ||
| APP_ID = "canonical.cloud_init" |
There was a problem hiding this comment.
Why the underscore? If no hyphen is allowed then I would prefer "canonical.cloudinit" over this.
There was a problem hiding this comment.
Adapted to prefer hyphenated like our product name.
| rotated_logs = [] | ||
| if not cfg or not isinstance(cfg, dict): | ||
| return logs | ||
| default_log = cfg.get("def_log_file") |
There was a problem hiding this comment.
The changes in this file seem unnecessary.
cloudinit/log/security_event_log.py
Outdated
| userid: str, | ||
| new_userid: str, | ||
| attributes: Optional[Dict[str, Any]] = None, | ||
| log_file: Optional[str] = DEFAULT_SECURITY_LOG, |
There was a problem hiding this comment.
Why the log file parameters? This seems unnecessary.
cloudinit/log/security_event_log.py
Outdated
| def sec_log_user_created( | ||
| userid: str, | ||
| new_userid: str, | ||
| attributes: Optional[Dict[str, Any]] = None, |
There was a problem hiding this comment.
Optional parameters often complicate type annotations unnecessarily. Can we restructure this to be cleaner? Same comment repeated throughout this code.
cloudinit/distros/__init__.py
Outdated
| util.logexc(LOG, "Failed to set password for %s", user) | ||
| raise e | ||
|
|
||
| # Log security event for password change |
There was a problem hiding this comment.
This comment seems unnecessary.
cloudinit/distros/__init__.py
Outdated
| cmd = ["chpasswd"] + (["-e"] if hashed else []) | ||
| subp.subp(cmd, data=payload) | ||
|
|
||
| # Log security event for each password change |
There was a problem hiding this comment.
This comment seems unnecessary.
| if params: | ||
| # Filter out None values and convert to strings | ||
| filtered_params = [str(p) for p in params if p is not None] | ||
| if filtered_params: |
There was a problem hiding this comment.
This conditional is unnecessary.
|
Hello! Thank you for this proposed change to cloud-init. This pull request is now marked as stale as it has not seen any activity in 14 days. If no activity occurs within the next 7 days, this pull request will automatically close. If you are waiting for code review and you are seeing this message, apologies! Please reply, tagging blackboxsw, and he will ensure that someone takes a look soon. (If the pull request is closed and you would like to continue working on it, please do tag blackboxsw to reopen it.) |
Replace LevelBasedFormatter with handler-level filters. SecurityOnlyFilter and NoSecurityFilter are applied at the handler level so SECURITY records flow only to a dedicated FileHandler on cloud-init-output.log using SECURITY_LOG_FORMAT, and are blocked from stderr and LogExporter. setup_security_logging() silently no-ops on OSError so unprivileged test environments are unaffected. Update test_logger_prints_security_as_ json_lines to assert file output and absence from stderr.
753f0fc to
046db76
Compare
Use netdev_info to discover any interface with a global scope IP address. If no IPv4 address is present, return IPv6. If no network devices are up or no global addresses are present, avoid adding host_ip to event logs. Additionally, add "type": "security" to all events.
046db76 to
e7168c0
Compare
…down Correct type hint for netdev_info
|
I believe I have addressed all comments and this is ready for re-review. |
Provide an initial spike for logging OWASP formatted events in cloud-init for discussion. Integration tests will be added upon agreement for security logging procedures.
Proposed Commit Message
Additional Context
A followup PR will provide USER update functionality due to
groups: { admingrp: [root, sys]}because this will require a refactor of Distro.create_group for multiple distrosTest Steps