Skip to content

[Ssh] Fix #8994: az ssh cert: Allow skipping certificate file permissions on ACL-less filesystems - #10505

Open
Aditya Pujara (a0x1ab) wants to merge 1 commit into
Azure:mainfrom
a0x1ab:agent-assist/azure-azure-cli-extensions-issue-8994-52a4a7761084
Open

Aditya Pujara (a0x1ab) wants to merge 1 commit into
Azure:mainfrom
a0x1ab:agent-assist/azure-azure-cli-extensions-issue-8994-52a4a7761084

Conversation

@a0x1ab

@a0x1ab Aditya Pujara (a0x1ab) commented Oct 22, 2026 •

Copy link
Copy Markdown
Member

🤖 PR Validation — ⚠️ Review suggested

Breaking Changes
⚠️ None
⚠️Azure CLI Extensions Breaking Change Test
⚠️ssh
rule cmd_name rule_message suggest_message
⚠️ 1006 - ParaAdd ssh cert cmd ssh cert added parameter skip_cert_file_permissions

Description

Fixes #8994.

Allow skipping certificate file permissions on ACL-less filesystems.

Root cause: src/ssh/azext_ssh/custom.py:_write_cert_file writes and closes the public certificate, then unconditionally calls oschmod.set_mode(cert_file, 0o644). oschmod 0.3.12 dereferences a missing Windows DACL in _win_set_permissions, aborting az ssh cert after successful output. ssh_cert and _params.py:load_arguments had no permission bypass.

Fix: Added cert-only --skip-cert-file-permissions (short alias --skip-cert-perms to satisfy the CLI option-length rule), default false, threaded from ssh_cert through _get_and_write_certificate to _write_cert_file. Explicit skip writes the certificate normally, avoids oschmod, and warns that file permissions were not adjusted. Default behavior still sets mode 0644; a finally block localized to permission setting emits retry guidance on any permission exception without catching or replacing it. Actual write errors are outside that block and still fail. Added real-file and …

How verified (trusted validation output):

  • Trusted validation passed (1 command).
  • New or changed tests fail on the base revision and pass with the change: src/ssh/azext_ssh/tests/latest/test_custom.py::SshCertPermissionsTest::test_ssh_cert_cli_argument_and_help, src/ssh/azext_ssh/tests/latest/test_custom.py::SshCertPermissionsTest::test_ssh_cert_default_permissions, src/ssh/azext_ssh/tests/latest/test_custom.py::SshCertPermissionsTest::test_ssh_cert_permission_errors_preserved_with_retry_guidance.
  • Unit tests: 37 executed, 0 skipped.
  • flake8, pylint and the breaking change check passed.
  • Pre-publication gates passed: release-artifact, generated-ownership, command-help, regression-test, independent-review.
  • Repository CI and live tests run on this PR.

Risks and limitations (from the job report):

  • No real Windows memory filesystem with absent DACL is available here; deterministic fault injection reproduces …
  • Explicit skip leaves filesystem-provided public certificate permissions unchanged and emits a warning. It does not …
  • Repository CI, Windows/Azure live tests and final base-revision gates remain required. In-session self-check passed with …

This checklist is used to make sure that common guidelines for a pull request are followed.

Related command

az ssh cert

General Guidelines

  • Have you run azdev style <YOUR_EXT> locally? (pip install azdev required)
  • Have you run python scripts/ci/test_index.py -q locally? (pip install azdev required)
  • My extension version conforms to the Extension version schema

For new extensions:

About Extension Publish

There is a pipeline to automatically build, upload and publish extension wheels.
Once your pull request is merged into main branch, a new pull request will be created to update src/index.json automatically.
You only need to update the version information in file setup.py and historical information in file HISTORY.rst in your PR but do not modify src/index.json.

Independent review: an independent reviewer checked this change against 7 acceptance criteria derived from the issue (5 required); every required criterion is backed by evidence in the diff.

…permissions on ACL-less filesystems

Root cause: src/ssh/azext_ssh/custom.py:_write_cert_file writes and
closes the public certificate, then unconditionally calls
oschmod.set_mode(cert_file, 0o644). oschmod 0.3.12 dereferences a
missing Windows DACL in _win_set_permissions, aborting az ssh cert after
successful output. ssh_cert and _params.py:load_arguments had no
permission bypass.

Change: Added cert-only --skip-cert-file-permissions (short alias
--skip-cert-perms to satisfy the CLI option-length rule), default false,
threaded from ssh_cert through _get_and_write_certificate to
_write_cert_file. Explicit skip writes the certificate normally, avoids
oschmod, and warns that file permissions were not adjusted. Default
behavior still sets mode 0644; a finally block localized to permission
setting emits retry guidance on any permission exception without
catching or replacing it. Actual write errors are outside that block and
still fail. Added real-file and …

Alternatives considered:
- Silently tolerate permission errors: rejected because users must
  explicitly choose to retain filesystem-provided permissions and
  unrelated failures must still surface.
- Patch oschmod or replace its Windows ACL implementation: rejected
  because the extension can explicitly avoid the unsupported operation
  without modifying a dependency or weakening other SSH commands.

Verified:
- Trusted validation passed (1 command).
- New or changed tests fail on the base revision and pass with the
  change:
  `src/ssh/azext_ssh/tests/latest/test_custom.py::SshCertPermissionsTest::test_ssh_cert_cli_argument_and_help`,
  `src/ssh/azext_ssh/tests/latest/test_custom.py::SshCertPermissionsTest::test_ssh_cert_default_permissions`,
  `src/ssh/azext_ssh/tests/latest/test_custom.py::SshCertPermissionsTest::test_ssh_cert_permission_errors_preserved_with_retry_guidance`.
- Unit tests: 37 executed, 0 skipped.
- flake8, pylint and the breaking change check passed.
- Pre-publication gates passed: release-artifact, generated-ownership,
  command-help, regression-test, independent-review.
- Repository CI and live tests run on this PR.

Fixes Azure#8994
@x-engineering-agent

x-engineering-agent Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

X Engineering Agent Report

Review — Comments

Aditya Pujara (@a0x1ab)

No confirmed findings in the bounded semantic review.

  • Issue resolution: The opt-in bypass requested in Writing a cert file to a directory without permission system may fail #8994 is registered only for ssh cert (src/ssh/azext_ssh/_params.py:76-89) and reaches the certificate writer through custom.py:128-129,234. custom.py:336-352 skips permission adjustment with a warning, preserves default permission exceptions with retry guidance, and leaves write failures outside the bypass.
  • Regression evidence: src/ssh/azext_ssh/tests/latest/test_custom.py:502-639 checks certificate contents, both flag spellings, default mode, permission-error identity, write failures, and exclusion from sibling commands. Upstream CI reports 24 passed of 25 completed checks, with zero failed.
  • Not verified: Actual Windows ACL-less filesystem behavior and real certificate issuance/parsing are not established by these mocked regression tests. No tests were run during this review; the Agent live-test result below is supplied by the router, not a new execution.
Test validation
  • Live test: Passed. 37 of 37 test(s) passed on 461a92853bc3. Workflow run
  • Regression coverage: Not applicable to the changed files.
Risk assessment — 47/100 · Medium

The Medium rating is driven by public CLI behavior, dependency or supply chain, failure-handling behavior.

  • Change scope: 6 changed files, 197 changed lines (+187 / -10), including 4 production files.
  • Affected components: ssh
  • Risk drivers: public CLI behavior (+18); dependency or supply chain (+18); failure-handling behavior (+12)
  • Regression evidence: Changed regression tests are included, reducing risk.
  • Confidence: High because changed-line patches were available for every production file.
  • Required review: Owning-squad review is required for ssh before merge.

Posted by x-engineering-agent (Reviewer)

Live test — Passed (37/37)

PASS

Selectors: azext_ssh.test_custom (extension)
PR head ref: agent-assist/azure-azure-cli-extensions-issue-8994-52a4a7761084
PR head sha: 461a92853bc318e8a73d1f1fc5354900149d618b
PR base ref: main
New test files in PR: false

Changed test files run
src/ssh/azext_ssh/tests/latest/test_custom.py

Workflow run: https://github.com/Azure/issue-sentinel/actions/runs/39729330465

Last 80 lines of azdev output

=============
| Run Tests |
=============


test index found: /home/runner/.azdev/env_config/home/runner/work/issue-sentinel/issue-sentinel/.venv/test_index/latest.json

Test on modules: azext_ssh.test_custom

WARNING: RUNNING TESTS LIVE
The tests are set to run against current profile "latest"
============================= test session starts ==============================
platform linux -- Python 3.12.15, pytest-9.1.1, pluggy-1.7.0 -- /home/runner/work/issue-sentinel/issue-sentinel/.venv/bin/python
cachedir: .pytest_cache
rootdir: /home/runner/work/issue-sentinel/issue-sentinel/azure-cli-extensions
configfile: pytest.ini
plugins: forked-1.7.5, xdist-3.8.0, platformdirs-4.13.5
collecting ... collected 37 items

azure-cli-extensions/src/ssh/azext_ssh/tests/latest/test_custom.py::SshCustomCommandTest::test_assert_args_cert_with_no_user PASSED [  2%]
azure-cli-extensions/src/ssh/azext_ssh/tests/latest/test_custom.py::SshCustomCommandTest::test_assert_args_invalid_cert_filepath PASSED [  5%]
azure-cli-extensions/src/ssh/azext_ssh/tests/latest/test_custom.py::SshCustomCommandTest::test_assert_args_invalid_resource_type PASSED [  8%]
azure-cli-extensions/src/ssh/azext_ssh/tests/latest/test_custom.py::SshCustomCommandTest::test_assert_args_ip_with_vm_or_rg PASSED [ 10%]
azure-cli-extensions/src/ssh/azext_ssh/tests/latest/test_custom.py::SshCustomCommandTest::test_assert_args_no_ip_or_vm PASSED [ 13%]
azure-cli-extensions/src/ssh/azext_ssh/tests/latest/test_custom.py::SshCustomCommandTest::test_assert_args_vm_rg_mismatch PASSED [ 16%]
azure-cli-extensions/src/ssh/azext_ssh/tests/latest/test_custom.py::SshCustomCommandTest::test_check_or_create_public_private_files_defaults PASSED [ 18%]
azure-cli-extensions/src/ssh/azext_ssh/tests/latest/test_custom.py::SshCustomCommandTest::test_check_or_create_public_private_files_defaults_with_cred_folder PASSED [ 21%]
azure-cli-extensions/src/ssh/azext_ssh/tests/latest/test_custom.py::SshCustomCommandTest::test_check_or_create_public_private_files_no_private PASSED [ 24%]
azure-cli-extensions/src/ssh/azext_ssh/tests/latest/test_custom.py::SshCustomCommandTest::test_check_or_create_public_private_files_no_public PASSED [ 27%]
azure-cli-extensions/src/ssh/azext_ssh/tests/latest/test_custom.py::SshCustomCommandTest::test_delete_credentials_not_cloudshell PASSED [ 29%]
azure-cli-extensions/src/ssh/azext_ssh/tests/latest/test_custom.py::SshCustomCommandTest::test_do_ssh_arc_op_aad_user PASSED [ 32%]
azure-cli-extensions/src/ssh/azext_ssh/tests/latest/test_custom.py::SshCustomCommandTest::test_do_ssh_op_aad_user_compute PASSED [ 35%]
azure-cli-extensions/src/ssh/azext_ssh/tests/latest/test_custom.py::SshCustomCommandTest::test_do_ssh_op_arc_local_user PASSED [ 37%]
azure-cli-extensions/src/ssh/azext_ssh/tests/latest/test_custom.py::SshCustomCommandTest::test_do_ssh_op_local_user_compute PASSED [ 40%]
azure-cli-extensions/src/ssh/azext_ssh/tests/latest/test_custom.py::SshCustomCommandTest::test_do_ssh_op_no_public_ip PASSED [ 43%]
azure-cli-extensions/src/ssh/azext_ssh/tests/latest/test_custom.py::SshCustomCommandTest::test_get_modulus_exponent_file_not_found PASSED [ 45%]
azure-cli-extensions/src/ssh/azext_ssh/tests/latest/test_custom.py::SshCustomCommandTest::test_get_modulus_exponent_parse_error PASSED [ 48%]
azure-cli-extensions/src/ssh/azext_ssh/tests/latest/test_custom.py::SshCustomCommandTest::test_get_modulus_exponent_success PASSED [ 51%]
azure-cli-extensions/src/ssh/azext_ssh/tests/latest/test_custom.py::SshCustomCommandTest::test_ssh_arc PASSED [ 54%]
azure-cli-extensions/src/ssh/azext_ssh/tests/latest/test_custom.py::SshCustomCommandTest::test_ssh_cert PASSED [ 56%]
azure-cli-extensions/src/ssh/azext_ssh/tests/latest/test_custom.py::SshCustomCommandTest::test_ssh_cert_cert_file_missing PASSED [ 59%]
azure-cli-extensions/src/ssh/azext_ssh/tests/latest/test_custom.py::SshCustomCommandTest::test_ssh_cert_no_args PASSED [ 62%]
azure-cli-extensions/src/ssh/azext_ssh/tests/latest/test_custom.py::SshCustomCommandTest::test_ssh_config_cred_folder PASSED [ 64%]
azure-cli-extensions/src/ssh/azext_ssh/tests/latest/test_custom.py::SshCustomCommandTest::test_ssh_config_credentials_folder_and_key PASSED [ 67%]
azure-cli-extensions/src/ssh/azext_ssh/tests/latest/test_custom.py::SshCustomCommandTest::test_ssh_config_no_cred_folder PASSED [ 70%]
azure-cli-extensions/src/ssh/azext_ssh/tests/latest/test_custom.py::SshCustomCommandTest::test_ssh_vm PASSED [ 72%]
azure-cli-extensions/src/ssh/azext_ssh/tests/latest/test_custom.py::SshCustomCommandTest::test_ssh_vm_debug PASSED [ 75%]
azure-cli-extensions/src/ssh/azext_ssh/tests/latest/test_custom.py::SshCustomCommandTest::test_ssh_vm_delete_credentials_cloudshell PASSED [ 78%]
azure-cli-extensions/src/ssh/azext_ssh/tests/latest/test_custom.py::SshCustomCommandTest::test_ssh_vm_rdp PASSED [ 81%]
azure-cli-extensions/src/ssh/azext_ssh/tests/latest/test_custom.py::SshCustomCommandTest::test_write_cert_file PASSED [ 83%]
azure-cli-extensions/src/ssh/azext_ssh/tests/latest/test_custom.py::SshCertPermissionsTest::test_ssh_cert_cli_argument_and_help PASSED [ 86%]
azure-cli-extensions/src/ssh/azext_ssh/tests/latest/test_custom.py::SshCertPermissionsTest::test_ssh_cert_default_permissions PASSED [ 89%]
azure-cli-extensions/src/ssh/azext_ssh/tests/latest/test_custom.py::SshCertPermissionsTest::test_ssh_cert_permission_errors_preserved_with_retry_guidance PASSED [ 91%]
azure-cli-extensions/src/ssh/azext_ssh/tests/latest/test_custom.py::SshCertPermissionsTest::test_ssh_cert_skip_permissions PASSED [ 94%]
azure-cli-extensions/src/ssh/azext_ssh/tests/latest/test_custom.py::SshCertPermissionsTest::test_ssh_cert_write_error_not_skipped PASSED [ 97%]
azure-cli-extensions/src/ssh/azext_ssh/tests/latest/test_custom.py::SshCertPermissionsTest::test_write_cert_file_skip_permissions PASSED [100%]

- generated xml file: /home/runner/work/issue-sentinel/issue-sentinel/test-output/results.xml -
============================== 37 passed in 0.94s ==============================

Workflow run

Posted by x-engineering-agent (Tester)

@x-engineering-agent
x-engineering-agent Bot marked this pull request as ready for review October 22, 2026 05:54
Copilot AI balanced review requested due to automatic review settings October 22, 2026 05:54
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
There may be pipelines that require an authorized user to comment /azp run to run.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 Approval recommended

The narrowly scoped, opt-in bypass preserves existing defaults and error propagation, with focused regression coverage and no unresolved findings.

No open issues 🎉

What changed in this PR

Addresses #8994 by adding an opt-in permission bypass to az ssh cert, allowing public certificates to be written on filesystems without access control list (ACL) support.

Changes:

  • Adds --skip-cert-file-permissions and its shorter alias while preserving default permission enforcement and write-error propagation.
  • Adds warnings, retry guidance, help examples, and regression tests.
  • Updates the SSH extension to version 2.0.10 and records the change.
File Description
src/​ssh/​setup.py Bumps the extension version to 2.0.10.
src/​ssh/​HISTORY.rst Documents the permission bypass and preserved defaults.
src/​ssh/​azext_ssh/​tests/​latest/​test_custom.py Tests bypass behavior, defaults, errors, and CLI registration.
src/​ssh/​azext_ssh/​custom.py Threads the bypass option through certificate writing and adds warnings.
src/​ssh/​azext_ssh/​_params.py Registers the cert-only option and alias.
src/​ssh/​azext_ssh/​_help.py Adds a usage example for filesystems without ACL support.

Review effort: Balanced

@x-engineering-agent x-engineering-agent Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Aditya Pujara (@a0x1ab) Ready for human review. See the X Engineering Agent Report.

@x-engineering-agent x-engineering-agent Bot added X Engineering Agent Reviewed Agent reviewed this PR and removed Request X Engineering Agent Ask the agent to triage this issue or review this PR labels Oct 22, 2026
@yonzhan

Copy link
Copy Markdown
Collaborator

Please fix CI issues

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Writing a cert file to a directory without permission system may fail

3 participants