-
Notifications
You must be signed in to change notification settings - Fork 1.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
beiboot: feature parity with cockpit-ssh #19401
beiboot: feature parity with cockpit-ssh #19401
Conversation
This comment was marked as outdated.
This comment was marked as outdated.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks like it's coming together really nicely and I'm starting to get excited about the finished result. 🚀
src/cockpit/beiboot.py
Outdated
|
||
async def do_askpass(self, messages: str, prompt: str, hint: str) -> Optional[str]: | ||
if self.basic_password and 'password:' in prompt.lower(): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yikes...
I guess this will work "for now" but we really need to do something more (airquotes) 'professional' here...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, see my comment above, that's why it's still draft. Of course we could also consider/mark this as # HACK
and update ferny as a follow-up. I still do want to do that, but it's much nicer to iterate on something that actually works.
774f2ee
to
60c6729
Compare
edae05b
to
ee489a1
Compare
This comment was marked as resolved.
This comment was marked as resolved.
fede982
to
1220a19
Compare
This comment was marked as outdated.
This comment was marked as outdated.
d7f3fa3
to
a10e8a8
Compare
This comment was marked as outdated.
This comment was marked as outdated.
a10e8a8
to
f3ee0e7
Compare
aeae62e
to
f21866b
Compare
This comment was marked as resolved.
This comment was marked as resolved.
f21866b
to
fb73cc7
Compare
@mvollmer Can you please review this? This PR almost celebrates its first birthday, and I sorted out all the disagreements with @allisonkarlitskaya. This is a prerequisite for fully eliminating cockpit-ssh in PR #19441. That mostly works, but still needs some more things done. |
f7c3ba6
to
1cd9453
Compare
Just rebasing to current main to get an updated set of tests, as this became stale. No actual changes. |
Consistently spell the section "Ssh-Login". This makes it easier to grep. Document the current behaviour that the specified `host` will not have its host key checked. Fix indentation of the `connectToUnknownHosts` paragraph.
We only ever need to get or update the keys for a particular host. So replace the current direct database manipulation with `{get,set}_hostkeys()`. This will come in handy in the next commits when we need to update the host keys from multiple places.
In the bastion host case, create a temporary UserKnownHostsFile for ssh and populate it with data from the authorization (if provided). On successful login, send the updated key data back to the user via the long-disused `x-login-data` mechanism. That'll get it added to the JSON blob that is sent as the reply to the `GET /login` request. On failed login, send the key data as part of the Cockpit protocol error, in a `known-hosts` attribute. Co-Authored-By: Martin Pitt <[email protected]>
Mock enough of the flatpak environment (/.flatpak-info and `flatpak-spawn` wrapper) to make beiboot.py take its `connect_from_flatpak()` code path. This makes the test more realistic.
In bastion mode, we previously used the default 3 password prompts, and sending the same password every time. This just unnecessarily trigges `faillock` and takes too long. Do the same as in "remote channel" bridge mode (`SshPeer` in remote.py). This can be dropped if/when we rework the whole machinery to not restart ssh from scratch with each new authentication attempt, and just follow the conversation.
Using a constant `-` for the authorization challenge is problematic: The login page load always starts with an initial generic auth challenge, which gets cleaned up (in `cockpit_session_reset()`) after some time. When the second challenge (for e.g. an unknown host key) then uses the same `-` challenge, ws fails with "Invalid conversation token: -". Avoid this by using an *actual* nonce: Combine the pid and time to a value that is reasonably unique to avoid collisions. Also add an assertion that the returned challenge is the one that we actually sent.
1cd9453
to
afce95b
Compare
src/cockpit/beiboot.py
Outdated
self.router = router | ||
self.basic_password = basic_password | ||
self.have_basic_password = bool(basic_password) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is not None? Although one can argue that "" is also not a password
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just slightly different behaviour
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We'd have to change connect_from_bastion_host()
to rewrite an empty password as None here:
user, _, basic_password = user_password.partition(':')
I.e. doable, but less robust IMHO. I can do it if you prefer, though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This would I presume break auth when a user has an empty password, I am not sure if that is supposed to be supported. That is my point, because:
bool("") => False
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, I see -- I suppose I could add a test case for an empty password (not sure if that works with ssh), and then do the above condition as "user and password are empty → user_password = None". I don't know if it's supported as we never tested that anywhere.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It works with ssh with
echo "PermitEmptyPasswords yes" > /etc/ssh/sshd_config.d/01-empty-password.conf
And then I can also log into Cockpit with beiboot.
This works because there isn't any actual prompt from ssh for a password, it logs you straight in.
So ""
never actually appears in this code path, and thus is not None
just works as well. Changed accordingly.
b.eval_js(f"""window.localStorage.setItem('known_hosts', '{{"{my_ip}": "{bad_key}"}}')""") | ||
try_login("admin", "foobar") | ||
b.wait_text("#hostkey-title", f"{my_ip} key changed") | ||
b.wait_in_text("#hostkey-fingerprint", "SHA256:") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we want to bother testing ED25519 host keys?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We actually do -- our VMs use that by default. But we don't really care about the specific key type anywhere -- login page and localStorage treat that as an opaque value.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Was more about the custom regex parsing. I think we actually don't test IPv6 in this scenario.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see. Doesn't matter much for this particular case, as host names can't have spaces.
Nevertheless, IPv6 tests can't hurt, added. I pulled in another commit from PR #19441 to fix IPv6 address parsing in beiboot.py (that's covered by the full integration tests, but not yet in TestLogin.testLoginSshBeiboot
-- note that that test will go away again).
Teach `via_ssh()` to correctly treat IPv6 addresses like `::1`, i.e. don't consider a double :: as a port separator. For IPv6 addresses with a port, the usual syntax is `[A::B:C]:22`, but `ssh` does not recognize the brackets. Strip them off after separating the port. (Both covered by `TestLogin.testServer`)
afce95b
to
a33575d
Compare
@jelly Addressed your comments: interdiff ... and found/tested/fixed another small bug |
a33575d
to
6699c33
Compare
Oh-uh, setting an empty password with There's a related problem on rhel-9-5 where Fixed both by using |
6699c33
to
242e2d3
Compare
Interesting arch bug -- after Update: It's due to restarting sshd with the new |
In order to use cockpit.beiboot as cockpit-ssh replacement from the bastion host (not Client mode) login page, it needs to consider the given username and password. cockpit-ssh sends an initial `authorize` message for that and checks for "Basic" auth. If that fails, it aborts immediately with `authentication-failed`. Implement the same in cockpit.beiboot. Note: The UI does not currently get along with multiple password attempts. Once we drop cockpit-ssh, we should fix the UI and cockpit.beiboot to behave like the flatpak, keep the initial SSH running, and just answer the "try again" prompts. Cover this in a new `TestLogin.testLoginSshBeiboot`. Once we generally replace cockpit-ssh with cockpit.beiboot, this will get absorbed by TestLogin and TestMultiMachine* and can be dropped again.
Stop treating host key prompts as generic conversation messages. We want the UI to handle them properly, with some verbiage/buttons and the UI/recipe for confirming/validating host keys, instead of letting the user type "yes". The login page recognizes these through the presence of the `host-key` authorize field. We can't use ferny's builtin `do_hostkey()` responder for this, as that requires `ferny.Session(handle_host_key=True)`, and that API is not flexible enough to handle our ssh command modifications and the extra beiboot_helper handler. This needs some bigger redesign, see cockpit-project#19668. So just recognize and parse SSH's host key prompts, and rely on our integration tests to spot breakage in future distro releases. While cockpit-ssh gave us the full host key right away during conversation, ssh (and thus ferny/beiboot) don't work that way: During conversation we only get the fingerprint. So for the sending direction, utilize the recently introduced temporary UserKnownHostsFile feature of beiboot, and send the known_hosts entry for the given host as part of the `Authorization:` header. For the receiving direction we need to handle three cases: * For a previously unknown host and a successful login, we only get the full host key as part of the GET /login's `login-data` response. So defer updating our localStorage's `known_hosts` database during the conversation until that happens. * For a failed login (like wrong password) after already confirming the key change, get the host key from the protocol error message. * ssh (and thus ferny/beiboot) don't report changed host keys as part of conversation, but immediatley abort. So remember that state in `ssh_host_key_change_host`, and re-attempt the login without sending known_hosts data. Our usual logic in `do_hostkey_verification()` will notice the change and present the correct dialog. Adjust TestLogin.testLoginSshBeiboot to only expect the host key on the first login attempt. Add testing of changed host key behaviour.
242e2d3
to
db8d88c
Compare
|
||
if (key_db[key_host] == key) { | ||
// code path for old C cockpit-ssh, which doesn't set a known_hosts file in advance (like beiboot) | ||
if (db_keys == key) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This added line is not executed by any test.
} catch (ex) { | ||
console.error("Failed to parse response text as JSON:", xhr.responseText, ":", JSON.stringify(ex)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These 2 added lines are not executed by any test.
console.debug("setup_localstorage(): updating known_hosts database for deferred host key for", login_data_host, ":", hostkey); | ||
set_hostkeys(login_data_host, hostkey); | ||
} else { | ||
console.error("login.js internal error: setup_localstorage() received a pending login-data host, but login-data does not contain known-hosts"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This added line is not executed by any test.
Yay -- this PR missed its first birthday cake by 4 days! 🎂 |
In order to use cockpit-beiboot as cockpit-ssh replacement from the "normal" (not Client mode) login page, it needs to consider the given username and password. It also needs to properly handle "unknown host key" prompts.
This paves the way for completely replacing cockpit-ssh with cockpit.beiboot. See https://issues.redhat.com/browse/COCKPIT-954 and #19441.