Friday, April 15, 2016

LDAP server setup and client authentication

We recently bought at work a CloudBees Jenkins Enterprise license and I wanted to tie the user accounts to a directory service. I first tried to set up Jenkins authentication via the AWS Directory Service, hoping it will be pretty much like talking to an Active Directory server. That proved to be impossible to set up, at least for me. I also tried to have an LDAP proxy server talking to the AWS Directory Service and have Jenkins authenticate against the LDAP proxy. No dice. I ended up setting up a good old-fashioned LDAP server and managed to get Jenkins working with it. Here are some of my notes.

OpenLDAP server setup


I followed this excellent guide from Digital Ocean. The server was an Ubuntu 14.04 EC2 instance in my case. What follows in terms of the server setup is taken almost verbatim from the DO guide.

Set the hostname



# hostnamectl set-hostname my-ldap-server


Edit /etc/hosts and make sure this entry exists:

LOCAL_IP_ADDRESS my-ldap-server.mycompany.com my-ldap-server

(it makes a difference that the FQDN is the first entry in the line above!)

Make sure the following types of names are returned when you run hostname with different options:


# hostname
my-ldap-server

# hostname -f
my-ldap-server.mycompany.com

# hostname -d
mycompany.com

Install slapd



# apt-get install slapd ldap-utils
# dpkg-reconfigure slapd

(here you specify the LDAP admin password)

Install the SSL Components



# apt-get install gnutls-bin ssl-cert

Create the CA Template


# mkdir /etc/ssl/templates
# vi /etc/ssl/templates/ca_server.conf
# cat /etc/ssl/templates/ca_server.conf
cn = LDAP Server CA
ca
cert_signing_key


Create the LDAP Service Template



# vi /etc/ssl/templates/ldap_server.conf
# cat /etc/ssl/templates/ldap_server.conf
organization = "My Company"
cn = my-ldap-server.mycompany.com
tls_www_server
encryption_key
signing_key
expiration_days = 3650


Create the CA Key and Certificate



# certtool -p --outfile /etc/ssl/private/ca_server.key
# certtool -s --load-privkey /etc/ssl/private/ca_server.key --template /etc/ssl/templates/ca_server.conf --outfile /etc/ssl/certs/ca_server.pem

Create the LDAP Service Key and Certificate



# certtool -p --sec-param high --outfile /etc/ssl/private/ldap_server.key
# certtool -c --load-privkey /etc/ssl/private/ldap_server.key --load-ca-certificate /etc/ssl/certs/ca_server.pem --load-ca-privkey /etc/ssl/private/ca_server.key --template /etc/ssl/templates/ldap_server.conf --outfile /etc/ssl/certs/ldap_server.pem


Give OpenLDAP Access to the LDAP Server Key



# usermod -aG ssl-cert openldap
# chown :ssl-cert /etc/ssl/private/ldap_server.key
# chmod 640 /etc/ssl/private/ldap_server.key


Configure OpenLDAP to Use the Certificate and Keys


IMPORTANT NOTE: in modern versions of slapd, configuring the server is not done via slapd.conf anymore. Instead, you put together ldif files and run LDAP client utilities such as ldapmodify against the local server. The Distinguished Name of the entity you want to modify in terms of configuration is generally dn: cn=config but it can also be the LDAP database dn: olcDatabase={1}hdb,cn=config.

# vi addcerts.ldif
# cat addcerts.ldif
dn: cn=config
changetype: modify
add: olcTLSCACertificateFile
olcTLSCACertificateFile: /etc/ssl/certs/ca_server.pem
-
add: olcTLSCertificateFile
olcTLSCertificateFile: /etc/ssl/certs/ldap_server.pem
-
add: olcTLSCertificateKeyFile
olcTLSCertificateKeyFile: /etc/ssl/private/ldap_server.key


# ldapmodify -H ldapi:// -Y EXTERNAL -f addcerts.ldif
# service slapd force-reload
# cp /etc/ssl/certs/ca_server.pem /etc/ldap/ca_certs.pem
# vi /etc/ldap/ldap.conf

* set TLS_CACERT to following:
TLS_CACERT /etc/ldap/ca_certs.pem

# ldapwhoami -H ldap:// -x -ZZ
Anonymous


Force Connections to Use TLS


Change olcSecurity attribute to include 'tls=1':

# vi forcetls.ldif
# cat forcetls.ldif
dn: olcDatabase={1}hdb,cn=config
changetype: modify
add: olcSecurity
olcSecurity: tls=1


# ldapmodify -H ldapi:// -Y EXTERNAL -f forcetls.ldif
# service slapd force-reload
# ldapsearch -H ldap:// -x -b "dc=mycompany,dc=com" -LLL dn
(shouldn’t work)

# ldapsearch -H ldap:// -x -b "dc=mycompany,dc=com" -LLL -Z dn
(should work)


Disallow anonymous bind


Create user binduser to be used for LDAP searches:


# vi binduser.ldif
# cat binduser.ldif
dn: cn=binduser,dc=mycompany,dc=com
objectClass: top
objectClass: account
objectClass: posixAccount
objectClass: shadowAccount
cn: binduser
uid: binduser
uidNumber: 2000
gidNumber: 200
homeDirectory: /home/binduser
loginShell: /bin/bash
gecos: suser
userPassword: {crypt}x
shadowLastChange: -1
shadowMax: -1
shadowWarning: -1


# ldapadd -x -W -D "cn=admin,dc=mycompany,dc=com" -Z -f binduser.ldif
Enter LDAP Password:
adding new entry "cn=binduser,dc=mycompany,dc=com"

Change olcDissalows attribute to include bind_anon:


# vi disallow_anon_bind.ldif
# cat disallow_anon_bind.ldif
dn: cn=config
changetype: modify
add: olcDisallows
olcDisallows: bind_anon


# ldapmodify -H ldapi:// -Y EXTERNAL -ZZ -f disallow_anon_bind.ldif
# service slapd force-reload

Also disable anonymous access to frontend:

# vi disable_anon_frontend.ldif
# cat disable_anon_frontend.ldif
dn: olcDatabase={-1}frontend,cn=config
changetype: modify
add: olcRequires
olcRequires: authc


# ldapmodify -H ldapi:// -Y EXTERNAL -f disable_anon_frontend.ldif
# service slapd force-reload


Create organizational units and users


Create helper scripts:

# cat add_ldap_ldif.sh
#!/bin/bash


LDIF=$1


ldapadd -x -w adminpassword -D "cn=admin,dc=mycompany,dc=com" -Z -f $LDIF


# cat modify_ldap_ldif.sh
#!/bin/bash


LDIF=$1


ldapmodify -x -w adminpassword -D "cn=admin,dc=mycompany,dc=com" -Z -f $LDIF


# cat set_ldap_pass.sh
#!/bin/bash


USER=$1
PASS=$2


ldappasswd -s $PASS -w adminpassword -D "cn=admin,dc=mycompany,dc=com" -x "uid=$USER,ou=users,dc=mycompany,dc=com" -Z

Create ‘mypeople’ organizational unit:


# cat add_ou_mypeople.ldif
dn: ou=mypeople,dc=mycompany,dc=com
objectclass: organizationalunit
ou: users
description: all users

# ./add_ldap_ldif.sh add_ou_mypeople.ldif

Create 'groups' organizational unit:


# cat add_ou_groups.ldif
dn: ou=groups,dc=mycompany,dc=com
objectclass: organizationalunit
ou: groups
description: all groups


# ./add_ldap_ldif.sh add_ou_groups.ldif

Create users (note the shadow attributes set to -1, which means they will be ignored):


# cat add_user_myuser.ldif
dn: uid=myuser,ou=mypeople,dc=mycompany,dc=com
objectClass: top
objectClass: account
objectClass: posixAccount
objectClass: shadowAccount
cn: myuser
uid: myuser
uidNumber: 2001
gidNumber: 201
homeDirectory: /home/myuser
loginShell: /bin/bash
gecos: myuser
userPassword: {crypt}x
shadowLastChange: -1
shadowMax: -1
shadowWarning: -1

# ./add_ldap_ldif.sh add_user_myuser.ldif
# ./set_ldap_pass.sh myuser MYPASS


Enable LDAPS


In /etc/default/slapd set:

SLAPD_SERVICES="ldap:/// ldaps:/// ldapi:///"


Enable debugging


This was a life saver when it came to troubleshooting connection issues from clients such as Jenkins or other Linux boxes. To enable full debug output, set olcLogLevel to -1:

# cat enable_debugging.ldif
dn: cn=config
changetype: modify
add: olcLogLevel
olcLogLevel: -1

# ldapadd -H ldapi:// -Y EXTERNAL -f enable_debugging.ldif
# service slapd force-reload


Configuring Jenkins LDAP authentication


Verify LDAPS connectivity from Jenkins to LDAP server


In my case, the Jenkins server is in the same VPC and subnet as the LDAP server, so I added an /etc/hosts entry on the Jenkins box pointing to the FQDN of the LDAP server so it can hit its internal IP address:

IP_ADDRESS_OF_LDAP_SERVER my-ldap-server.mycompany.com

I verified that port 636 (used by LDAPS) on the LDAP server is reachable from the Jenkins server:

# telnet my-ldap-server.mycompany.com 636
Trying IP_ADDRESS_OF_LDAP_SERVER...
Connected to my-ldap-server.mycompany.com.
Escape character is '^]'.

Set up LDAPS client on Jenkins server (StartTLSdoes not work w/ Jenkins LDAP plugin!)


# apt-get install ldap-utils

IMPORTANT: Copy over /etc/ssl/certs/ca_server.pem from LDAP server as /etc/ldap/ca_certs.pem on Jenkins server and then:

# vi /etc/ldap/ldap.conf
set:
TLS_CACERT /etc/ldap/ca_certs.pem

Add LDAP certificates to Java keystore used by Jenkins


As user jenkins:
$ mkdir .keystore
$ cp /usr/lib/jvm/java-7-openjdk-amd64/jre/lib/security/cacerts .keystore/
(you may need to customize the above line in terms of the path to the cacerts file -- it is the one under your JAVA_HOME)

$ keytool --keystore /var/lib/jenkins/.keystore/cacerts --import --alias my-ldap-server.mycompany.com:636 --file /etc/ldap/ca_certs.pem
Enter keystore password: changeit
Owner: CN=LDAP Server CA
Issuer: CN=LDAP Server CA
Serial number: 570bddb0
Valid from: Mon Apr 11 17:24:00 UTC 2016 until: Tue Apr 11 17:24:00 UTC 2017
Certificate fingerprints:
....
Extensions:
....

Trust this certificate? [no]:  yes
Certificate was added to keystore

In /etc/default/jenkins, set JAVA_ARGS to:
JAVA_ARGS="-Djava.awt.headless=true -Djavax.net.ssl.trustStore=/var/lib/jenkins/.keystore/cacerts -Djavax.net.ssl.trustStorePassword=changeit"  

As root, restart jenkins:

# service jenkins restart

Jenkins settings for LDAP plugin


This took me a while to get right. The trick was to set the rootDN to dc=mycompany, dc=com and the userSearchBase to ou=mypeople (or to whatever name you gave to your users' organizational unit). I also tried to get LDAP groups to work but wasn't very successful.

Here is the LDAP section in /var/lib/jenkins/config.xml:
 <securityRealm class="hudson.security.LDAPSecurityRealm" plugin="ldap@1.11">
   <server>ldaps://my-ldap-server.mycompany.com:636</server>
   <rootDN>dc=mycompany,dc=com</rootDN>
   <inhibitInferRootDN>true</inhibitInferRootDN>
   <userSearchBase>ou=mypeople</userSearchBase>
   <userSearch>uid={0}</userSearch>
<groupSearchBase>ou=groups</groupSearchBase> <groupMembershipStrategy class="jenkins.security.plugins.ldap.FromGroupSearchLDAPGroupMembershipStrategy"> <filter>member={0}</filter> </groupMembershipStrategy>
   <managerDN>cn=binduser,dc=mycompany,dc=com</managerDN>
   <managerPasswordSecret>JGeIGFZwjipl6hJNefTzCwClRcLqYWEUNmnXlC3AOXI=</managerPasswordSecret>
   <disableMailAddressResolver>false</disableMailAddressResolver>
   <displayNameAttributeName>displayname</displayNameAttributeName>
   <mailAddressAttributeName>mail</mailAddressAttributeName>
   <userIdStrategy class="jenkins.model.IdStrategy$CaseInsensitive"/>
   <groupIdStrategy class="jenkins.model.IdStrategy$CaseInsensitive"/>

 </securityRealm>


At this point, I was able to create users on the LDAP server and have them log in to Jenkins. With CloudBees Jenkins Enterprise, I was also able to use the Role-Based Access Control and Folder plugins in order to create project-specific folders and folder-specific groups specifying various roles. For example, a folder MyProjectNumber1 would have a Developers group defined inside it, as well as an Administrators group and a Readers group. These groups would be associated with fine-grained roles that only allow certain Jenkins operations for each group.

I tried to have these groups read by Jenkins from the LDAP server, but was unsuccessful. Instead, I had to populate the folder-specific groups in Jenkins with user names that were at least still defined in LDAP.  So that was half a win. Still waiting to see if I can define the groups in LDAP, but for now this is a workaround that works for me.

Allowing users to change their LDAP password


This was again a seemingly easy task but turned out to be pretty complicated. I set up another small EC2 instance to act as a jumpbox for users who want to change their LDAP password.

The jumpbox is in the same VPC and subnet as the LDAP server, so I added an /etc/hosts entry on the jumpbox pointing to the FQDN of the LDAP server so it can hit its internal IP address:

IP_ADDRESS_OF_LDAP_SERVER my-ldap-server.mycompany.com

I verified that port 636 (used by LDAPS) on the LDAP server is reachable from the jumpbox:

# telnet my-ldap-server.mycompany.com 636
Trying IP_ADDRESS_OF_LDAP_SERVER...
Connected to my-ldap-server.mycompany.com.
Escape character is '^]'.

# apt-get install ldap-utils

IMPORTANT: Copy over /etc/ssl/certs/ca_server.pem from LDAP server as /etc/ldap/ca_certs.pem on the jumpbox and then:

# vi /etc/ldap/ldap.conf
set:
TLS_CACERT /etc/ldap/ca_certs.pem

Next, I followed this LDAP Client Authentication guide from the Ubuntu documentation.

# apt-get install ldap-auth-client nscd

Here I had to answer the setup questions on LDAP server FQDN, admin DN and password, and bind user DN and password. 

# auth-client-config -t nss -p lac_ldap

I edited /etc/auth-client-config/profile.d/ldap-auth-config and set:

[lac_ldap]
nss_passwd=passwd: ldap files
nss_group=group: ldap files
nss_shadow=shadow: ldap files
nss_netgroup=netgroup: nis

I edited /etc/ldap.conf and made sure the following entries were there:

base dc=mycompany,dc=com
uri ldaps://my-ldap-server.mycompany.com
binddn cn=binduser,mycompany,dc=com
bindpw BINDUSERPASS
rootbinddn cn=admin,mycompany,dc=com
port 636
ssl on
tls_cacertfile /etc/ldap/ca_certs.pem
tls_cacertdir /etc/ssl/certs

I allowed password-based ssh logins to the jumpbox by editing /etc/ssh/sshd_config and setting:

PasswordAuthentication yes

# service ssh restart


IMPORTANT: On the LDAP server, I had to allow users to change their own password by adding this ACL:

# cat set_userpassword_acl.ldif

dn: olcDatabase={1}hdb,cn=config
changetype: modify
add: olcAccess
olcAccess: {0}to attrs=userpassword by dn="cn=admin,dc=mycompany,dc=com" write by self write by anonymous auth by users none

Then:

# ldapmodify -H ldapi:// -Y EXTERNAL -f set_userpassword_acl.ldif


At this point, users were able to log in via ssh to the jumpbox using a pre-set LDAP password, and change their LDAP password by using the regular Unix 'passwd' command.

I am still fine-tuning the LDAP setup on all fronts: LDAP server, LDAP client jumpbox and Jenkis server. The setup I have so far allows me to have a single sign-on account for users to log in to Jenkins. Some of my next steps is to use the same user LDAP accounts  for authentication and access control into MySQL and other services.

Wednesday, April 06, 2016

Joining an EC2 Windows instance to an AWS Directory Service domain

I've been struggling with the existing documentation on how to join an EC2 instance running Windows Server 2012 to an AWS Directory Service domain, so I am hastening to jot down some notes on how I got it to work.

1) Create an AWS Directory Service domain

There is good documentation on doing this. I chose the Microsoft Active Directory option.

A few notes on the creation of an AWS Directory Service:

  • I created a new VPC with 2 subnets for the Directory Service usage
  • I made sure each subnet has an Internet gateway associated so that it can be reachable from the outside
During the creation of the Directory Service, you'll be asked to specify an administrator-type user name and password. Make sure you remember what you specified there because you'll need this info in a subsequent step. Also make note of the DNS server IP addresses that were set during the Directory Service creation.

2) Create an IAM role to be associated with the EC2 Windows instance

  • the IAM role needs to be associated with the AmazonEC2RoleforSSM and AmazonSSMFullAccess policies
  • the IAM role also needs to have a trust relationship with ec2.amazonaws.com

3) Launch EC2 Windows instance associated with the AWS Directory Service domain

I chose the Windows_Server-2012-R2_RTM-English-64Bit-Base-2016.03.09 AMI.  In Step 3 of the AWS launch instance wizard ("Configure instance details") I made sure I specified the following:
  • Network: the VPC created in step 1) above
  • Subnet: one of the 2 subnets created in step 1) above
  • Domain join directory: the directory name for the Directory Service created in step 1) above
  • IAM role: the IAM role created in step 2) above
4) Connect to EC2 Windows instance via RDP

First get the administrator password via the AWS console (you'll need to paste the contents of the private key corresponding to the EC2 key you used when launching the Windows instance). Then connect to the Windows instance as the local administrator user.

Verify that you see the fully qualified domain name of your Directory Service (whatever you indicated in step 1) as the domain of the Windows instance (in Server Manager -> Local Server). If you don't, something went wrong with joining the domain during the instance launch. You can see the system log of that instance in the AWS console by selecting the instance, then going to Actions->Instance Settings->Get System Log. For example, in one of my failed attempts to get all of this working I saw errors related to the IAM role I was using, which at the time didn't have the correct SSM policies attached.

If the Windows instance is correctly joined to the domain, you need to install the Active Directory management tools in order to actually manage the AWS Directory Service. Here is a Rackspace article I found with good instructions.


5) Log in to the EC2 Windows instance as the domain admin to manage AD

After the EC2 Windows instance was rebooted, I managed to log in via RDP as my.aws.directory.fqdn\myusername (where both of these values are the ones chosen in Step 1 above) with the password also chosen in Step 1. At this point I was able to use the Active Directory management tools to add new AD users etc.

Here are some other good resources I found:



Friday, February 12, 2016

Running a static website with Hugo on Google Cloud Storage

I've played a bit with Hugo, the static web site generator written in golang that has been getting a lot of good press lately. At the suggestion of my colleague Warren Runk, I also experimented with hosting the static files generated by Hugo on Google Cloud Storage (GCS). That way there is no need for launching any instances that would serve those files. You can achieve this by using AWS S3 as well of course.

Notes on GCS setup


You first need to sign up for a Google Cloud Platform (GCP) account. You get a 30-day free trial with a new account. Once you are logged into the Google Cloud console, you need to create a new project. Let's call it my-gcs-hugo-project.

You need to also create a bucket in GCS. If you want to serve your site automatically out of this bucket, you need to give the bucket the same name as your site. Let's assume you call the bucket hugotest.mydomain.com. You will have to verify that you own mydomain.com either by creating a special CNAME in the DNS zone file for mydomain.com pointing to google.com, or by adding a special META tag to the HTML file served at hugotest.mydomain.com (you can achieve the latter by temporarily CNAME-ing hugotest to www.mydomain.com and adding the HEAD tag to the home page for www).

If you need to automate deployments to GCS, it's a good idea to create a GCP Service Account. Click on the 'hamburger' menu in the upper left of the GCP console, then go to Permissions, then Service Accounts. Create a new service account and download its private key in JSON format (the key will be called something like my-gcs-hugo-project-a37b5acd7bc5.json.

Let's say your service account is called my-gcp-service-account1. The account will automatically be assigned an email address similar to my-gcp-service-account1@my-gcs-hugo-project.iam.gserviceaccount.com.

I wanted to be able to deploy the static files generated by Hugo to GCS using Jenkins. So I followed these steps on the Jenkins server as the user running the Jenkins process (user jenkins in my case):

1) Installed the Google Cloud SDK



$ wget https://dl.google.com/dl/cloudsdk/channels/rapid/google-cloud-sdk.tar.gz
$ tar xvfz google-cloud-sdk.tar.gz
$ cd google-cloud-sdk/
$ ./install.sh

- source .bashrc

$ which gcloud
/var/lib/jenkins/google-cloud-sdk/bin/gcloud


2) Copied the service account's private key my-gcs-hugo-project-a37b5acd7bc5.json to the .ssh directory of the jenkins user.

3) Activated the service account using the gcloud command-line utility (still as user jenkins)

$ gcloud auth activate-service-account --key-file .ssh/my-gcs-hugo-project-a37b5acd7bc5.json
Activated service account credentials for: [my-gcp-service-account1@my-gcs-hugo-project.iam.gserviceaccount.com]

4) Set the current GCP project to my-gcs-hugo-project


$ gcloud config set project my-gcs-hugo-project

$ gcloud config list
Your active configuration is: [default]

[core]
account = my-gcp-service-account1@my-gcs-hugo-project.iam.gserviceaccount.com
disable_usage_reporting = True

project = my-gcs-hugo-project

5) Configured GCS via the gsutil command-line utility (this may actually be redundant since we already configured the project with gcloud, but I leave it here in case you encounter issues with using just gcloud)

$ gsutil config -e
It looks like you are trying to run "/var/lib/jenkins/google-cloud-sdk/bin/bootstrapping/gsutil.py config".
The "config" command is no longer needed with the Cloud SDK.
To authenticate, run: gcloud auth login
Really run this command? (y/N) y
Backing up existing config file "/var/lib/jenkins/.boto" to "/var/lib/jenkins/.boto.bak"...
This command will create a boto config file at /var/lib/jenkins/.boto
containing your credentials, based on your responses to the following
questions.
What is the full path to your private key file? /var/lib/jenkins/.ssh/my-gcs-hugo-project-a37b5acd7bc5.json

Please navigate your browser to https://cloud.google.com/console#/project,
then find the project you will use, and copy the Project ID string from the
second column. Older projects do not have Project ID strings. For such projects,
click the project and then copy the Project Number listed under that project.

What is your project-id? my-gcs-hugo-project

Boto config file "/var/lib/jenkins/.boto" created. If you need to use
a proxy to access the Internet please see the instructions in that

file.

6) Added the service account created above as an Owner for the bucket hugotest.mydomain.com

7) Copied a test file from the local file system of the Jenkins server to the bucket hugotest.mydomain.com  (still logged in as user jenkins), then listed all files in the bucket, then removed the test file

$ gsutil cp test.go gs://hugotest.mydomain.com/
Copying file://test.go [Content-Type=application/octet-stream]...
Uploading   gs://hugotest.mydomain.com/test.go:             951 B/951 B

$ gsutil ls gs://hugotest.mydomain.com/
gs://hugotest.mydomain.com/test.go

$ gsutil rm gs://hugotest.mydomain.com/test.go
Removing gs://hugotest.mydomain.com/test.go...

8) Created a Jenkins job for uploading all static files for a given website to GCS

Assuming all these static files are checked in to GitHub, the Jenkins job will first check them out, then do something like this (where TARGET is the value selected from a Jenkins multiple-choice dropdown for this job):

BUCKETNAME=$TARGET

# upload all filee and disable caching (for testing purposes)
gsutil -h "Cache-Control:private" cp -r * gs://$BUCKETNAME/

# set read permissions for allUsers
for file in `find . -type f`; do
    # remove first dot from file name
    file=${file#"."}
    gsutil acl ch -u allUsers:R gs://${BUCKETNAME}${file}
done

The first gsutil command does a recursive copy (cp -r *) of all files to the bucket. This will preserve the directory structure of the website. For testing purposes, the gsutil command also sets the Cache-Control header on all files to private, which tells browsers not to cache the files.

The second gsutil command is executed for each object in the bucket, and it sets the ACL on that object so that the object has Read (R) permissions for allUsers (by default only owners and other specifically assigned users have Read permissions). This is because we want to serve a public website out of our GCS bucket.

At this point, you should be able to hit hugotest.mydomain.com in a browser and see your static site in all its glory.

Notes on Hugo setup


I've only dabbled in Hugo in the last couple of weeks, so these are very introductory-type notes.

Installing Hugo on OSX and creating a new Hugo site

$ brew update && brew install hugo
$ mkdir hugo-sites
$ cd hugo-sites
$ hugo new site hugotest.mydomain.com
$ git clone --recursive https://github.com/spf13/hugoThemes themes
$ cd hugotest.mydomain.com
$ ln -s ../themes .

At this point you have a skeleton directory structure created by Hugo (via the hugo new site command) under the directory hugotest.mydomain.com:

$ ls
archetypes  config.toml content     data        layouts     static themes

(note that we symlinked the themes directory into the hugotest.mydomain.com directory to avoid duplication)

Configuring your Hugo site and choosing a theme

One file you will need to pay a lot of attention to is the site configuration file config.toml. The default content of this file is deceptively simple:

$ cat config.toml
baseurl = "http://replace-this-with-your-hugo-site.com/"
languageCode = "en-us"
title = "My New Hugo Site"

Before you do anything more, you need to decide on a theme for your site. Browse the Hugo Themes page and find something you like. Let's assume you choose the Casper theme. You will need to become familiar with the customizations that the theme offers. Here are some customizations I made in config.toml, going by the examples on the Casper theme web page:

$ cat config.toml
baseurl = "http://hugotest.mydomain.com/"
languageCode = "en-us"
title = "My Speedy Test Site"
newContentEditor = "vim"

theme = "casper"
canonifyurls = true

[params]
  description = "Serving static sites at the speed of light"
  cover = "images/header.jpg"
  logo = "images/mylogo.png"
  # set true if you are not proud of using Hugo (true will hide the footer note "Proudly published with HUGO.....")
  hideHUGOSupport = false

#  author = "Valère JEANTET"
#  authorlocation = "Paris, France"
#  authorwebsite = "http://vjeantet.fr"
#  bio= "my bio"
#  googleAnalyticsUserID = "UA-79101-12"
#  # Optional RSS-Link, if not provided it defaults to the standard index.xml
#  RSSLink = "http://feeds.feedburner.com/..."
#  githubName = "vjeantet"
#  twitterName = "vjeantet"
  # facebookName = ""
  # linkedinName = ""

I left most of the Casper-specific options commented out and only specified a cover image, a logo and a description. 

Creating a new page

If you want blog-style posts to appear on your home page, create a new page with Hugo under a directory called post (some themes want this directory to be named post and others want it posts, so check what the theme expects). 

Let's assume you want to create a page caled hello-world.md (I haven't even mentioned this so far, but Hugo deals by default with Markdown pages, so you will need to brush up a bit on our Markdown skills). You would run:

$ hugo new post/hello-world.md

This creates the post directory under the content directory, creates a file called hello-world.md in content/post, and opens up the file for editing in the editor you specified as the value for newContentEditor in config.toml (vim in my case). The default contents of the md file are specific to the theme you used. For Casper, here is what I get by default:

+++
author = ""
comments = true
date = "2016-02-12T11:54:32-08:00"
draft = false
image = ""
menu = ""
share = true
slug = "post-title"
tags = ["tag1", "tag2"]
title = "hello world"

+++


Now add some content to that file and save it. Note that the draft property is set to false by the Casper theme. Other themes set it to true, in which case it would not be published by Hugo by default. The slug property is set by Casper to "post-title" by default. I changed it to "hello-world". I also changed the tags list to only contain one tag I called "blog".

At this point, you can run the hugo command by itself, and it will take the files it finds under content, static, and its other subdirectories, turn them into html/js/css/font files and save it in a directory called public:

$ hugo
0 draft content
0 future content
1 pages created
3 paginator pages created
1 tags created
0 categories created
in 55 ms

$ find public
public
public/404.html
public/css
public/css/nav.css
public/css/screen.css
public/fonts
public/fonts/example.html
public/fonts/genericons.css
public/fonts/Genericons.eot
public/fonts/Genericons.svg
public/fonts/Genericons.ttf
public/fonts/Genericons.woff
public/index.html
public/index.xml
public/js
public/js/index.js
public/js/jquery.fitvids.js
public/js/jquery.js
public/page
public/page/1
public/page/1/index.html
public/post
public/post/hello-world
public/post/hello-world/index.html
public/post/index.html
public/post/index.xml
public/post/page
public/post/page/1
public/post/page/1/index.html
public/sitemap.xml
public/tags
public/tags/blog
public/tags/blog/index.html
public/tags/blog/index.xml
public/tags/blog/page
public/tags/blog/page/1
public/tags/blog/page/1/index.html

That's quite a number of files and directories created by hugo. Most of it is boilerplate coming from the theme. Our hello-world.md file was turned into a directory called hello-world under public/post, with an index.html file dropped in it. Note that the Casper theme names the hello-world directory after the slug property in the hello-world.md file.

Serving the site locally with Hugo

Hugo makes it very easy to check your site locally. Just run

$ hugo server
0 draft content
0 future content
1 pages created
3 paginator pages created
1 tags created
0 categories created
in 35 ms
Watching for changes in /Users/grig.gheorghiu/mycode/hugo-sites/hugotest.mydomain.com/{data,content,layouts,static,themes}
Serving pages from memory
Web Server is available at http://localhost:1313/ (bind address 127.0.0.1)
Press Ctrl+C to stop


Now if you browse to http://localhost:1313 you should see something similar to this:


Not bad for a few minutes of work.

For other types of content, such as static pages not displayed on the home page, you can create Markdown files in a pages directory:

$ hugo new pages/static1.md
+++
author = ""
comments = true
date = "2016-02-12T12:24:26-08:00"
draft = false
image = ""
menu = "main"
share = true
slug = "static1"
tags = ["tag1", "tag2"]
title = "static1"

+++

Static page 1.

Note that the menu property value is "main" in this case. This tells the Casper theme to create a link to this page in the main drop-down menu available on the home page.

If you run hugo server again, you should see something the menu available in the upper right corner, and a link to static1 when you click on the menu:




To deploy your site to GCS, S3 or regular servers, you need to upload the files and directories under the public directory. It's that simple.

I'll stop here with my Hugo notes. DigitalOcean has a great tutorial on installing and running Hugo on Ubuntu 14.04.




Modifying EC2 security groups via AWS Lambda functions

One task that comes up again and again is adding, removing or updating source CIDR blocks in various security groups in an EC2 infrastructur...