Category Archives: programming

Let’s Encrypt using Acme.sh on Centos and Apache

Recently I installed Let’s Encrypt, the free, automated, and open Certificate Authority to websites:

  • brifishjones.com (this website)
  • jenfishjones.com (my wife’s website featuring her paintings)
  • big-dogs-large-stories.com (my wife’s latest artistic collaboration with dog owners)
  • rubycms.org (a content management system I developed over 10 years ago using Ruby on Rails)

I wanted a solution that was written in Shell with no dependencies on Python to eliminate any additional installs, so Acme.sh was chosen. There were a couple of pain points along the way to get https functional so I thought I’d share those with you. Note that this tutorial applies to the following setup:

Begin by logging in to your server as root (or as a user with sudo privileges). Download Acme.sh into your home directory:

# curl https://get.acme.sh | sh

This will create a hidden folder called .acme.sh in your home directory that will contain all of the files, certificates, and keys needed for certification.

Next issue the certificates for each site. Replace /path/to/your/webroot with your actual path. For example:

 # acme.sh --issue -d jenfishjones.com -d www.jenfishjones.com -w /path/to/your/webroot/jenfishjones.com/public_html

Install the certificates (you will need to be root or have sudo privileges):

# acme.sh --install-cert -d jenfishjones.com \
--cert-file /path/to/your/webroot/jenfishjones.com/cert.pem  \
--key-file /path/to/your/webroot/jenfishjones.com/key.pem \
--fullchain-file /path/to/your/webroot/jenfishjones.com/fullchain.pem \
--reloadcmd "service httpd force-reload"

Repeat the installation process for any additional domains then restart Apache:

# service httpd restart

There was a problem restarting Apache.

Starting httpd:
skipping
(98)Address already in use: make_sock: could not bind to address 0.0.0.0:80
no listening sockets available, shutting down
Unable to open logs
[FAILED]

To remedy the problem the old process had to be killed manually (11188 is the process id, yours will be different):

# fuser 80/tcp
80/tcp:              11188
# kill 11188

It is assumed that you already have a functioning Apache server with a secure VirtualHost directive <VirtualHost xx.xx.xxx.xxx:443> already in place in your conf file. For each domain modify /etc/httpd/conf/httpd.conf to include the new certs. Place them within the <VirtualHost xx.xx.xxx.xxx:443> declaration for each ServerName. Note that your conf file may be at a different location:

SSLEngine on
SSLProtocol all -SSLv2 -SSLv3
SSLCertificateFile /path/to/your/webroot/jenfishjones.com/cert.pem
SSLCertificateKeyFile /path/to/your/webroot/jenfishjones.com/key.pem
SSLCertificateChainFile /path/to/your/webroot/jenfishjones.com/fullchain.pem
SSLCACertificateFile /path/to/your/webroot/jenfishjones.com/fullchain.pem

With the certificates added to the httpd.conf file and the server restarted all of the sites served up https except brifishjones.com. Since the server itself had the same name as the website, an outdated certificate at /etc/pki/tls/certs/localhost.crt was being read instead. This was deduced by running the following commands:

# echo | openssl s_client -connect brifishjones.com:443 -servername brifishjones.com 2>/dev/null | awk '/Certificate chain/,/---/'

and:

# grep -r SSLCertificateFile /etc/httpd
/etc/httpd/conf.d/ssl.conf:# Point SSLCertificateFile at a PEM encoded certificate.  If
/etc/httpd/conf.d/ssl.conf:SSLCertificateFile /etc/pki/tls/certs/localhost.crt
/etc/httpd/conf.d/ssl.conf:#   the referenced file can be the same as SSLCertificateFile

To fix the file /etc/httpd/conf.d/ssl.conf was changed so that the Let’s Encrypt generated certificates would be read instead:

# Server Certificate:
# Point SSLCertificateFile at a PEM encoded certificate. If
# the certificate is encrypted, then you will be prompted for a
# pass phrase. Note that a kill -HUP will prompt again. A new
# certificate can be generated using the genkey(1) command.
#SSLCertificateFile /etc/pki/tls/certs/localhost.crt
SSLCertificateFile /path/to/your/webroot/brifishjones.com/cert.pem
# Server Private Key:
# If the key is not combined with the certificate, use this
# directive to point at the key file. Keep in mind that if
# you've both a RSA and a DSA private key you can configure
# both in parallel (to also allow the use of DSA ciphers, etc.)
#SSLCertificateKeyFile /etc/pki/tls/private/localhost.key
SSLCertificateKeyFile /path/to/your/webroot/brifishjones.com/key.pem
# Server Certificate Chain:
# Point SSLCertificateChainFile at a file containing the
# concatenation of PEM encoded CA certificates which form the
# certificate chain for the server certificate. Alternatively
# the referenced file can be the same as SSLCertificateFile
# when the CA certificates are directly appended to the server
# certificate for convinience.
#SSLCertificateChainFile /etc/pki/tls/certs/server-chain.crt
SSLCertificateChainFile /path/to/your/webroot/brifishjones.com/fullchain.pem
# Certificate Authority (CA):
# Set the CA certificate verification path where to find CA
# certificates for client authentication or alternatively one
# huge file containing all of them (file must be PEM encoded)
#SSLCACertificateFile /etc/pki/tls/certs/ca-bundle.crt
SSLCACertificateFile /path/to/your/webroot/brifishjones.com/fullchain.pem

Once the certificate chain was changed to /path/to/your/webroot/brifishjones.com and the Apache server restarted, all sites were up and running with the Let’s Encrypt certificates.

To wrap up there are a couple of items to note. Acme.sh installs a cron job that keeps the certificates up-to-date. You should see a listing like:

# crontab -l
0 0 * * * "/root/.acme.sh"/acme.sh --cron --home "/root/.acme.sh" > /dev/null

If you want to force a manual renewal issue the command:

# acme.sh --renew -d jenfishjones.com --force

Periodically Acme.sh should be updated to the latest version:

# acme.sh --upgrade

Or simply enable auto upgrade and updates will happen automatically:

acme.sh --upgrade --auto-upgrade

As we have now seen, Let’s Encrypt using Acme.sh is a relatively straightforward way to generate signed certificates so that users can visit your websites securely using https.

Giving Day with Socket.IO

Introduction

In recent years college campuses have added Giving Day to entice donors to contribute to their alma maters, and Luther College is no exception. The first online campaign was in 2016, and the main goal that year was to integrate it within the Reason CMS. An improperly coded challenge that gave a higher total pledge was the only glitch, blamed on a mischievous gnome, and corrected quickly behind the scene.

But the application had one serious flaw—users had to refresh their browsers in order to get the latest running pledge totals. Instead what was needed was a secure websocket solution that pushed the information to the browser.

Reason, like many open-source content management products, runs on the LAMP platform. There was already a signed certificate issued by a third party certificate authority on our domain, but I was unable to get the PHP solutions available for websocket servers to work with secure connections. So I looked at socket.io, written in Javascript, that runs on node.js, and was able to construct a websocket server that allows pages served by https to connect.

The state diagram below shows how the socket.io server works with the rest of the Giving Day process:

giving-day-diagram

During the 18-hour window of Giving Day, when a person visits https://www.luther.edu/giving-day

  • the status of each challenge is calculated along with the current total raised
  • challenge totals and the total raised are written in JSON to a temporary file, which will be polled for changes every 15 seconds by the socket.io server
  • and finally a request is made as a socket.io client to securely connect to the socket.io server, ready to get new totals as they become available

The same temporary JSON files are also updated when a pledge form is submitted by calling the php function write_totals():

function write_totals()
// write updated totals to file so that socket io server can read files and broadcast updates to all connected clients
{
	file_put_contents("/var/tmp/giving-day-challenge-totals.txt", json_encode($this->challenge));
	file_put_contents("/var/tmp/giving-day-total.txt", $this->total_amount);
}

Installation and Implementation

If you want to try out a complete solution of this project on a virtual box of your own, please visit https://github.com/brifishjones/giving-day-socketio-server.

Ubuntu by default installs a very old version of nodejs, so get the lastest version instead:

curl -sL https://deb.nodesource.com/setup_8.x | sudo -E bash -

See https://stackoverflow.com/questions/26020917/what-is-the-function-of-etc-apt-sources-list-d and https://stackoverflow.com/questions/28381359/probably-incorrect-release-file-causing-apt-get-update-to-fail if there are errors running the above command.

sudo apt-get install -y nodejs build-essential
nodejs --version
v8.9.4
npm --version
5.6.0

Create a directory for the socket.io server and create a new project:

sudo mkdir /etc/nodejs/giving-day
sudo cd /etc/nodejs/giving-day
sudo npm init

Name the package giving-day, and the entry point server.js then install the socket.io module and add it to package.json

sudo npm install --save socket.io

Create the server.js file:

#!/usr/bin/env node
'use strict';
const fs = require('fs');
const https = require('https');
const port = 4443;
const options = {
    key: fs.readFileSync('/etc/apache2/ssl/wildcard.key'),
    cert: fs.readFileSync('/etc/apache2/ssl/wildcard.crt'),
    ca: fs.readFileSync('/etc/apache2/ssl/gd_bundle-g2-g1.crt'), 
    requestCert: true,
    rejectUnauthorized: false 
};
var connections = [];
var app = https.createServer(options); 
var io = require('socket.io')(app);
var prev_total = 0;
var total = 0;
app.listen(port);
console.log('server started ...');
var timer = setInterval(function() {
  if (connections.length > 0) {
    fs.readFile('/var/tmp/giving-day-total.txt', 'ascii', function(err, content) {
      total = parseInt(content);
      if (total > 0 && total != prev_total) {
        console.log("%s: total amount = $%s", Date(), total.toString());
        io.sockets.emit('update-totals', {total: total.toString()});  
        fs.readFile('/var/tmp/giving-day-challenge-totals.txt', 'ascii', function(err, content) {
          var myjson = JSON.stringify(content);
          var obj = JSON.parse(content);
          io.sockets.emit('update-challenges', obj);  
        });
      }
      prev_total = total;
    });
  }
}, 15000);
io.on('connection', function (socket) {
  socket.once('disconnect', function() {
    connections.splice(connections.indexOf(socket), 1);
    console.log("%s: client id %s disconnected. (%s sockets remaining)", Date(), socket.id, connections.length);
    socket.disconnect();
  });
  connections.push(socket);
  console.log("%s: client id %s connected at ip %s. (%s sockets connected)", Date(), socket.id, socket.conn.remoteAddress, connections.length);
})

The client-side connection is quite simple since it is only receiving a simple emit from the socket.io server. In the Reason CMS module that displays the giving-day page first reference the socket.io javascript file:



Display the grand total:



Total Raised

$123,456

And the javascript that securely connects to the socket.io server and then updates the total when the socket.io server emits an update-totals message:


var socket = io(connecthost + ':4443', {secure: true});
socket.on('update-totals', function (data) {
	document.getElementsByClassName("money-raised")[0].innerHTML = "$" + data.total.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
});
giving-day-diagram

To make the giving day socket.io server into a service, add the following code to /etc/init/giving-day-socket-server.conf:

 

To start service:

 

To stop service:

 

The socket-server console.log messages will be saved in the log file: /var/log/node.js

The Final Results

On Giving Day the socket.IO server had at its peak about 250 connected clients, which were each updated with challenge progress during the course of the day.

giving-day-challenges

Viewers to the website could also access a live video feed of interviews with faculty, staff, students, and alumni.

giving-day-stream

Or could check out the latest social media posts using Tagboard.

giving-day-social