Skip to content

Implement OWASP security logging.#6689

Open
blackboxsw wants to merge 11 commits intocanonical:mainfrom
blackboxsw:owasp-logs-poc
Open

Implement OWASP security logging.#6689
blackboxsw wants to merge 11 commits intocanonical:mainfrom
blackboxsw:owasp-logs-poc

Conversation

@blackboxsw
Copy link
Collaborator

@blackboxsw blackboxsw commented Jan 26, 2026

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

    feat: add OSWAP security event logging for user creation and system restart
    
    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).

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 distros

Test Steps

CLOUD_INIT_CLOUD_INIT_OS_IMAGE=resolute tox -e integration-tests -- tests/integration_tests/modules/test_combined.py::TestCombined::test_security_logs

## Merge type

- [ ] Squash merge using "Proposed Commit Message"
- [x] Rebase and merge unique commits. Requires commit messages per-commit each referencing the pull request number (#<PR_NUM>)

…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).
@github-actions github-actions bot added the documentation This Pull Request changes documentation label Jan 26, 2026
@blackboxsw blackboxsw requested a review from holmanb January 26, 2026 21:48
@blackboxsw blackboxsw assigned blackboxsw and holmanb and unassigned blackboxsw Jan 26, 2026
@blackboxsw
Copy link
Collaborator Author

cc: @dbungert

Copy link
Member

@holmanb holmanb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +146 to +150
# 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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +825 to +829
sec_log_user_created(
userid="cloud-init",
new_userid=name,
attributes={"snapuser": True, "sudo": True},
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good point, let's go decorator route instead,

LOG = logging.getLogger(__name__)

# Hard-coded application identifier per spec
APP_ID = "canonical.cloud_init"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the underscore? If no hyphen is allowed then I would prefer "canonical.cloudinit" over this.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The changes in this file seem unnecessary.

userid: str,
new_userid: str,
attributes: Optional[Dict[str, Any]] = None,
log_file: Optional[str] = DEFAULT_SECURITY_LOG,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the log file parameters? This seems unnecessary.

def sec_log_user_created(
userid: str,
new_userid: str,
attributes: Optional[Dict[str, Any]] = None,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional parameters often complicate type annotations unnecessarily. Can we restructure this to be cleaner? Same comment repeated throughout this code.

util.logexc(LOG, "Failed to set password for %s", user)
raise e

# Log security event for password change
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment seems unnecessary.

cmd = ["chpasswd"] + (["-e"] if hashed else [])
subp.subp(cmd, data=payload)

# Log security event for each password change
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This conditional is unnecessary.

@holmanb holmanb added the incomplete Action required by submitter label Feb 10, 2026
@github-actions
Copy link

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.)

@github-actions github-actions bot added the stale-pr Pull request is stale; will be auto-closed soon label Feb 25, 2026
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.
@github-actions github-actions bot removed the stale-pr Pull request is stale; will be auto-closed soon label Feb 26, 2026
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.
@blackboxsw blackboxsw removed the incomplete Action required by submitter label Mar 2, 2026
@blackboxsw blackboxsw changed the title POC: Implement OWASP security logging. Implement OWASP security logging. Mar 5, 2026
@blackboxsw blackboxsw requested a review from holmanb March 5, 2026 05:19
@blackboxsw
Copy link
Collaborator Author

I believe I have addressed all comments and this is ready for re-review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation This Pull Request changes documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants