For creating Alexa skills, you have to implement an AWS Lambda function that is invoked for every request a user makes to Alexa (that involves your skill) in order to carry out your intended actions and possibly return a response to the user.

Now the usual »Cloud« scenario is that your Smart home devices connect to the Cloud of the manufacturer and are always connected to their servers. The Lambda function would then talk to an internal service of the manufacturer in order to relay your Alexa requests to your devices.

I don’t want that. :scream:

If you want to control your Smart Home devices locally but still use Alexa for voice control, you need get the Alexa requests that go to your skills’ Lambda function into your local network – either process in Lambda before triggering actions or just pass them to your own »Alexa service« in your home network that does that for you.

There are multiple ways to achieve that:

  • Open your Smart Home controller services up to the internet (with authentication, of course) in your home router. This would probably the easiest solution, but you would have to that for every service you want to control through Alexa. Also you would implement all the control logic in Lambda – which can be annoying to debug sometimes.
  • Create a full-scale VPC with a VPN gateway that connects to your home network. Overkill. You would also pay for an EC2 instance that acts merely as a proxy. Now you can forward your requests to your in-home »Alexa service« or process them in Lambda and directly trigger the corresponding actions.
  • Pipe all Alexa requests through your home router to an HTTP proxy on, e.g., a Raspberry Pi. This proxy would then authenticate and forward incoming Alexa requests to your local »Alexa service«.

I decided for the last approach.

Setting up nginx on a Raspberry Pi

For the following example I’m using Arch Linux ARM. For other distros, please adapt any paths as needed.

$ sudo pacman -S nginx certbot-nginx

Generate Let’s Encrypt certificates for your proxy

Make sure your router forwards ports 80 and 443 to your Raspberry Pi.

$ sudo certbot --nginx certonly

This will generate valid certificates in /etc/letsencrypt.

We can create a directory in /etc/nginx/certs to have easier access to them:

$ mkdir -p /etc/nginx/certs
$ ln -sf /etc/letsencrypt/live/<your domain> /etc/nginx/certs/

Generating a CA and client certificates

Your Lambda function will use a client certificate to authenticate itself against your »Alexa service«.

First we create the CA that signs those certificates:

$ cd /etc/nginx/certs
$ openssl genpkey -out client-ca.key 2048
$ openssl req -new -x509 -years 100 -key client-ca.key -out client-ca.pem

Then we create a client ceriticate for our Lambda function:

$ mkdir -p clients
$ cd clients
$ openssl genrsa -out aws-lambda.key 2048
$ openssl req -new -key aws-lambda.key -out aws-lambda.csr
$ openssl x509 -req -days 365 \
    -in aws-lambda.csr \
    -CA ../client-ca.pem -CAkey ../client-ca.key \
    -set_serial 01 -out aws-lambda.pem

Configure nginx

Add the following to the http section of your nginx.conf:

server {
    listen       443 ssl;
    server_name  <your domain>;

    # Let's Encrypt server certificate
    ssl_certificate         /etc/nginx/certs/<your domain>/fullchain.pem;
    ssl_trusted_certificate /etc/nginx/certs/<your domain>/chain.pem;
    ssl_certificate_key     /etc/nginx/certs/<your domain>/privkey.pem;

    ssl_session_cache    shared:SSL:1m;
    ssl_session_timeout  5m;

    ssl_ciphers               HIGH:!aNULL:!MD5; # please don't
    ssl_prefer_server_ciphers on;

    # your own client ceritificate CA
    ssl_client_certificate /etc/nginx/certs/client-ca.pem;
    ssl_verify_client      on;

    location / {
        proxy_pass       http://localhost:4567/;
        proxy_set_header X-ClientCert-DN $ssl_client_s_dn;
    }
}

Now start your service on port 4567 on your Raspberry Pi. For example, using a minimal Sinatra app:

$ gem install sinatra

main.rb:

require 'sinatra'

accepted_client_dns = [
  # insert certificate DNs here if you want to differentiate / identify different client certificates
]

def proccess_alexa_request(request)
  # ...
end

post '/transport' do
  # uncomment to enable DN validation:
  #raise unless accepted_client_dns.include? request.env['HTTP_X_CLIENTCERT_DN']

  request.body.rewind
  request_body = JSON.parse(request.body.read)
  
  response_body = process_alexa_request(request_body)
  
  JSON.generate(response_body)
end
$ ruby main.rb

Creating a Lambda function that redirects your Alexa requests

This one is simple. Just create a Lambda function running in the NodeJS environment.

index.js:

'use strict';

var fs    = require('fs'); 
var https = require('https'); 

var options = { 
  hostname: '<your domain>',
  port:     443,
  path:     '/transport',
  method:   'POST',
  key:      fs.readFileSync('aws-lambda.key'),
  cert:     fs.readFileSync('aws-lambda.pem')
}; 

// this function will be invoked by Alexa when a user makes a request involving your skill
exports.handler = (request, context, callback) => {
  var request_json = JSON.stringify(request);
  console.log(`Request: ${request_json}`);
  
  // send request to your local »Alexa service« and wait for a response
  var response_json = '';
  var req           = https.request(options, function(res) { 
    res.on('data', function(data) {
      response_json += data;
    });
    res.on('end', function() {
      console.log(`Response: ${response_json}`);
      var response = JSON.parse(response_json);
      
      // pass the response back to Alexa
      callback(null, response);
    });
  });
  
  req.on('error', function(e) {
    // oh noes 
    console.error(e);
    callback(new Error(e));
  });
  
  req.write(request_json)
  req.end();
};

Now add your previously generated aws-lambda.key and aws-lambda.pem client key and certificate files to the same folder as index.js.

There you go. Your requests will start filing into your Alexa service ready for being processed by you.