Can I use mTLS to secure home web server? (re: Alexa Flash Briefing and Google Actions APIs)

I’d like to enable Alexa and Google Assistant with my local Home Assistant server. However, Amazon (Alexa) and Google need to send requests to Home Assistant, etc.

I don’t want to expose my local server to the public and it looks like Amazon and Google both support mTLS.

Can I configure Cloudflare to use mTLS and verify it’s Google and/or Amazon before passing the request to my home server?

I did manage to set something like this up today for Alexa.

I set up the Alexa skill for home assistant using instructions on the home assistant site.

The Alexa skill points to a lambda function in AWS which calls my Cloudflare tunnel (which is linked to home assistant). The tunnel has mtls firewall rules set up. The lambda function sends the client cert with the request such that only calls from Alexa get passed through the tunnel. Any other device visiting the url of my tunnel gets an access denied error.

Thanks so much for the information!

Can you post the lambda function you are using?

The base function comes from the HA alexa skill instructions (“\gist.github.com/matt2005/744b5ef548cc13d88d0569eea65f5e5b”), but modified the http requst object to connect with client cert For Cloudflare firewall rules you will need to let the alexa token refresh request through, no mtls for that.

"""
Copyright 2019 Jason Hu <awaregit at gmail.com>

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
import os
import json
import logging
import urllib3
from urllib3.connectionpool import HTTPSConnectionPool

_debug = bool(os.environ.get('DEBUG'))

_logger = logging.getLogger('HomeAssistant-SmartHome')
_logger.setLevel(logging.DEBUG if _debug else logging.INFO)

def lambda_handler(event, context):
    """Handle incoming Alexa directive."""
    
    _logger.debug('Event: %s', event)

    base_url = os.environ.get('BASE_URL')
    base_url_ex = os.environ.get('BASE_URL_EX')
    assert base_url is not None, 'Please set BASE_URL environment variable'
    base_url = base_url.strip("/")
    base_url_ex = base_url_ex.strip("/")

    directive = event.get('directive')
    assert directive is not None, 'Malformatted request - missing directive'
    assert directive.get('header', {}).get('payloadVersion') == '3', \
        'Only support payloadVersion == 3'
    
    scope = directive.get('endpoint', {}).get('scope')
    if scope is None:
        # token is in grantee for Linking directive 
        scope = directive.get('payload', {}).get('grantee')
    if scope is None:
        # token is in payload for Discovery directive 
        scope = directive.get('payload', {}).get('scope')
    assert scope is not None, 'Malformatted request - missing endpoint.scope'
    assert scope.get('type') == 'BearerToken', 'Only support BearerToken'

    token = scope.get('token')
    if token is None and _debug:
        token = os.environ.get('LONG_LIVED_ACCESS_TOKEN')  # only for debug purpose
    
    verify_ssl = not bool(os.environ.get('NOT_VERIFY_SSL'))
    
    try:
        
        http = HTTPSConnectionPool(base_url_ex, cert_file='client.pem', key_file='key.pem', cert_reqs='CERT_REQUIRED')

        #http = urllib3.connection_from_url(
        #    base_url,
        #    cert_file='client.pem',
        #    key_file='key.pem',
        #    cert_reqs='CERT_REQUIRED' if verify_ssl else 'CERT_NONE')

        response = http.request(
            'POST', 
            '{}/api/alexa/smart_home'.format(base_url),
            headers={
                'Authorization': 'Bearer {}'.format(token),
                'Content-Type': 'application/json',
            },
            body=json.dumps(event).encode('utf-8'),
        )
    except Exception as e:
        _logger.info('http failed: %s', e)

    else:
        if response.status >= 400:
            _logger.debug('Request Error: %s', response.data.decode("utf-8"))
            return {
                'event': {
                    'payload': {
                        'type': 'INVALID_AUTHORIZATION_CREDENTIAL' 
                                if response.status in (401, 403) else 'INTERNAL_ERROR',
                        'message': response.data.decode("utf-8"),
                    }
                }
            }
        
        _logger.debug('Response: %s', response.data.decode("utf-8"))
        return json.loads(response.data.decode('utf-8'))