Sunday, April 29, 2012

Setting the time zone on remote SSH hosts

The tasks: I have one or more desktop/laptop machines with varying local time zones (because the persons using them are actually in different time zones, or because the one person using them travels). I also have a number of servers configured in some random time zones. (It could be the time zone where they are physically located, or the time zone of the person who installed it, or UTC for neutrality.)

Now what I would like to have happen is that if I log in using SSH from a desktop to a server, I see time on that server in my local time zone. For things like ls -l, for example. Obviously, this illusion will never be perfect. Nothing (except something very complicated) will adjust the time stamps in the syslog output, for example. But the ls -l case in particular seems to come up a lot, to check how long ago was this file modified.

This should be completely doable in principle, because you can set the TZ environment variable to any time zone you like, and it will be used for things like ls -l. But how do you get the TZ setting from here to there?

First, you have to make the remote SSH server accept the TZ environment variable. At least on Debian, this is not done by default. So make a setting like this in /etc/ssh/sshd_config:

# Allow client to pass locale environment variables
AcceptEnv LANG LC_* TZ

You also need to make the equivalent setting on the client side, either in /etc/ssh/ssh_config or in ~/.ssh/config:

SendEnv LANG LC_* TZ

Which leaves the question, how do you get your local time zone into the TZ variable to pass to the remote server? The actual time zone configuration is the file /etc/localtime, which belongs to glibc. In current Debian, this is (normally) a copy of some file under /usr/share/zoneinfo/. In the distant past, it was a symlink, which would have made things easier, but now it's a copy, so you don't know where it came from. But the name of the time zone is also written to /etc/timezone, so you can use that.

The format of the TZ environment variable can be found in the glibc documentation. If you skip past most of the text, you will see the relevant parts:

The third format looks like this:

:CHARACTERS

Each operating system interprets this format differently; in the GNU C library, CHARACTERS is the name of a file which describes the time zone.

So what you could do is set

TZ=":$(cat /etc/timezone)"

Better yet, for hopping through multiple SSH hosts in particular, make sure to preserve an already set TZ:

TZ=${TZ:-":$(cat /etc/timezone)"}

And finally, how does one hook this into ssh? The best I could think of is a shell alias:

alias ssh='TZ=${TZ:-":$(cat /etc/timezone)"} ssh'

Now this set up has a number of flaws, including:

  • Possibly only works between Linux (Debian?) hosts.

  • Only works if available time zone names match.

  • Only works when calling ssh from the shell.

But it practice it has turned out to be quite useful.

Comments? Improvements? Better ideas?

Related thoughts:

  • With this system in hand, I have resorted to setting the time zone on most servers to UTC, since I will see my local time zone automatically.

  • Important for the complete server localization illusion: some ideas on dealing with locales on remote hosts.

8 comments:

  1. If there is no security concern with TZ, maybe it could be in SendEnv and AcceptEnv by default?

    ReplyDelete
  2. Maybe you ssh to systems where you aren't root, so you can't add AcceptEnv in sshd.conf.

    Typically, the server will already accept LC_*, so send your timezone as LC__TZ. Manipulate this in a shell start-up script like ~/.bashrc, something like this:
    if LC__TZ is set; then set TZ from it; else set LC__TZ for later ssh; fi

    It's too bad that in the case where you're not restricted in the commands you can run, you're restricted in the environment you can send. After all, you can just use the remote command 'env MYVAR=value ...' to achieve the same end.

    ReplyDelete
    Replies
    1. Yeah, that is pretty stupid. Nice workaround.

      Delete
  3. If you set TZ based on /etc/timezone in your local environment scripts, then there's no need to alias SSH. Thus, SSH instances not invoked directly from a shell session (graphical sftp client?) will get it too. Is having the environment variable set for everything a problem in any way?

    ReplyDelete
    Replies
    1. The environment variable wouldn't be updated when the time zone of the machine changes. One would effectively have to restart one's desktop session to get the changes communicated everywhere. Call me traumatized. ;-) Of course, if the shell alias is really a problem, one could also divert and overwrite the ssh binaries. So there are a number of solutions available with different tradeoffs. None of them are really that nice.

      Delete
  4. /etc/timezone is not on Fedora or FreeBSD, what about using "date +%Z" instead of "cat /etc/timezone" to get the current timezone so that it's more OS/distro agnostic?

    Also, AcceptEnv set to LC_* seems to be common to Debian systems. FreeBSD doesn't have it defined at all by default, and Fedora/CentOS have the list spelled out by default, (each individual LC_ var) so jepler's comment is a damn good idea but won't work in those other environments, unfortunately.

    ReplyDelete
    Replies
    1. The time zone produced by "date +%Z" doesn't appear to be accepted as input for the TZ variable. That is consistent with the glibc documentation.

      Delete