Hacker Newsnew | past | comments | ask | show | jobs | submitlogin
Building Interactive SSH Applications (drewdevault.com)
321 points by stargrave on Sept 2, 2019 | hide | past | favorite | 44 comments


Neat. I was always wary about unintended side-effects of doing something like this in the main openssh server instance because I did not trust myself to configure everything securely enough without accidentally exposing more than I wanted. While more work than the solution in the blog post, what worked well for me was using [0] to implement a separate server that only exposes whatever you explicitly want it to. It turned out to be pretty feature complete for my use cases (including certificate logins, SFTP, exec and working fine with curses-like applications).

[0] https://github.com/mscdex/ssh2


There's also Go and assembler as evidenced by:

Show HN: ssh chat.shazow.net https://news.ycombinator.com/item?id=8743374

Sshtalk: An SSH-based chat made in assembler https://news.ycombinator.com/item?id=15829206


Nice github username :-)


Just for the record, that's not my library, I just used it in a few projects. The author is on HN though.


On Python you can use paramiko


An interesting, useful real-world example of this is Gitolite [0]. Gitolite requires no background daemons or anything -- just a normal unix/ssh environment.

[0] https://gitolite.com/gitolite/index.html


I'm using gitolite from years and works flawless.


I just started using it and I love it. Simple, easy to configure, and it just works.


Interesting approach. I have had good luck with the go ssh library; and that server doesn't require you to deal with any UNIX internals. You get a channel that you can read and write to; so if you just want to implement nethack, and your nethack library has something like "func (*nethack) Command(string) string", then you feed their lines (perhaps parsed with bufio.Scanner) into that, and send the results back to the channel. Thus, very little can go wrong. Some system configuration changes, and there is no way that access can escalate to shell access.

The reason I'm familiar with the client was to automate connections to network hardware. Basically, we have a rotated or per-user password stored somewhere secure; the ssh client contacts that server, gets the credentials, and then creates a connection to the actual device. The ssh client believes that passwords are insecure and should never be used, so doesn't let you programatically pipe one in. My solution? Just rewrite the ssh client to do what I want. golang.org/x/crypto/ssh made it simple, and users don't even know that they're using a fake SSH client, as I copied the text from the real SSH client verbatim (for accepting host keys and whatnot). As for ssh servers; I had to write one to test the client.

The only thing to beware of is that you need to manage the user's tty; if you haven't used a tty in raw mode before it will be a new experience. And the library has a bunch of places where waits are unbounded. So you need to do anything that blocks in its own goroutine that kills the connection when <-ctx.Done returns. Or accept indefinite blocking, as most programs seem to be happy with. Other than that, I like it a lot. Seems safer to me to just write the program you want, rather than to deal with 9000 historical edge cases in the opensshd+unix combination. PAM is also not involved ever, which is a great improvement to everyone's life.


I want to say that prgmr.com used (uses?) something like this for out of band access to VMs for serial console access or reboots, etc [0]. It certainly can be a powerful tool to give someone limited access to infrastructure.

I swear I read about this in a blog post, but that was many years ago and I couldn't quickly find it.

[0] https://wiki.prgmr.com/mediawiki/index.php/Management_Consol...


We still do this, though the implementation has had a complete rewrite. It's very accessible.


What do you mean by "very accessible"? Are you referring to the design and implementation of this interaction technique? Or the accessibility of the UI?


Probably meaning the amount of access you get to configure your VM. I’ve never had a need for one from them, but the amount of configuration they describe in their documentation from an SSH/CLI to your VM is pretty comprehensive. It’s a slick way to avoid the overhead and infrastructure needed to support a big web GUI.

You can pretty much see the entire interface from the link above.


> def tail(job_id, info):

> logs = os.path.join(cfg("builds.sr.ht::worker", "buildlogs"), str(job_id))

Depending on where your job_id comes from, you may want to ensure "logs" variable still ends up pointing a subdirectory of buildlogs. You could use a combination of os.path.abspath() and various methods for checking the common prefix.

Maybe this is only a simplified snippet, but concatenating values with external parameters without checking ng the end result can lead to unpleasant surprises in production code.


I believe job_id is an int in this case. Buy yeah, it doesn't hurt to double-check, just in case it somehow gets called with a user-supplied variable at some point.


So can we have a BBS revival now? :) I wonder what are the differences that make vinyl have a revival but not bbs via ssh.


There seems to be quite a few BBS systems with SSH as a connection option out there: https://www.telnetbbsguide.com/connection/ssh/ :-)


A wrote a little one in Perl a few years ago. The primary challenges were the same ones you would have with any curses application and annoying input latency. The users were also constantly trying to escape to a shell, but I don’t remember any of them succeeding.

If you thought websites were centralized, try a system where every keystroke gets mirrored to the server!


To think, interactive curses apps like that used to be the norm. It was much simpler building apps that way, compared to web apps. I rarely used curses when I built my own stuff in the 90's. I generated ANSI escape sequences myself...


I ran a BBS over SSH and telnet in the mid 90's through early 2000's. All users had restricted shells (the BBS app.) There's many, many BBSes out there, but most of them are dead... almost no activity.

I would really like to see a Usenet / NNTP revival.


I think it’s underway. The keywords are “pubnix” and “phlog.”


Be careful with the authorized keys file approach. If the commands allow the user to append content to it, the commands can be changed. Most of the time the file is writable by the user (unless you specify otherwise in the opensshd configuration)


Possibly a lot of shell command interpolation going on there.

What if I try to login as "bob'; rm -rf /" or some such? Is the system robust against that?


The only point I see for injection is when OpenSSH runs the AuthorizedKeysCommand. I'm pretty sure OpenSSH handles it fine.


Then look at the code. You may be surprised.

Looking at the escaping function, it will allow $(execute whatever you want), because it doesn't handle $.

https://github.com/openssh/openssh-portable/blob/master/misc...

Anyway, this depends on being able to specify any username, which while possible, requires you to pass the check that user exists in the system, which will be harder.

But it may still be possible if some weird PAM module is used.


I don't think that function is used other than for logging.

https://github.com/openssh/openssh-portable/blob/b52c0c2e649...

There the "command" variable is the result of the function you mention, but here the use is only for logging. The actual argv for the subprocess is the "av" argument which comes from first splitting and then replacing "%u" etc.


I see. Good.


Changing this to use ' instead of " may fix this issue.

https://github.com/openssh/openssh-portable/blob/master/misc...

Because then you'll end up with '$(some)' instead of "$(some)"


Does bob have proper permissions to delete root? Probably not...

But otherwise, yes, you can run any valid command or pipeline on the server side so rm -rf is certainly acceptable.


Bob does not, but the AuthorizedKeysCommand runs as:

    AuthorizedKeysCommandUser root
I'm pretty sure that is enough permissions to cause trouble.


I confess I read the article after my comment and saw that.

I was just talking about standard ssh behavior.

I wouldn’t use that without seriously studying it end to end. As another comment said, “seems kind of hacky”.


Yeah it is a bit concerning to be honest, feels hacky.


Where do you see the problematic interpolation?


    keys = (f"command=\"buildsrht-shell '{b64key}'\",restrict,pty " +
            f"{key_type} {b64key} somebody\n")
Now it depends on how b64key is sanitized.


AuthorizedKeysCommand, see above


Note that if your process is the root of the process tree controlling a terminal, init will prepend a '-' to argv[0] (e.g. instead of being "bash" it will be "-bash".

I used this to disable c-X c-c (the exit command) when running gnu emacs as my login shell in the mid 1980s.


It'd be great to have more discussion how to build applications to be exposed like this, especially the security aspect; what sort of sandboxing to use etc to minimize attack surface. Basically how to avoid giving users shell, and how to restrict what they can do even if they did gain code execution of some sort.

The web application security practices are pretty well known and lot of that does apply here too. But lot is different too, crucially I imagine each user session is it's own process which means you can apply more OS level primitives to them. chroot would be an obvious example, but there is tons of more things available.


Yes all container technologies are at play, including namespaces, cgroups (for resource quotas), seccomp, Linux Security Modules, and probably some odds and ends I am forgetting.

Of course e.g. FreeBSD is a different bag.


    Try to avoid the use of ‘IN’ or ‘NOT IN’. By
    doing this you are performing a full table scan
    as the query engine looks through every row to
    check if the condition is met.
Is that really true for PostgreSQL? Take this query for example:

   SELECT * FROM t WHERE id IN (1,2,3)
I do this often in MySql DBs. I am pretty sure it uses an index on "id" if one exists. EXPLAIN confirms this. Why would a DB engine not use an index?


No it's not, at least not always. Depending on the query, data, and/or indexes, PostgreSQL will optimise this into a WHERE EXISTS, which in turn is more or less the same as an INNER JOIN. Also, I think you might be commenting on the wrong post.


How is that relevant to this post? I don't see the text you quoted there.


Most likely meant as a comment for https://news.ycombinator.com/item?id=20855441


The cost must be logarithmic, not lineal. When `id` is part of unique key autoincrement index and the database properly implements a binary tree for such id.

EDIT: know what you do before judge and adopt technology, code reviews require previous background.


I mean, there are some reasons not to use NOT IN (https://wiki.postgresql.org/wiki/Don%27t_Do_This#DON.27T_USE...) but that's not a problem if you just have a list of ids.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: