You are currently viewing all posts tagged with linux.

Notes on Unix

As a long-time user of Unix-like systems, I prefer to do as much work in the command-line as possible. I store data in plain text whenever appropriate. I edit in vim and take advantage of the pipeline to manipulate the data with powerful tools like awk and grep.

Notes are one such instance where plain text makes sense. All of my notes – which are scratch-pads for ideas, reference material, logs, and whatnot – are kept as individual text files in the directory ~/documents/notes. The entire ~/documents directory is synced between my laptop and my work computer, and of course it is backed up with tarsnap.

When I want to read, edit or create a note, my habit is simply to open the file in vim.

1
$ vim ~/documents/notes/todo.txt

When I want to view a list of my notes, I can just ls the directory. I pass along the -t and -r flags. The first flag sorts the files by modification date, newest first. The second flag reverses the order. The result is that the most recently modified files end up at the bottom of the list, nearest the prompt. This allows me to quickly see which notes I have recently created or changed. These notes are generally active – they’re the ones I’m currently doing something with, so they’re the ones I want to see. Using ls to see which files have been most recently modified is incredibly useful, and a behaviour that I use often enough to have created an alias for it.

1
$ lt ~/documents/notes

Recently I was inspired by some extremely simple shell functions on the One Thing Well blog to make working with my notes even easier.

The first function, n(), takes the name of the note as an argument – minus the file extension – and opens it.

1
2
3
function n {
nano ~/Dropbox/Notes/$1.txt 
}

I liked the idea. It would allow me to open a note from anywhere in the filesystem without specifying the full path. After changing the editor and the path, I could open the same note as before with far fewer keystrokes.

1
$ n todo

I needed to make a few changes to the function to increase its flexibility.

First, the extension. Most of my notes have .txt extensions. Some have a .gpg extension.1 Some have no extension. I didn’t want to force the .txt extension in the function.

If I specified a file extension, that extension should be used. If I failed to specify the extension, I wanted the function to open the file as I specified it only if it existed. Otherwise, I wanted it to look for that file with a .gpg extension and open that if it was found. As a last resort, I wanted it to open the file with a .txt extension regardless of whether it existed or not. I implemented this behaviour in a separate function, buildfile(), so that I could take advantage of it wherever I wanted.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
# Take a text file and build it with an extension, if needed.
# Prefer gpg extension over txt.
buildfile() {
    # If an extension was given, use it.
    if [[ "$1" == *.* ]]; then
        echo "$1"

    # If no extension was given...
    else
        # ... try the file without any extension.
        if [ -e "$1" ]; then
            echo "$1"
        # ... try the file with a gpg extension.
        elif [ -e "$1".gpg ]; then
            echo "$1".gpg
        # ... use a txt extension.
        else
            echo "$1".txt
        fi
    fi
}

I then rewrote the original note function to take advantage of this.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# Set the note directory.
NOTEDIR=~/documents/notes

# Create or edit notes.
n() {
    # If no note was given, list the notes.
    if [ -z "$1" ]; then
        lt "$NOTEDIR"
    # If a note was given, open it.
    else
        $EDITOR $(buildfile "$NOTEDIR"/"$1")
    fi
}

Now I can edit the note ~/documents/notes/todo.txt by simply passing along the filename without an extension.

1
$ n todo

If I want to specify the extension, that will work too.

1
$ n todo.txt

If I have a note called ~/documents/notes/world-domination.gpg, I can edit that without specifying the extension.

1
$ n world-domination

If I have a note called ~/documents/notes/readme, I can edit that and the function will respect the lack of an extension.

1
$ n readme

Finally, if I want to create a note, I can just specify the name of the note and a file with that name will be created with a .txt extension.

The other change I made was to make the function print a reverse-chronologically sorted list of my notes if it was called with no arguments. This allows me to view my notes by typing a single character.

1
$ n

The original blog post also included a function ns() for searching notes by their title.

1
2
3
function ns {
ls -c ~/Dropbox/Notes | grep $1
}

I thought this was a good idea, but I considered the behaviour to be finding a note rather than searching a note. I renamed the function to reflect its behaviour, took advantage of my ls alias, and made the search case-insensitive. I also modified it so that if no argument was given, it would simply print an ordered list of the notes, just like n().

1
2
3
4
5
6
7
8
# Find a note by title.
nf() {
    if [ -z "$1" ]; then
        lt "$NOTEDIR"
    else
        lt "$NOTEDIR" | grep -i $1
    fi
}

I thought it would be nice to also have a quick way to search within notes. That was accomplished with ns(), the simplest function of the trio.

1
2
# Search within notes.
ns() { cd $NOTEDIR; grep -rin $1; cd "$OLDPWD"; }

I chose to change into the note directory before searching so that the results do not have the full path prefixed to the filename. Thanks to the first function I don’t need to specify the full path of the note to open it, and this makes the output easier to read.

1
2
3
$ ns "take over"
world-domination.txt:1:This is my plan to take over the world.
$ n world-domination

To finish it up, I added completion to zsh so that I can tab-complete filenames when using n.

1
2
# Set autocompletion for notes.
compctl -W $NOTEDIR -f n

If you’re interested in seeing some of the other way that I personalize my working environment, all of my dotfiles are on GitHub. The note functions are in shellrc, which is my shell-agnostic configuration file that I source from both zshrc and bashrc2.

Notes

  1. I use vim with the gnupg.vim plugin for seamless editing of PGP-encrypted files.
  2. I prefer zsh, but I do still find myself in bash on some machines. I find it prudent to maintain configurations for both shells. There's a lot of cross-over between them and I like to stick to the DRY principle.

Strategies for Working Offline

Earlier this year I went through a period where I had only intermittent internet access. Constant, high-speed internet access is so common these days that I had forgotten what it was like to work on a computer that was offline more often than it was online. It provided an opportunity to reevaluate some aspects of how I work with data.

E-Mail

For a period of a couple years I accessed my mail through the Gmail web interface exclusively. About a year ago, I moved back to using a local mail client. That turned out to be a lucky move for this experience. I found that I had three requirements for my mail:

  1. I needed to be able to read mail when offline (all of it, attachments included),
  2. compose mail when offline,
  3. and queue messages up to be automatically sent out the next time the mail client found that it had internet access.

Those requirements are fairly basic. Most mail clients will handle them without issue. I’ve always preferred to connect to my mail server via IMAP rather than POP3. Most mail clients offer to cache messages retrieved over IMAP. They do this for performance reasons rather than to provide the ability to read mail offline, but the result is the same. For mail clients like Mutt that don’t have built-in caching, a tool like OfflineImap is great.

Wikipedia

Wikipedia is too valuable a resource to not have offline access to. There are a number of options for getting a local copy. I found Kiwix to be a simple and effective solution. It downloads a compressed copy of the Wikipedia database and provides a web-browser-like interface to read it. The English Wikipedia is just shy of 10 gigabytes (other languages are of course available). That includes all articles, but no pictures, history or talk pages. Obviously this is something you want to download before you’re depending on coffee shops and libraries for intermittent internet access. After Kiwix has downloaded the database, it needs to be indexed for proper searching. Indexing is a resource-intensive process that will take a long time, but it’s worth it. When it’s done, you’ll have a not-insignificant chunk of our species’ combined knowledge sitting on your hard-drive. (It’s the next best thing to the Guide, really.)

Arch Wiki

For folks like myself who run Arch Linux, the Arch Wiki is an indispensable resource. For people who use other distributions, it’s less important, but still holds value. I think it’s the single best repository of Linux information out there.

For us Arch users, getting a local copy is simple. The arch-wiki-docs package provides a flat HTML copy of the wiki. Even better is the arch-wiki-lite package which provides a console interface for searching and viewing the wiki.

Users of other distributions could, at a minimum, extract the contents of the arch-wiki-docs package and grep through it.

Tunneling

Open wireless networks are dirty places. I’m never comfortable using them without tunneling my traffic. An anonymizing proxy like Tor is overkill for a situation like this. A full-fledged VPN is the best option, but I’ve found sshuttle to make an excellent poor-man’s VPN. It builds a tunnel over SSH, while addressing some of the shortcomings of vanilla SSH port forwarding. All traffic is forced through the tunnel, including DNS queries. If you have a VPS, a shared hosting account, or simply a machine sitting at home, sshuttle makes it dead simple to protect your traffic when on unfamiliar networks.

YouTube

YouTube is a great source of both education and entertainment. If you are only going to be online for a couple hours a week, you probably don’t want to waste those few hours streaming videos. There’s a number of browser plugins that allow you to download YouTube videos, but my favorite solution is a Python program called youtube-dl. (It’s unfortunately named, as it also supports downloading videos from other sites like Vimeo and blip.tv.) You pass it the URL of the YouTube video page and it grabs the highest-quality version available. It has a number of powerful options, but for me the killer feature is the ability to download whole playlists. Say you want to grab every episode of the great web-series Sync. Just pass the URL of the playlist to youtube-dl.

$ youtube-dl -t https://www.youtube.com/playlist?list=PL168F329FADED6741

That’s it. It goes out and grabs every video. (The -t flag tells it to use the video’s title in the file name.) If you come back a few weeks later and think there might have been a couple new videos added to the playlist, you can just run the same command again but with the -c flag, which tells it to resume. It will see that it already downloaded part of the playlist and will only get videos that it doesn’t yet have.

Even now that I’m back to having constant internet access, I still find myself using youtube-dl on a regular basis. If I find a video that I want to watch at a later time, I download it. That way I don’t have to worry about buffering, or the video disappearing due to DMCA take-down requests.

Backups

I keep backitup.sh in my network profile so that my online backups attempt to execute whenever I get online. If you’re only online once or twice a week, you probably have more important things to do than remembering to manually trigger your online backups. It’s nice to have that automated.

Back It Up: A Solution for Laptop Backups

A laptop presents some problems for reliably backing up data. Unlike a server, the laptop may not always be turned on. When it is on, it may not be connected to the backup medium. If you’re doing online backups, the laptop may be offline. If you’re backing up to an external drive, the drive may not be plugged in. To address these issues I wrote a shell script called backitup.sh.

The Problem

Let’s say you want to backup a laptop to an external USB drive once per day with cryptshot.

You could add a cron entry to call cryptshot.sh at a certain time every day. What if the laptop isn’t turned on? What if the drive isn’t connected? In either case the backup will not be completed. The machine will then wait a full 24 hours before even attempting the backup again. This could easily result in weeks passing without a successful backup.

If you’re using anacron, or one of its derivatives, things get slightly better. Instead of specifying a time to call cryptshot.sh, you set the cron interval to @daily. If the machine is turned off at whatever time anacron is setup to execute @daily scripts, all of the commands will simply be executed the next time the machine boots. But that still doesn’t solve the problem of the drive not being plugged in.

The Solution

backitup.sh attempts to perform a backup if a certain amount of time has passed. It monitors for a report of successful completion of the backup. Once configured, you no longer call the backup program directly. Instead, you call backitup.sh. It then decides whether or not to actually execute the backup.

How it works

The script is configured with the backup program that should be executed, the period for which you want to complete backups, and the location of a file that holds the timestamp of the last successful backup. It can be configured either by modifying the variables at the top of the script, or by passing in command-line arguments.

$ backitup.sh -h
Usage: backitup.sh [OPTION...]
Note that any command line arguments overwrite variables defined in the source.

Options:
    -p      the period for which backups should attempt to be executed
            (integer seconds or 'DAILY', 'WEEKLY' or 'MONTHLY')
    -b      the backup command to execute; note that this should be quoted if it contains a space
    -l      the location of the file that holds the timestamp of the last successful backup.
    -n      the command to be executed if the above file does not exist

When the script executes, it reads the timestamp contained in the last-run file. This is then compared to the user-specified period. If the difference between the timestamp and the current time is greater than the period, backitup.sh calls the backup program. If the difference between the stored timestamp and the current time is less than the requested period, the script simply exits without running the backup program.

After the backup program completes, the script looks at the returned exit code. If the exit code is 0, the backup was completed successfully, and the timestamp in the last-run file is replaced with the current time. If the backup program returns a non-zero exit code, no changes are made to the last-run file. In this case, the result is that the next time backitup.sh is called it will once again attempt to execute the backup program.

The period can either be specified in seconds or with the strings DAILY, WEEKLY or MONTHLY. The behaviour of DAILY differs from 86400 (24-hours in seconds). With the latter configuration, the backup program will only attempt to execute once per 24-hour period. If DAILY is specified, the backup may be completed successfully at, for example, 23:30 one day and again at 00:15 the following day.

Use

You still want to backup a laptop to an external USB drive once per day with cryptshot. Rather than calling cryptshot.sh, you call backitup.sh.

Tell the script that you wish to complete daily backups, and then use cron to call the script more frequently than the desired backup period. For my local backups, I call backitup.sh every hour.

@hourly backitup.sh -l ~/.cryptshot-daily -b "cryptshot.sh daily"

The default period of backitup.sh is DAILY, so in this case I don’t have to provide a period of my own. But I also do weekly and monthly backups, so I need two more entries to execute cryptshot with those periods.

@hourly backitup.sh -l ~/.cryptshot-monthly -b "cryptshot.sh monthly" -p MONTHLY
@hourly backitup.sh -l ~/.cryptshot-weekly -b "cryptshot.sh weekly" -p WEEKLY

All three of these entries are executed hourly, which means that at the top of every hour, my laptop attempts to back itself up. As long as the USB drive is plugged in during one of those hours, the backup will complete. If cryptshot is executed, but fails, another attempt will be made the next hour. Daily backups will only be successfully completed, at most, once per day; weekly backups, once per week; and monthly backups, once per month. This setup works well for me, but if you want a higher assurance that your daily backups will be completed every day you could change the cron interval to */5 * * * *, which will result in cron executing backitup.sh every 5 minutes.

What if you want to perform daily online backups with Tarsnapper?

@hourly backitup.sh -l ~/.tarsnapper-lastrun -b tarsnapper.py

At the top of every hour your laptop will attempt to run Tarsnap via Tarsnapper. If the laptop is offline, it will try again the following hour. If Tarsnap begins but you go offline before it can complete, the backup will be resumed the following hour.

The script can of course be called with something other than cron. Put it in your ~/.profile and have you backups attempt to execute every time you login. Add it to your network manager and have your online backups attempt to execute every time you get online. If you’re using something like udev, have your local backups attempt to execute every time your USB drive is plugged in.

The Special Case

The final configuration option of backitup.sh represents a special case. If the script runs and it can’t find the specified file, the default behaviour is to assume that this is the first time it has ever run: it creates the file and executes the backup. That is what most users will want, but this behaviour can be changed.

When I first wrote backitup.sh it was to help manage backups of my Dropbox folder. Dropbox doesn’t provide support client-side encryption, which means users need to handle encryption themselves. The most common way to do this is to create an encfs file-system or two and place those within the Dropbox directory. That’s the way I use Dropbox.

I wanted to backup all the data stored in Dropbox with Tarsnap. Unlike Dropbox, Tarsnap does do client-side encryption, so when I backup my Dropbox folder, I don’t want to actually backup the encrypted contents of the folder – I want to backup the decrypted contents. That allows me to take better advantage of Tarsnap’s deduplication and it makes restoring backups much simpler. Rather than comparing inodes and restoring a file using an encrypted filename like 6,8xHZgiIGN0vbDTBGw6w3lf/1nvj1,SSuiYY0qoYh-of5YX8 I can just restore documents/todo.txt.

If my encfs filesystem mount point is ~/documents, I can configure Tarsnapper to create an archive of that directory, but if for some reason the filesystem is not mounted when Tarsnapper is called, I would be making a backup of an empty directory. That’s a waste of time. The solution is to tell backitup.sh to put the last-run file inside the encfs filesystem. If it can’t find the file, that means that the filesystem isn’t mounted. If that’s the case, I tell it to call the script I use to automatically mount the encfs filesystem (which, the way I have it setup, requires no interaction from me).

@hourly backitup.sh -l ~/documents/.lastrun -b tarsnapper.py -n ~/bin/encfs_automount.sh

Problem Solved

backitup.sh solves all of my backup scheduling problems. I only call backup programs directly if I want to make an on-demand backup. All of my automated backups go through backitup.sh. If you’re interested in the script, you can download it directly from GitHub. You can clone my entire backups repository if you’re also interested in the other scripts I’ve written to manage different aspects of backing up data.

Hey yo but wait, back it up, hup, easy back it up

Cryptshot: Automated, Encrypted Backups with rsnapshot

Earlier this year I switched from Duplicity to rsnapshot for my local backups. Duplicity uses a full + incremental backup schema: the first time a backup is executed, all files are copied to the backup medium. Successive backups copy only the deltas of changed objects. Over time this results in a chain of deltas that need to be replayed when restoring from a backup. If a single delta is somehow corrupted, the whole chain is broke. To minimize the chances of this happening, the common practice is to complete a new full backup every so often – I usually do a full backup every 3 or 4 weeks. Completing a full backup takes time when you’re backing up hundreds of gigabytes, even over USB 3.0. It also takes up disk space. I keep around two full backups when using Duplicity, which means I’m using a little over twice as much space on the backup medium as what I’m backing up.

The backup schema that rsnapshot uses is different. The first time it runs, it completes a full backup. Each time after that, it completes what could be considered a “full” backup, but unchanged files are not copied over. Instead, rsnapshot simply hard links to the previously copied file. If you modify very large files regularly, this model may be inefficient, but for me – and I think for most users – it’s great. Backups are speedy, disk space usage on the backup medium isn’t too much more than the data being backed up, and I have multiple full backups that I can restore from.

The great strength of Duplicity – and the great weakness of rsnapshot – is encryption. Duplicity uses GnuPG to encrypt backups, which makes it one of the few solutions appropriate for remote backups. In contrast, rsnapshot does no encryption. That makes it completely inappropriate for remote backups, but the shortcoming can be worked around when backing up locally.

My local backups are done to an external, USB hard drive. Encrypting the drive is simple with LUKS and dm-crypt. For example, to encrypt /dev/sdb:

$ cryptsetup --cipher aes-xts-plain --key-size 512 --verify-passphrase luksFormat /dev/sdb

The device can then be opened, formatted, and mounted.

$ cryptsetup luksOpen /dev/sdb backup_drive
$ mkfs.ext4 -L backup /dev/mapper/backup_drive
$ mount /dev/mapper/backup_drive /mnt/backup/

At this point, the drive will be encrypted with a passphrase. To make it easier to mount programatically, I also add a key file full of some random data generated from /dev/urandom.

$ dd if=/dev/urandom of=/root/supersecretkey bs=1024 count=8
$ chmod 0400 /root/supersecretkey
$ cryptsetup luksAddKey /dev/sdb /root/supersecretkey

There are still a few considerations to address before backups to this encrypted drive can be completed automatically with no user interaction. Since the target is a USB drive and the source is a laptop, there’s a good chance that the drive won’t be plugged in when the scheduler kicks in the backup program. If it is plugged in, the drive needs to be decrypted before calling rsnapshot to do its thing. I wrote a wrapper script called cryptshot to address these issues.

Cryptshot is configured with the UUID of the target drive and the key file used to decrypt the drive. When it is executed, the first thing it does is look to see if the UUID exists. If it does, that means the drive is plugged in and accessible. The script then decrypts the drive with the specified key file and mounts it. Finally, rsnapshot is called to execute the backup as usual. Any argument passed to cryptshot is passed along to rsnapshot. What that means is that cryptshot becomes a drop-in replacement for encrypted, rsnapshot backups. Where I previously called rsnapshot daily, I now call cryptshot daily. Everything after that point just works, with no interaction needed from me.

If you’re interested in cryptshot, you can download it directly from GitHub. The script could easily be modified to execute a backup program other than rsnapshot. You can clone my entire backups repository if you’re also interested in the other scripts I’ve written to manage different aspects of backing up data.

Tarsnapper: Managing Tarsnap Backups

Tarsnap bills itself as “online backups for the truly paranoid”. I began using the service last January. It fast became my preferred way to backup to the cloud. It stores data on Amazon S3 and costs $0.30 per GB per month for storage and $0.30 per GB for bandwidth. Those prices are higher than just using Amazon S3 directly, but Tarsnap implements some impressive data de-duplication and compression that results in the service costing very little. For example, I currently have 67 different archives stored in Tarsnap from my laptop. They total 46GB in size. De-duplicated that comes out to 1.9GB. After compression, I only pay to store 1.4GB. Peanuts.

Of course, the primary requirement for any online backup service is encryption. Tarsnap delivers. And, most importantly, the Tarsnap client is open-source, so the claims of encryption can actually be verified by the user. The majority of for-profit, online backup services out there fail on this critical point.

So Tarsnap is amazing and you should use it. The client follows the Unix philosophy: “do one thing and do it well”. It’s basically like tar. It can create archives, read the contents of an archive, extract archives, and delete archives. For someone coming from an application like Duplicity, the disadvantage to the Tarsnap client is that it doesn’t include any way to automatically manage backups. You can’t tell Tarsnap how many copies of a backup you wish to keep, or how long backups should be allowed to age before deletion.

Thanks to the de-duplication and compression, there’s not a great economic incentive to not keep old backups around. It likely won’t cost you that much extra. But I like to keep things clean and minimal. If I haven’t used an online backup in 4 weeks, I generally consider it stale and have no further use for it.

To manage my Tarsnap backups, I wrote a Python script called Tarsnapper. The primary intent was to create a script that would automatically delete old archives. It does this by accepting a maximum age from the user. Whenever Tarsnapper runs, it gets a list of all Tarsnap archives. The timestamp is parsed out from the list and any archive that has a timestamp greater than the maximum allowed age is deleted. This is seamless, and means I never need to manually intervene to clean my archives.

Tarsnapper also provides some help for creating Tarsnap archives. It allows the user to define any number of named archives and the directories that those archives should contain. On my laptop I have four different directories that I backup with Tarsnap, three of them in one archive and the last in another archive. Tarsnapper knows about this, so whenever I want to backup to Tarsnap I just call a single command.

Tarsnapper also can automatically add a suffix to the end of each archive name. This makes it easier to know which archive is which when you are looking at a list. By default, the suffix is the current date and time.

Configuring Tarsnapper can be done either directly by changing the variables at the top of the script, or by creating a configuration file named tarsnapper.conf in your home directory. The config file on my laptop looks like this:

1
2
3
4
5
6
[Settings]
tarsnap: /usr/bin/tarsnap

[Archives]
nous-cloud: /home/pigmonkey/work /home/pigmonkey/documents /home/pigmonkey/vault/
nous-config: /home/pigmonkey/.config

There is also support for command-line arguments to specify the location of the configuration file to use, to delete old archives and exit without creating new archives, and to execute only a single named-archive rather than all of those that you may have defined.

$ tarsnapper.py --help
usage: tarsnapper.py [-h] [-c CONFIG] [-a ARCHIVE] [-r]

A Python script to manage Tarsnap archives.

optional arguments:
  -h, --help            show this help message and exit
  -c CONFIG, --config CONFIG
                        Specify the configuration file to use.
  -a ARCHIVE, --archive ARCHIVE
                        Specify a named archive to execute.
  -r, --remove          Remove archives old archives and exit.

It makes using a great service very simple. My backups can all be executed simply by a single call to Tarsnapper. Stale archives are deleted, saving me precious picodollars. I use this system on my laptop, as well as multiple servers. If you’re interested in it, Tarsnapper can be downloaded directly from GitHub. You can clone my entire backups repository if you’re also interested in the other scripts I’ve written to manage different aspects of backing up data.

An Ubuntu VPS for Django

Three years ago I wrote a guide to building a VPS web server for serving sites in a PHP environment. That setup served me well for some time, but most of the sites I run now – including this one – are now written in Python. Earlier this year I built another web server to reflect this. It’s similar to before; I still use Ubuntu and I still like to serve pages with nginx. But PHP has been replaced with Python, and many of the packages used to build the environment have changed as a result. As with the last time, I decided to compile my notes into a guide, both for my own reference and in case anyone else would like to duplicate it. So far, the server has proven to be fast and efficient. It serves Python using uWSGI, uses a PostgreSQL database, and includes a simple mail server provided by Postfix. I think it’s a good setup for serving simple Django-based websites.

Basic Setup

As with last time, I recommend following Slicehost’s basic server setup article. It discusses user administration, SSH security, and firewalls. I no longer use Slicehost as my VPS provider, but I find that Slicehost’s articles provide an excellent base regardless of the host.

Packages

Packages should upgraded immediately to address any known security vulnerabilities.

1
2
$ sudo apt-get update
$ sudo apt-get upgrade

After the repositories have been updated, I install some essential packages.

1
$ sudo apt-get install build-essential screen dnsutils

Build-essential includes necessary tools to compile programs. I am incapable of using a computer that does not have screen on it, so that gets installed too. The third package, dnsutils, is optional, but includes dig which is useful for troubleshooting DNS issues.

DenyHosts

Slicehost’s setup article recommends turning off password authentication in SSH, forcing users to login with keys only. I use keys whenever I can, but I appreciate the option of being able to login to my server from any computer, when I may or may not have my SSH key with me. So I leave password authentication enabled. This presents the possibility of brute-force attacks. Enter DenyHosts. DenyHosts, which I have discussed previously attempts to protect against SSH attacks by banning hosts after a certain number of failed login attempts. When password authentication is enabled, running DenyHosts is a smart move.

1
2
$ sudo apt-get install denyhosts
$ sudo vim /etc/denyhosts.conf

Personalize the Environment

I use update-alternatives to set my default editor to vim.

1
$ sudo update-alternatives --config editor

All of my personal configuration files are kept in a github repository. I’ll check out that repository into ~/src and install the files.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
$ mkdir ~/src
$ cd ~/src
$ sudo apt-get install git-core
$ git clone git://github.com/pigmonkey/dotfiles.git
$ ln -s ~/src/dotfiles/bash_profile ~/.bash_profile
$ ln -s ~/src/dotfiles/bashrc ~/.bashrc
$ ln -s ~/src/dotfiles/bash_aliases ~/.bash_aliases
$ ln -s ~/src/dotfiles/bash_colors ~/.bash_colors
$ ln -s ~/src/dotfiles/vimrc ~/.vimrc
$ ln -s ~/src/dotfiles/vim ~/.vim
$ ln -s ~/src/dotfiles/screenrc ~/.screenrc
$ source ~/.bash_profile

Time

The next step is to set the server’s timezone and sync the clock with NTP.

1
2
$ sudo dpkg-reconfigure tzdata
$ sudo apt-get install ntp

Rootkits

There’s no reason not to run both chkrootkit and rkhunter to check for rootkits and related vulnerabilities.

chrkrootkit

Slicehost has an excellent article for setting up and using chkrootkit.

Later on I’ll be installing some Python development packages. One of them creates a directory called /usr/lib/pymodules/python2.6/.path, which sets off a warning in chkrootkit. Part of chkrootkit’s desgin philosophy is to not include any whitelists: if chkrootkit finds something that it doesn’t like, you’re going to hear about it. I have cron run chkrootkit nightly and I want to receive any warnings, but I don’t want to receive the same false positive every morning in my inbox.

The solution is to create a file that contains chkrootkit’s warning. I call that file whitelist and store it in the same directory as chkrootkit. When chkrootkit is run, any output is redirected to a file. That file is compared to the whitelist using diff and the output of that – if any – is then read. At the end, the file containing chkrootkit’s output is deleted so that the working directory is ready for the next run. The effect is that I only hear warnings from chkrootkit that I have not explicit whitelisted. All of this can be accomplished in a single crontab entry.

1
0 3 * * * (cd /home/demo/src/chkrootkit-0.49; ./chkrootkit -q > message 2>&1; diff -w whitelist message; rm -f message)

rkhunter

I’m sure it doesn’t surprise you that I’m going to recommend reading Slicehost’s article on rkhunter.

Unlike chkrootkit, rkhunter does allow for whitelists. On a clean Ubuntu 10.04 system, I find that I need to whitelist a few items in the config.

1
2
3
4
5
6
7
$ sudo vim /etc/rkhunter.conf

SCRIPTWHITELIST="/usr/sbin/adduser /usr/bin/ldd /bin/which"
ALLOWHIDDENDIR="/dev/.udev /dev/.initramfs"
APP_WHITELIST="openssl:0.9.8k gpg:1.4.10"

$ sudo /usr/local/bin/rkhunter --propupd

The script that my cronjob runs is slightly different from the one demonstrated in the Slicehost article. Their script executes a few commands, groups the output together, and sends it to mail to email the system administrator. This results in daily emails, regardless of whether rkhunter finds any warnings or not. My script is simpler and does not result in so many messages.

1
2
3
4
#!/bin/sh
/usr/local/bin/rkhunter --versioncheck -q
/usr/local/bin/rkhunter --update -q
/usr/local/bin/rkhunter --cronjob --report-warnings-only

The version check and update commands both have the -q switch, which disables any output – I don’t care to know whether rkhunter updated itself or not. The final line actually executes the scan. Notice that there’s no reference to mail. This script does not send any messages. The reason for that is that rkhunter itself provides the mail functionality. Inside of /etc/rkhunter.conf there is a MAIL-ON-WARNING variable. As long as the machine has an smtp server on it (which I’ll get to later in this guide), simply filling in this variable will result in any warnings being emailed to the system administrator.

Web Server

With the basics complete, it’s time to start serving something! In my previous article I covered serving a PHP-based Wordpress site via FastCGI and nginx. This time around the stack will be different: nginx, uWSGI, Python, and Django.

A few basic packages will help flesh out the server’s Python development environment:

1
$ sudo apt-get install python-psycopg2 python-setuptools python2.6-dev psmisc python-imaging locate python-dateutil libxml2-dev python-software-properties

uWSGI

Installing uWSGI is a simple matter of compiling it and moving the resulting binary into one of your system’s bin directories.

1
2
3
4
5
6
$ cd ~/src/
$ wget http://projects.unbit.it/downloads/uwsgi-0.9.8.tar.gz
$ tar xvzf ~/uwsgi-0.9.8.tar.gz
$ cd uwsgi-0.9.8/
$ make -f Makefile.Py26
$ sudo cp uwsgi /usr/local/sbin

nginx

The nginx package in Ubuntu’s official repositories is always notoriously outdated. It used to be you had to compile the server from source, but there is now an Ubuntu PPA for the latest stable versions. As described by the nginx wiki, all that is needed is to add the PPA to your sources.list and apt-get away!

1
2
3
$ sudo add-apt-repository ppa:nginx/stable
$ sudo apt-get update 
$ sudo apt-get install nginx

Python and Django

If you do Python development and haven’t heard of virtualenv, it is well worth reading up on. It allows the user to create an isolated, virtual Python environment for each project. This helps immensely when developing (or serving) multiple projects on a single machine. Needless to say, I consider it to be a required package.

Install

I’ll be installing virtualenv and virtualenvwrapper (a set of scripts to facilitate working with virtual environments). I also prefer pip over easy_install for managing Python packages.

1
2
3
$ sudo easy_install -U pip 
$ sudo pip install virtualenv
$ sudo pip install virtualenvwrapper

Setup a virtual environment

Virtual environments can be stored wherever you fancy. For now, I keep them in a hidden folder in my home directory. For these examples, I’ll setup an environment called myproject.

1
2
3
$ mkdir ~/.virtualenvs
$ cd ~/.virtualenvs
$ virtualenv --no-site-packages --distribute myproject

Notice the --no-site-packages switch. That tells virtualenv to create this environment without any of the Python packages already installed, creating a completely fresh, clean environment. The --distribute switch causes the new virtual environment to be setup with distribute, a replacement for the old and rather broken setuptools.

All that’s needed to get virtualenvwrapper up and running is to add two lines to your .bashrc and re-source the file.

1
2
3
4
5
6
$ vim ~/.bashrc

export WORKON_HOME=$HOME/.virtualenvs
source /usr/local/bin/virtualenvwrapper.sh

$ . ~/.bashrc

We can now use commands like workon to ease the process of activating a certain environment.

I’ll go ahead and install yolk in the environment to help manage packages.

1
2
3
$ workon myproject
$ pip install yolk
$ yolk -l

The last command will cause yolk to list all packages installed in the environment. Try deactivating the environment and then running yolk again.

1
2
3
$ deactivate
$ yolk -l
yolk: command not found

‘yolk’ wasn’t found, because it was only installed within the virtual environment. Neat!

Install Django

Finally, it’s time to install Django! The process is simple enough.

1
2
$ workon myproject
$ pip install django

And that’s it!

The Python Imaging Library is likely to be needed for any Django project. I installed it in the beginning of this section, but because I used the --no-site-packages when creating my virtual environment, it is not available for use within the project. To fix that, I’ll just link the package in. I also previously installed psyopg2, which Python will need to communicate with my PostgreSQL database, so I’ll link that in as well. psyopg2 depends on mx, which was also previously installed but still must be made available in the environment.

1
2
3
4
5
$ cdsitepackages
$ ln -s /usr/lib/python2.6/dist-packages/PIL
$ ln -s /usr/lib/python2.6/dist-packages/PIL.pth
$ ln -s /usr/lib/python2.6/dist-packages/psycopg2
$ ln -s /usr/lib/python2.6/dist-packages/mx

That wasn’t too painful!

Create a Django project

While I’m still in the virtual environment, I’ll go ahead and create a new Django project. The project will have the same name as the environment: myproject. For this tutorial, I’ll stick with the precedence set by the Slicehost tutorials and use demo as the name of both my user and group on the server.

I like to keep my sites in the /srv/ directory. I structure them so that the code that runs the site, any public files, logs, and backups are all stored in separate sub-directories.

1
2
3
4
5
6
7
8
9
$ cd /srv/
$ sudo mkdir -p myproject.com/{code,public,logs,backup}
$ sudo mkdir -p myproject.com/public/{media,static}
$ sudo chown -R demo:demo myproject.com
$ cd myproject.com
$ sudo chown -R :www-data logs public
$ sudo chmod -R g+w logs public
$ cd code/
$ django-admin.py startproject myproject

Notice that the logs and public directories were chowned to the www-data group. That is the name of the user and group that nginx will run as. The web server will need permissions to write to those locations.

Save Requirements

With the environment setup and all the necessary packages installed, now is a good time to tell pip to freeze all the packages and their versions. I keep this file in a deploy folder in my project.

1
2
$ mkdir /srv/myproject.com/code/deploy
$ pip freeze > /srv/myproject.com/code/deploy/requirements.txt

Now, if I needed to recreate the virtual environment somewhere else, I could just tell pip to install all the packages from that file.

1
$ pip install -r /srv/myproject.com/code/deploy/requirements.txt

Configure uWSGI

Now that I have something to serve, I’ll configure uWSGI to serve it. The first step is to create a configuration file for the project. I call mine wsgi.py and store it in /srv/myproject.com/code/myproject/. It appends the current directory to the Python path, specifies the Django settings file for the project, and registers the WSGI handler.

1
2
3
4
5
6
7
8
9
import sys
import os

sys.path.append(os.path.abspath(os.path.dirname(__file__)))
os.environ['DJANGO_SETTINGS_MODULE'] = 'myproject.settings'

import django.core.handlers.wsgi

application = django.core.handlers.wsgi.WSGIHandler()

With that done, the next step is to decide how uWSGI should be run. I’m going to use Ubuntu’s upstart to supervise the service. I keep the upstart script in my project’s deploy/ directory.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
$ vim /srv/myproject.com/code/deploy/uwsgi.conf

description "uWSGI server for My Project"

start on runlevel [2345]
stop on runlevel [!2345]

respawn
exec /usr/local/sbin/uwsgi \
--home /home/demo/.virtualenvs/myproject/ \
--socket /var/run/myproject.com.sock \
--chmod-socket \
--pythonpath /srv/myproject.com/code/ \
--module myproject.wsgi \
--process 2 \
--harakiri 30 \
--master \
--logto /srv/myproject.com/logs/uwsgi.log

Sadly, upstart doesn’t seem to recognize links. Rather than linking the config file into /etc/init/, I have to copy it.

1
$ sudo cp /srv/myproject.com/code/deploy/uwsgi.conf /etc/init/uwsgi-myproject.conf

Configure nginx

Nginx’s configuration is pretty straight-forward. If you’ve never configured the server before, Slicehost’s articles can set you down the right path. My own nginx config looks something like this:

user www-data www-data;
worker_processes 4;
pid /var/run/nginx.pid;

events {
        worker_connections 768;
        use epoll;
}

http {

    ##
    # Basic Settings
    ##

    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout 30;
    types_hash_max_size 2048;
    # server_tokens off;

    # server_names_hash_bucket_size 64;
    # server_name_in_redirect off;

    include /etc/nginx/mime.types;
    default_type application/octet-stream;

    ##
    # Logging Settings
    ##

    access_log /var/log/nginx/access.log;
    error_log /var/log/nginx/error.log;

    ##
    # Gzip Settings
    ##

    gzip on;
    gzip_disable "msie6";
    gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript;
    gzip_proxied any;
    gzip_comp_level 2;

    # gzip_vary on;
    # gzip_buffers 16 8k;
    # gzip_http_version 1.1;

    ##
    # Virtual Host Configs
    ##

    include /etc/nginx/conf.d/*.conf;
    include /etc/nginx/sites-enabled/*;
}

I keep the virtual host config for my project inside the project’s code/deploy/ directory. A basic virtual host for a Django project would looks like this:

server {
    listen 80;
    server_name www.myproject.com;
    rewrite ^/(.*) http://myproject.com/$1 permanent;
}

server {
    listen 80;
    server_name myproject.com;

    access_log /srv/myproject.com/logs/access.log;
    error_log /srv/myproject.com/logs/error.log;

    location /media {
        root /srv/myproject.com/public/;
    }

    location /static {
        root /srv/myproject.com/public/;
    }

    location / {
        uwsgi_pass unix:///var/run/myproject.com.sock;
        include uwsgi_params;
    }
}

To install and enable the virtual host, I’ll link the configuration file first to the nginx sites-available directory, and then link that link to the sites-enabled directory.

1
2
$ sudo ln -s /srv/myproject.com/code/deploy/nginx.conf /etc/nginx/sites-available/myproject.com
$ sudo ln -s /etc/nginx/sites-available/myproject.com /etc/nginx/sites-enabled/myproject.com

SSL

If you need to encrypt communications, Linode has a tutorial on using both self-signed certificates and commercial certificates with nginx.

Fire it Up

Nginx should be set to talk to uWSGI, which should be set to talk to the Django project. Time for a test run!

1
2
$ sudo start uwsgi-myproject
$ sudo /etc/init.d/nginx start

memcached

Django has a very good built-in cache framework. I like to take advantage of it with a memory-based backend: namely, memcached. It’s fast, efficient, and easy to setup.

All that’s needed is to install memcached on the server, followed by the Python API python-memcached.

1
2
3
$ sudo apt-get install memcached
$ workon myproject
$ pip install python-memcached

The default configuration file in Ubuntu lives at /etc/memcached.conf. I usually stick with the defaults, but sometimes end up changing the port that memchached runs on or the amount of memory it is allowed to use.

logrotate

With the web server more-or-less complete, I like to setup logrotate to manage the logs in my project’s directory. Once again, Slicehost has an excellent introduction to logrotate and an example config for virtual hosts.

I maintain a configuration file for each of the domains being served by the machine. The file for a domain lives in – you guessed it – the associated project’s deploy/ folder. Each contains two entries: one for the nginx virtual host and one for the uWSGI instance. The reason for this is that each config block needs a postrotate section to restart the associated server after the logs have been rotated. I don’t want nginx to be restarted everytime a uWSGI log is rotated, and I don’t want uWSGI restarted everytime an nginx log is rotated.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ vim /srv/myproject.com/code/deploy/logrotate

/srv/myproject.com/logs/access.log /srv/myproject.com/logs/error.log {
    rotate 14
    daily
    compress
    delaycompress
    sharedscripts
    postrotate
        [ ! -f /var/run/nginx.pid ] || kill -USR1 `cat /var/run/nginx.pid`
    endscript
}

/srv/myproject.com/logs/uwsgi.log {
    rotate 14
    daily
    compress
    delaycompress
    postrotate
       restart --quiet uwsgi-myproject
    endscript
}

This file is linked in to the /etc/logrotate.d/ directory. Logrotate will automatically include any file in that directory inside its configuration.

1
$ sudo ln -s /srv/myproject.com/code/deploy/logrotate /etc/logrotate.d/myproject

Database Server

A web server isn’t much use without a database these days. I use PostgreSQL.

Install

1
$ sudo apt-get install postgresql

Configure

PostgreSQL has some unique terminology and ways of doing things. When I first set it up for the first time, having coming from a MySQL background, not everything was completely straightforward. As usual, Slicehost has a number of articles that will provide a foundation.

In the /etc/postgresql/8.4/main/postgresql.conf file, I uncomment the following two lines:

track_counts = on
autovacuum = on

Then restart the database server.

1
$ sudo /etc/init.d/postgresql-8.4 restart

After that I’ll change the password for the postgres user and the postgres database role.

1
2
3
4
$ sudo passwd postgres
$ sudo -u postgres psql
\password postgres
\q

To allow local socket connections to the database using passwords, I open up /etc/postgresql/8.4/main/pg_hba.conf and find the following line:

local   all         all                               ident

Which I then change to:

local   all         all                               md5

After which another restart is in order.

1
$ sudo /etc/init.d/postgresql-8.4 restart

Create a database

The next step is to create a user (or role, in PostgreSQL’s parlance) and database for the project. I use the same name for both.

1
2
$ sudo -u postgres createuser -PE myproject
$ sudo -u postgres createdb -O myproject myproject

After that, I should be able to connect.

1
$ psql -U myproject

Import the Database

If I’m restoring a previous database from a backup, now would be the time to import the backup.

1
$ psql -U myproject < myproject.postgresql

And now Django should be able to connect!

The basic server is setup and secure. Django, uWSGI, nginx and PostgreSQL are all running and getting along swimmingly. At this point, many people would be done, but I also like to have a minimal mail server.

Mail Server

Most of my domains use Google Apps, so I don’t need a full-blown mail server. I do want programs and scripts to be able to send mail, and I prefer not to do so through an external SMTP server – I’d rather just deal with having sendmail running on my own box. And I do have a few domains that do not use Google Apps. They have one or two aliases associated with them, so the server needs to receive messages for those domains and forward them off to an external address. If any of this sounds vaguely familiar, it’s because it’s the same thing I detailed last time. My setup now is the same as then, so I won’t repeat any of it here.

For a more detailed explanation of running Postfix, you can read the Slicehost articles.

A Note on Git

I use Git to keep track of the code for all my projects. (If you’re new to Git, you ought to skim the Git Reference or Everyday GIT With 20 Commands Or So). To manage websites, I create a repository of the directory with the code that runs the site (in this case, /srv/myproject.com/code/) and another empty, bare repository to work as a hub. With a post-update and post-commit, the end result is an excellent web workflow:

  • A copy of the hub can be checked out on a local machine for development. Whenever a change is committed, a simple git push will push the code to the web server and automatically make it live.
  • Changes can be made on the server in the actual live website directory. (This is not a best practice, but I do it more often than I should probably admit.) Whenever a change is committed, it is automatically pushed to the hub, so that a simple git pull is all that’s needed on the development machine to update its repository.

A more detailed explanation of this workflow is at Joe Maller’s blog.

To start, I need to create a repository for the new project I created in this tutorial. And, since this is a new server, I need to give Git my name and email address to record with every commit.

1
2
3
4
$ git config --global user.name "Pig Monkey"
$ git config --global user.email "pm@pig-monkey.com"
$ cd /srv/myproject.com/code/
$ git init

Before adding the files, I create a .gitignore file in the repository root to tell Git to ignore compiled Python files.

$ vim .gitignore

*.pyc

Now I add all the files to the repository, confirm that it worked, and commit the files.

1
2
3
$ git add .
$ git status -s
$ git commit -m "Initial commit of myproject.com"

I create the bare hub directory directly along side the projects code/.

1
2
3
4
$ cd ../
$ mkdir hub.git
$ cd hub.git
$ git --bare init

With the hub created, I need to add it as the remote for the main repository and push the master branch to it.

1
2
3
4
$ cd ../code
$ git remote add hub /srv/myproject.com/hub.git
$ git remote show hub
$ git push hub master

Now the hub needs a post-update script so that every time something is pushed to it, that change is automagically pulled into the live website directory.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
$ vim /srv/myproject.com/hub.git/hooks/post-update

#!/bin/sh
echo
echo "**** Pulling changes into live"
echo

cd /srv/myproject.com/code || exit
unset GIT_DIR
git pull hub master

exec git-update-server-info

$ chmod +x /srv/myproject.com/hub.git/hooks/post-update

And the live website directory requires a post-commit script so that every time something is committed inside of it, that change is automagically pushed to the hub.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
$ vim /srv/myproject.com/code/.git/hooks/post-commit

#!/bin/sh
echo
echo "**** pushing changes to Hub"
echo

git push hub

$ chmod +x /srv/myproject.com/code/.git/hooks/post-commit

All that’s left is to check out the hub onto the development machine – my laptop, in this case!

1
2
3
$ mkdir ~/work/myproject/
$ cd ~/work/myproject/
$ git clone ssh://myserver.com/srv/myproject.com/hub.git code

To test things out, we can add a file to the repository on the development machine.

1
2
3
4
5
$ cd code/
$ touch test
$ git add test
$ git commit -m "A test"
$ git push

Now go back to the server, and the file should be there! To test things the other way around, I’ll delete the file from the live repository.

1
2
3
4
5
$ cd /srv/myproject.com/code/
$ ls
myproject test
$ git rm test
$ git commit -m "Removing the test file"

And once again to the development machine:

1
2
3
$ git pull
$ ls
deploy  myproject

No more test! It’s pretty dandy.

Restoring

If I was building a new server and restoring a project from an old server, I would simply mirror the old hub and then clone that in the live directory.

1
2
3
$ cd /srv/myproject.com/
$ git clone --mirror ssh://myoldserver.com/srv/myproject.com/hub.git
$ git clone hub.git code/

Resources

Prior to building this server, I was new to a lot of this – particularly, uWSGI and virtualenv. The following tutorials helped me a good deal in putting together the perfect setup for my needs.

Thoughts on SSH Security

OpenSSH has a history of security. Only rarely are holes found in the actual program. It’s much more likely that a system will be compromised through poor configuration of the SSH daemon. Ideally, an SSH config would allow only protocol 2 connections, allow only specified users to connect (and certainly not root), disable X11 forwarding, disable password authentication (forcing ssh keys instead), and allowing connections only from specified IPs. These config options would look like this:

Protocol 2
PermitRootLogin no
AllowUsers demo
X11Forwarding no
PasswordAuthentication no

Allowing connections from only specified IP addresses would be accomplished by adding something like the following to /etc/hosts.deny:

sshd: ALL # Deny all by default
sshd: 192.168.1.0/255.255.255.0 # Allow this subnet
sshd: 4.2.2.1 # Allow this IP

(You could also accomplish this with iptables, but I think editing the above file is simpler.)

But the last two options – disabling password auth and allowing only certain IP addresses – limits mobility. I constantly login to my slice from multiple IPs, and I also need to login during travel when I may or may not have my key on me.

The main thing these two options protect against is a brute force attack. By allowing password logins from any IP, we give the attacker the ability to exploit the weakest part of SSH. This is where DenyHosts comes in.

DenyHosts is a python script which attempts to recognize and block brute force attacks. It has many attractive features and is included in the default Ubuntu repositories.

1
$ sudo aptitude install denyhosts

The config file is located at /etc/denyhosts.conf. It is very simply and readable. I recommend reading through it, but most of the default options are acceptable. If any changes are made, the daemon must be restarted:

1
$ sudo /etc/init.d/denyhosts restart

Default Ports

Many people also advocating changing SSH’s default port to something other than 22 (more specifically, something over 1024, which won’t be scanned by default by nmap). The argument in support of this is that many automated attack scripts look for SSH only on port 22. By changing the port, you save yourself the headache of dealing with script kiddies. Opponents to changing the port would argue that the annoyance of having to specify the port number whenever using ssh or scp outweighs the minute security benefits. It’s a heated argument. I lean toward leaving SSH on the default port.

An Ubuntu VPS on Slicehost: Basic Setup

As mentioned previously, I’ve recently moved this domain over to Slicehost. What follows is Part One of a guide, compiled from my notes, to setting up an Ubuntu Hardy VPS. See also Part Two, Part Three, and Part Four.

Slicehost has an excellent article repository, containing guides on a number of subjects. After building a fresh Slice, you should first follow Part 1 and Part 2 of Slicehost’s basic setup articles.

I use slightly different coloring in my bash prompt, so, rather than what Slicehost suggests in their article, I add the following to ~/.bashrc:

1
export PS1='\[\033[0;32m\]\u@\[\033[0;35m\]\h\[\033[0;33m\] \w\[\033[00m\]: '

This is a good time to protect SSH by installing DenyHosts, which I discuss here:

1
$ sudo aptitude install denyhosts

Ubuntu’s default text editor is nano, which I abhor. Real men use vim. Ubuntu comes with a slimmed down version of vim, but you’ll probably want the full version:

1
$ sudo aptitude install vim

To change the global default editor variable, execute the following and select the editor of your choice:

1
$ sudo update-alternatives --config editor

This is also a perfect time to install GNU Screen.

1
$ sudo aptitude install screen

If you’re not familiar with Screen, Red Hat Magazine has a nice little introduction

My .screenrc looks like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# Print a pretty line at the bottom of the screen
hardstatus alwayslastline
hardstatus string '%{= kG}[ %{G}%H %{g}][%= %{=kw}%?%-Lw%?%{r}(%{W}%n*%f%t%?(%u)%?%{r})%{w}%?%+Lw%?%?%= %{g}][%{Y}%Y-%m-%d %{W}%c %{g}]'

# Nobody likes startup messages
startup_message off

# Turn visual bell on and set the message to display for only a fraction of a second
vbell on
vbellwait .3

# Set default shell title to blank
shelltitle ''

# Gimme my scrollback!
defscrollback 5000

# Change command character to backtick
escape ``

# Stop programs (like vim) from leaving their contents
# in the window after they exit
altscreen on

# Default screens
screen -t shell 0

I prefer to have my bash profile setup to connect me to Screen as soon as I login. If there are no running sessions, it will create one. If there is a current session, it will disconnect the session from wherever it is connected and connect it to my login. When I disconnect from screen, it automatically logs me out. To achieve this, I add the following to ~/.bashrc:

1
2
3
4
5
# If possible, reattach to an existing session and detach that session
# elsewhere. If not possible, create a new session.
if [ -z "$STY" ]; then
    exec screen -dR
fi

I would also recommend following Slicehost’s guide to installing chkrootkit and rkhunter.

One more thing: let’s set the timezone of the server to whatever is local to you (Slicehost’s Ubuntu image defaults to UTC). To do that, run:

1
$ sudo dpkg-reconfigure tzdata

Next up: install a web server.