Saturday, February 28, 2015

List running EC2 instances with golang and the aws-sdk-go

Managing multiple AWS accounts can sometimes be tough, even when doing something as simple as matching a private IP address with a hostname.

In the past I used the aws cli tools, but I had to constantly switch both the accounts and the regions when making requests:

# Make it's in the prod-5 account in us-west-1
aws ec2 --profile=prod-5 --region=us-west-1 | grep my_instance

# Okay not that account or region, let's try eu-west-1
aws ec2 --profile=prod-5 --region=eu-west-2 | grep my_instance

Repeat 1x for each account and region

As you can imagine this is extremely time consuming, even when using the CLI tools. I wrote a small tool that will find every single instance you can view using every account available to you (according to your ~/.aws/config). The aggregate results can then be searched.

I choose to use the existing ~/.aws/config file so that it works along side your normal aws cli tools. 

This is a very good use-case for Golang, which has nice concurrency primitives. With five accounts this script (once compiled) will display all of the results in under 1.5 seconds. Not bad.

The result is a space-separated list with some additional values added. It should be easy to find what you're looking for:

$ ./aws_list
i-71930187 prod-manage005 10.0.0.180 t2.micro 207.171.166.22 prod-account
i-71930187 stage-controller7 10.0.0.164 t2.medium 72.21.206.80 staging-account
i-71930187 prod-vpn01 10.0.0.239 m4.large None prod-account
i-71930187 stephen-test-host 10.0.0.216 m3.large 216.58.216.174 test-account
...

The output is a space-separated file and only "running" and "pending" instances are displayed. For a list of filters or instance attributes consult the official documentation.


package main

import (
    "fmt"
    "github.com/awslabs/aws-sdk-go/aws"
    "github.com/awslabs/aws-sdk-go/service/ec2"
    "github.com/vaughan0/go-ini"
    "net/url"
    "os"
    "runtime"
    "strings"
    "sync"
)

func check(e error) {
    if e != nil {
        panic(e)
    }
}

/*
printIds accepts an aws credentials file and a region, and prints out all
instances within the region in a format that's acceptable to us. Currently that
format is like this:

  instance_id name private_ip instance_type public_ip account

Any values that aren't available (such as public ip) will be printed out as
"None"

Because the "name" parameter is user-defined, we'll run QueryEscape on it so that
our output stays as a space-separated line.
*/
func printIds(creds aws.CredentialsProvider, account string, region string, wg *sync.WaitGroup) {
    defer wg.Done()

    svc := ec2.New(&aws.Config{
        Credentials: creds,
        Region:      region,
    })

    // Here we create an input that will filter any instances that aren't either
    // of these two states. This is generally what we want
    params := &ec2.DescribeInstancesInput{
        Filters: []*ec2.Filter{
            &ec2.Filter{
                Name: aws.String("instance-state-name"),
                Values: []*string{
                    aws.String("running"),
                    aws.String("pending"),
                },
            },
        },
    }

    // TODO: Actually care if we can't connect to a host
    resp, _ := svc.DescribeInstances(params)
    // if err != nil {
    //      panic(err)
    // }

    // Loop through the instances. They don't always have a name-tag so set it
    // to None if we can't find anything.
    for idx, _ := range resp.Reservations {
        for _, inst := range resp.Reservations[idx].Instances {

            // We need to see if the Name is one of the tags. It's not always
            // present and not required in Ec2.
            name := "None"
            for _, keys := range inst.Tags {
                if *keys.Key == "Name" {
                    name = url.QueryEscape(*keys.Value)
                }
            }

            important_vals := []*string{
                inst.InstanceID,
                &name,
                inst.PrivateIPAddress,
                inst.InstanceType,
                inst.PublicIPAddress,
                &account,
            }

            // Convert any nil value to a printable string in case it doesn't
            // doesn't exist, which is the case with certain values
            output_vals := []string{}
            for _, val := range important_vals {
                if val != nil {
                    output_vals = append(output_vals, *val)
                } else {
                    output_vals = append(output_vals, "None")
                }
            }
            // The values that we care about, in the order we want to print them
            fmt.Println(strings.Join(output_vals, " "))
        }
    }
}

func main() {
    // Go for it!
    runtime.GOMAXPROCS(runtime.NumCPU())

    // Make sure the config file exists
    config := os.Getenv("HOME") + "/.aws/config"
    if _, err := os.Stat(config); os.IsNotExist(err) {
        fmt.Println("No config file found at: %s", config)
        os.Exit(1)
    }

    var wg sync.WaitGroup

    file, err := ini.LoadFile(config)
    check(err)

    for key, values := range file {
        profile := strings.Fields(key)

        // Don't print the default or non-standard profiles
        if len(profile) != 2 {
            continue
        }

        // Where to find this host. The account isn't necessary for the creds
        // but it's something we expose to users when we print
        account := profile[1]
        key := values["aws_access_key_id"]
        pass := values["aws_secret_access_key"]
        creds := aws.Creds(key, pass, "")

        // Gather a list of all available AWS regions. Even though we're gathering
        // all regions, we still must use a region here for api calls.
        svc := ec2.New(&aws.Config{
            Credentials: creds,
            Region:      "us-west-1",
        })

        // Iterate over every single stinking region to get a list of available
        // ec2 instances
        regions, err := svc.DescribeRegions(&ec2.DescribeRegionsInput{})
        check(err)
        for _, region := range regions.Regions {
            wg.Add(1)
            go printIds(creds, account, *region.RegionName, &wg)
        }
    }

    // Allow the goroutines to finish printing
    wg.Wait()
}

Wednesday, February 25, 2015

Generate a unique, strong password on the command line (linux, mac osx)

Find yourself generating a lot of random passwords? Here's a way to generate quick, random, and secure passwords on the command line:
echo $(head -c 64 /dev/urandom | base64) $(date +%s) | shasum | awk '{print $1}'
This command will read 64 bytes of random data from /dev/urandom, base64 encode it, add a small salt (the current data in epoch time), and then create a sha1 hash of the data.
I like this because it's cryptographically secure and the chance of a collision (provided your PNRG isn't totally borked), is infinitely small. It's also a hexadecimal string, so I don't have to worry about quoting it in weird ways or escaping special characters. I can just double-click it in iterm and it's automatically added to my clipboard!

Go ahead and double click the shas below and then click the password from 1password. You'll know what I'm talking about.

The drawback being you can't possibly remember these passwords unless you're US memory champion Nelson Dellis, but you use a password manager anyway, right? Right?!
I do this so frequently that I created an alias, so I just have to type "pw" on the command line to get a random password.
alias pw="echo \$(head -c 64 /dev/urandom | base64) \$(date +%s) | shasum | awk '{print \$1}'"
Now you can create random passwords all day long.
[stephen ~]$ pw
fc2bff4a44cc71b77638185161383592adcf5a6d
[stephen ~]$ pw
172f09a28878eab53df26801564f164209da7b6e
[stephen ~]$ while true; do pw; done
cf8f04bfa23b16dea92b69a9af72a0433e67cb79
28219dc9f626233df6361b44c673505755ac380e
ce14392eeeb408d68a4436586fc05f691c334006
d9c82dd59637ee75d9090195a4633d4b184e6e65
26e6754f480cf039d6b0e131bf079b2a0338b3e2
75376f012bc2ff36c00cb224ac245da719c832ae
b530a231f3a60030db47c077a249857ce4bb2d45
...
# Here's a password from 1password. Go ahead and double click it to add it to your clipboard
Fp9ef>btgMUm%K2AokM(JXV,vkF?CGX9Ry4d78.a

Saturday, November 22, 2014

Encrypting and decrypting files with Gnu Privacy Guard (GPG)

Asymmetrical encryption with GnuPG


You should start using asymmetrical encryption for your backups. Here's a little guide.
With asymmetrical key encryption, other hosts and people can encrypt data that only you can decrypt, meaning that you don't have to share a secret key with anyone and you can distribute the public key freely.
At Elastica this is how we do backups. Every host gets preseeded with one of our strong backup keys. This allows them to gobble up sensitive data and store it in places like S3. If our S3 account was ever compromised then the intruder would have a bunch of encrypted files that would take them 20 years of computing power to decrypt.
Since decryption speed is not an issue with cold storage backups, I recommend the maximum 4096-bit RSA key.
Here's a quick guide to creating a strong crypto key and encrypting some files.
Also remember that gpg can be installed on your mac via brew.
Generate the key:
$ gpg --gen-key
(follow the prompts)
If you'd rather do this automatically or in a script, you can feed the options in:
$ gpg --batch --gen-key <<EOF
    Key-Type: rsa
    Key-Length: 4096
    Name-Real: Stephen Wood
    Name-Comment: For Testing
    Name-Email: smwood4@gmail.com
    Expire-Date: 0
    %pubring my_key.pub
    %secring my_key.sec
    %commit
    %echo done
EOF
You'll want to import the private key on any hosts that you want to be able to decrypt data.
$ gpg --allow-secret-key-import --import my_key.key
Remember to securely delete your secret key if you're not going to put the file elsewhere.
$ srm my_key.key

Encrypting and decrypting

Encrypting can be done on any host that has your public key. First, import the public key to the local hosts's gpg key ring
$ gpg --import my_key.pub
To encrypt a single file:
$ gpg --recipient smwood4@gmail.com --encrypt ping.heystephenwood.com.cert
(creates ping.heystephenwood.com.cert.gpg)
Note that the key is referred to as an email address. This is how gpg typically works.
Decrypting the file is easy if you have the private key:
$ gpg --decrypt ping.heystephenwood.com.cert.gpg > ping.heystephenwood.com.cert
By default gpg will decrypt a file to stdout, so we redirect it to a file. You can tell gpg to decrypt it to the original filename by passing in the --use-embedded-filename flag:
$ gpg --use-embedded-filename --decrypt ping.heystephenwood.com.cert.gpg
(creates ping.heystephenwood.com.cert.gpg)
Notice that we didn't even have to specify which key to use. That's because gpg will use imbedded metadata in the file to decrypt the key. You can see this information by passing in the --list-packets flag:
$ gpg --list-packets ping.heystephenwood.com.cert.gpg
:pubkey enc packet: version 3, algo 1, keyid 6FC17A3C5E45F65C
    data: [4096 bits]
:encrypted data packet:
    length: unknown
    mdc_method: 2
gpg: encrypted with 4096-bit RSA key, ID 5E45F65C, created 2014-06-13
      "Stephen Wood <smwood4@gmail.com>"
:compressed packet: algo=2
:literal data packet:
    mode b (62), created 1416686288, name="ping.heystephenwood.com.cert",
    raw data: 2171 bytes
Pretty nifty, huh?

Multiple files

Encrypting multiple files is much easier to do via the tar command. Since gpg files are compressed automatically, it's not even necessary to gzip them!
Here's how to encrypt an entire folder:
$ tar cf - some_folder | gpg --recipient smwood4@gmail.com
(creates some_folder.gpg)
Since gpg defaults to stdout, we simply need to pass that back to tar
$ gpg --decrypt some_folder.gpg | tar xf -
(creates some_folder)

Other fun things

You can also use your private key to sign a message that others can verify for authenticity.
Let's create a document:
cat > my_message.txt <<MESSAGE
Help! The NSA is after me! They've tapped my phone, TV, microwave, and
Nike Air Jordans! Also the fillings in my teeth won't stop playing episodes
of "This American Life" and Ira Glass is getting on my nerves.
MESSAGE
Now let's sign the message:
$ gpg -u smwood4@gmail.com --clearsign my_message.txt 
(produces my_message.txt.asc)
You've got yourself a signed document.
$ cat my_message.txt.asc
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

Help! The NSA is after me! They've tapped my phone, TV, microwave, and
Nike Air Jordans! Also the fillings in my teeth won't stop playing episodes
of "This American Life" and Ira Glass is getting on my nerves.
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1

iQIcBAEBAgAGBQJUcPACAAoJEKSJEkYj0afvor0QALvrYflk52NkNmwpq09FM+2x
06RKsmzQgXa8W+3yHpTUlAMhVPoToeETp9wrM4zod+6nmiyan/aqP1QzXq0QC+yc
SEOGIn7r69+d1ybCXHnC2ebbB2pVj8NwH30a6hLNUHlmZ9xGjX4bnqUREIo1booV
MnWqUab7PFjEJ97fVW9b47kpZ+Mi0P8MhphGA+QHzGl56UtDo5wPci8A3TKqfCsq
didoYGAJurVnVIwvjXPkwKnKwL4E0AvTRN5iiiN6Rib+be8Pmy8wUNA7LdwjERO1
H8zVCgyFvOzA16PbdmKJF5nnUAlJASO4ZStUD5z/AIYir9cXuNSlx45FFVse3lFu
yyftQkvJPJbzVdg/JaYswj7vMFPPmRBweFzFZjBqrDu+o1aPWougbOFnU3ZDuf3s
4SmbI7gJDDet7cvYHWn+8doGGI5BpPb0gXjfhW+WBuvMZ01GJbXwychi5d4xC1Qi
XYkd4RgdxxIhyLrO+whWYHFZlI3UY6cPPyyiDjTLR/UCP2IrtdL7gcPJicXGSGG3
EJLjG1mK+nk44QT4oW7Ghha6G1oI2YoqjAWrlWhaSyxlNSv4qw75x5w6WtzoL2yR
mayROMUmmHrBYfgFtgk64X+qcd3Li8/InZUw95A5SnV69r2h5p05fPiE70sgu73R
ozOu9eBEjYDTEb4PYS+5
=JiEL
-----END PGP SIGNATURE-----
Anyone can verify the message. They don't even need your public key:

gpg --verify my_message.txt.asc
gpg: Signature made Sat Nov 22 12:29:17 2014 PST using RSA key ID 23D1A7EF
gpg: Good signature from "Stephen Wood <smwood4@gmail.com>"
And what happens when somebody intercepts and alters your original message?

$ cat my_message.txt.asc
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

Dear friend. All is well here.
-----BEGIN PGP SIGNATURE-----
...
gpg --verify my_message.txt.asc
gpg: Signature made Sat Nov 22 12:20:18 2014 PST using RSA key ID 23D1A7EF
gpg: BAD signature from "Stephen Wood <smwood4@gmail.com>"
Looks like your friend is dead. I'm so sorry.
This can be taken a step further if you've already securely exchanged your public key with your "friend". They can both verify your message hasn't been altered, and also verify that it was sent from you. They just need to verify that the Key ID is the same as the one you've imported.
That's it for now. Encrypt your backups! It's easy to do.

Monday, September 29, 2014

OpenVPN & Raspberry Pi auto-installer

Openvpn server on your Raspberry PI


From wikipedia:
OpenVPN is an open source software application that implements virtual private network (VPN) techniques for creating secure point-to-point or site-to-site connections in routed or bridged configurations and remote access facilities. It uses a custom security protocol that utilizes SSL/TLS for key exchange.
What that means is that openvpn will allow you to safely and securely route your internet traffic through an untrusted network to a trusted one. It does this by encrypting your traffic so nobody can read it until it goes out from your openvpn server.
There are some benefits to this:
  1. Prevent others from snooping on your traffic.
  2. Access websites that are blocked by your work, school, or oppressive governments.
  3. Access assets on your home network from anywhere.
This auto-install script will turn your raspberry pi into an openvpn server so you can browse the internet safely and securely.
Then why use it? Because sometimes you end up on insecure networks (think starbucks, stadiums, etc). This will protect your privacy in those situations. It will not prevent people from finding you if you are stupid and do something illegal.

Running the installer

The auto-installer is completely automated and can be run directly from the web.
From your raspberry pi:
# Set the user
$ OPENVPN_USER='stephen.openvpn.local'

# Run the installer
$ curl "https://raw.githubusercontent.com/stephen-mw/raspberrypi-openvpn-autoinstall/master/bootstrap" | sudo bash

The default key size is 4096 bits long. You can change the key size by exporting the KEY_SIZE variable before running the installer:

# Change the key size to something other than 4096
$ KEY_SIZE=2048

Remember that you should always inspect these types of files before ever running them. You can download it locally and run it like so:
# Download the file
$ wget "https://raw.githubusercontent.com/stephen-mw/raspberrypi-openvpn-autoinstall/master/bootstrap"

# Make sure it's legit
$ less bootstrap

# Execute it
$ chmod +x bootstrap
$ sudo ./bootstrap stephen.openvpn.local

What's the installer do

The installer script will download openvpn and generate all of the necessary root certificates for you. Then it will generate and sign a new certificate for a user. Lastly it will create the ovpn file and place it in /root/client_<some_user>.
All you need to do is download that file into your openvpn client software and you'll be able to safely and securely connect to the host.

Testing

I've included a Vagrantfile for you to run tests. Simply clone the repo and then run:
$ vagrant up
==> default: Forcing shutdown of VM...
==> default: Destroying VM and associated drives...
==> default: Running cleanup tasks for 'shell' provisioner...
Bringing machine 'default' up with 'virtualbox' provider...
==> default: Importing base box 'wheezy64'...
...
==> default: Make sure that your firewall allows incoming UDP connections to port 1194.
==> default:
==> default: The last thing necessary is to securely copy the configuration file over to your
==> default: computer and then load it. The configuration file is located at:
==> default:
==> default:   /root/client_test.ovpn
The vagrant guest will be running the openvpn server. You can pull down the client file and connect to it locally.

Clone the repo

https://github.com/stephen-mw/raspberrypi-openvpn-auto-install

Edit: 2014-12-23

The original bootstrap was missing some iptables rules that allowed the raspberry pi to masquarade and forward IP. I've updated the script and everything should be working out of the box now.

Tuesday, September 23, 2014

Mac OS X time machine backups to a windows or linux samba share

Time machine is a great product from Apple. I used it just recently when I replaced my laptop. All I had to do was select the time machine disk when booting up for the first time and everything was just how I left it. Fantastic!

Creating backups is now very important to me. With a USB hard drive this is very simple, but I wanted to take backups to my home server, where there is plenty of secure space.

Making a backup to network drives (like windows and linux samba shares) is not supported out of the box. I'll show you how to do it.

Setting up the network drive

This guide assumes you have a network-attached drive that's available and mounted. You can use samba to create this drive on either windows or linux.

Creating the sparse disk

A sparse image is an image that grows in size. It's a mac thing. This image will become our dmg file that gets mounted.

Open up up the disk utility. You can do this by searching for it in finder:


  • Give it a name with something you'll remember
  • Before you do anything else, change the image format to "sparse disk image". This will allow us to create a 300GB disk image without allocating all of that space at once.
  • For format select "Mac OS extended"
  • For size select "custom" and put in your desired size. The image will grow to this size.
  • For partition select "single partition -- apple partition map"
  • Save it to your network network mount


You'll see that the disk is 300GB (or whatever size) large but you're only using around 300MB of it.

Next we need to run a command that will force time machine to use our new disk. Find the mount point for the disk. If you select the disk in disk utilities, you can find it at the bottom. In my case it's "/Volumes/stephen_time"


Open up your terminal and paste in the following command:

sudo tmutil setdestination <mount point>

Now open up your time machine preferences and check out the new disk:



Hurray! You've got your disk loaded and ready to use. Now just do the backups as normal. You'll need to remount the disk (find it and double click it) when you want to make backups.

Monday, September 22, 2014

Dyn DNS cli tool

Get the file: https://github.com/stephen-mw/dyncli

Usage

usage: dyncli [-h] [-e ENDPOINT] [-c CREATE] [--cname] [-v VALUE] [-t TTL]
              [-d DELETE] [-u UPDATE_RECORD] [-U USER] [-P PASSWORD]
              [-A ACCOUNT] [-Z ZONE] [-l] [--verbose]

optional arguments:
  -h, --help            show this help message and exit
  -e ENDPOINT, --endpoint ENDPOINT
                        The endpoint to make API requests.
  -c CREATE, --create CREATE
                        Create a new record. Defaults to A record unless
                        --cname is set.
  --cname               Create a cname record instead of an A record.
  -v VALUE, --value VALUE
                        The value to set the DNS record to. Eg 10.0.0.101.
  -t TTL, --ttl TTL     TTL of record in seconds.
  -d DELETE, --delete DELETE
                        Delete an existing record
  -u UPDATE_RECORD, --update UPDATE_RECORD
                        Perform a DNS update on an existing record.
  -U USER, --user USER  The username to connect to the dyn api. Can also be
                        set as DYN_USER in environment.
  -P PASSWORD, --password PASSWORD
                        The password to use with the dyn api. Can also be set
                        as DYN_PW in environment.
  -A ACCOUNT, --account ACCOUNT
                        The account name used to make API requests. Can be set
                        as DYN_ACCOUNT in environment.
  -Z ZONE, --zone ZONE  The zone to take action on. Eg example.com. Can be set
                        as DYN_ZONE in environment.
  -l, --list            List all dns records as a csv file.
  --verbose             Print out api responses.
Dyn is a nice DNS service. It's an alternative to Amazon's Route 53. Though I do not believe that dyn's API is as robust as amazon, with the right set of tools is just dandy.
This tool allows for the manipulation of DNS records via the restful API.
It currently supports the following:
  • Create A and CNAME records
  • Update A and CNAME records
  • Delete A and CNAME records
  • List all of the records as a CSV

Requirements

The requests & argparse packages are required but not currently part of the standard library. Get them with pip:
pip install -r requirements.txt
You can pass your username and password directly into the script, but it's better if you setup a few environment variables. dyncli will take advantage of the following environment variables if they are present:
DYN_USER
DYN_PW
DYN_ZONE
DYN_ACCOUNT
You can activate these by appending them to your ~/.bash_profile:
export DYN_USER='foo'
export DYN_PW='bar'
export DYN_ZONE='example.com'
export DYN_ACCOUNT='example'

Creating a record

Right now both A and CNAME record creation is supported. Create a record uses the -c flag along with the optional --cname flag if that's your desired record type.
The default record type is an address (A) record.
# Create an A record with default TTL
$ ./dyncli -c myfqdn.example.net -v 10.0.0.100

# Create an A record with custom TTL
$ ./dyncli -c myfqdn.example.net -v 10.0.0.100 --ttl 3600

# Create a CNAME record
$ ./dyncli -c myfqdn.example.net -v otherfqdn.example.net --cname

Updating a record

Once records are created, they can be easily updated with the -u or --update flag.
# Update the TTL of an existing record
$ ./dyncli -u myfqdn.example.net --ttl 3600

# Update the value of an existing cname record
$ ./dyncli -u myfqdn.example.net -v someotherfqdn.example.net

Deleting a record

Records are deleted by passing the -d or --delete flag.
# Delete an address (A) record
$ ./dyncli -d myfqdn.example.net

# Delete a cname record (requires the --cname flag)
$ ./dyncli -d myfqdn.example.net --cname

List all records (csv format)

Sometimes it's useful to list all of the records. We use this to do a simple nightly backup of our DNS zone.
The output format is a simple CSV file. Beware that if you have commas in your TXT record, they'll be translated to a pipe (|).
$ ./dyncli --list
zone,ttl,fqdn,record_type,data
example.net,300,test.example.net,CNAME,test2.example.net
example.net,300,foo.example.net,A,172,16.0.100
example.net,3600,example.net,TXT,public_key=sdjflksdj
..
Sometimes it's also useful to check if a zone exists before you go creating one willy nilly
$ ./dyncli --list | grep database
example.net,300,database.example.net,A,172.16.0.100
example.net,300,database.internal.example.net,A,172.16.0.100
example.net,300,database.something.example.net,A,172.16.0.100

(See what happens when you go on creating records without checking first?)
If you notice any bugs please open an issue.