SSH environment variables in Go

by on
  • go
  • ssh
  • technical

One of the tools I’m mantaining at work is Bunka, a Go CLI application to execute commands in parallel on a list of servers. We’re setting the DEBIAN_FRONTEND environment variable to noninteractive for all our connections as we regularly use apt and related commands. (If we don’t, command execution may hang as a dialog is shown and the process is waiting for user input.)

This week someone created a bug report because their apt-get commands were hanging on multiple servers. Pretty strange, as this was the exact reason we’re setting environment variables in the first place. Let’s investigate!

Changing the environment in Go

To change environment variable for the currently running process you can use the Setenv function provided by the standard library’s os package. This can be useful when you use the os/exec package to start subprocesses.

func Setenv(key, value string) error 

The ssh package provides a similar function that sets an environment variable on an SSH session. These variables will be available for any command you execute on that same session after changing calling the Setenv function.

func (s *Session) Setenv(name, value string) error

As this “simply sets a variable” I was ignoring the errors returned by the Setenv function. After all, what could go wrong when setting a simple environment variable? I couldn’t be more wrong.

ssh: setenv failed

When I actually checked the error object returned by Setenv it turned out not to be nil. It contained the amazingly useful message below.

ssh: setenv failed

Diving in the source code for the ssh package we can see that the request to set an environment variable is sent directly to the SSH server, there’s not much in that Go code that can go wrong. Also note that it returns the same error message regardless of what went wrong when calling SendRequest.

func (s *Session) Setenv(name, value string) error {
	msg := setenvRequest{
		Name:  name,
		Value: value,
	}
	ok, err := s.ch.SendRequest("env", true, Marshal(&msg))
	if err == nil && !ok {
		err = errors.New("ssh: setenv failed")
	}
	return err
}

So let’s take a step back and do it without Go:

[vic@local ~]$ export DEBIAN_FRONTEND=noninteractive
[vic@local ~]$ ssh -o SendEnv=DEBIAN_FRONTEND vic@remote
[vic@remote ~]# echo $DEBIAN_FRONTEND

Nothing, it simply doesn’t work.

Allowing environment variables over SSH

Turns out, you can’t simply change environment variables in SSH sessions. The OpenSSH server denies those requests unless you’ve whitelisted the specific variable in your server settings. This is for security reasons: people with access to a restricted list of commands could set an environment variable like LD_PRELOAD to escape their jail. (If your user has access to a shell there are other ways to set environment variables.)

To allow changing an environment variable over SSH, change the AcceptEnv setting in your OpenSSH server configuration (typically /etc/ssh/sshd_config) to accept your environment variable:

AcceptEnv DEBIAN_FRONTEND

And now it magically works!