All Thou Needest is Flask, Quoth The Python in Eldritch Speech

The Python is telling the truth

As a first blog post, I could have written some kind of "hello world!" post as is the custom in these Internet lands. However, I would rather write some lines about the technical choices behind this very simple blog.

If this topic doesn’t pique your interest, do feel free to return later — more cyber-ish content is coming in the months ahead.

But, if you have some keen interest for web technologies and hostings puzzles, let me share with you the reasons behind what could be viewed as a curious choice of some sort.

🧙‍♂️ One Ring to bring them

These days, I use the markup language Markdown for everything. Combined with tools such as Obsidian and reveal.js, Markdown forms the foundation of my daily knowledge work and communication.

For long as I can remember, I've told to myself:

I shall not start a new blog unless it uses Markdown — and draws from my Obsidian knowledge base.

And so, I set out to build exactly that.

As an infosec practitioner, I also wanted a site that offered:

  • Strong security guarantees

  • Minimal maintenance costs

That is why I chose a solution based on Markdown and Flask, hosted on AWS.

It's not the cheapest, nor the simplest option — but it meets both my requirements.

💡 Let there be light

To build this very simple blog, I was largely inspired by James Harding's blog.

First and foremost, articles are written in Markdown and stored in a static folder with minimal metadata, as shown below:

title: All thou needest is Flask, quoth the Python in eldritch speech
date: 2025-06-23
summary: This ...

## All thou needest is Flask, quoth the python in eldritch speech

As first blog post

Flask then interprets these files using the flask_flatpages library and generates static HTML files via the flask_frozen library

Below is a snippet of what’s running under the hood:

import sys
from flask import Flask, render_template, redirect, url_for
from flask_flatpages import FlatPages
from flask_frozen import Freezer

DEBUG = True
FLATPAGES_AUTO_RELOAD = DEBUG
FLATPAGES_EXTENSION = '.md'
FLATPAGES_ROOT = 'content'
POST_DIR = 'posts'
RES_DIR = 'posts'
FLATPAGES_MARKDOWN_EXTENSIONS = ['fenced_code', 'codehilite']

minimal = Flask(__name__)
flatpages = FlatPages(minimal)
freezer = Freezer(minimal)
minimal.config.from_object(__name__)

<...>

@minimal.route('/blog/<post>/')
def post(post):
    path = '{}/{}'.format(POST_DIR,post)
    content = flatpages.get_or_404(path)
    return render_template('post.html', post=content)

if __name__ == "__main__":
    if len(sys.argv) > 1 and sys.argv[1] == "build": 
        freezer.freeze()
    else:
        minimal.run(host='127.0.0.1', debug=DEBUG)

The final result is displayed through Jinja2 templates, like the one below:

{% extends 'base.html' %}
        {% block content %}
            <header>
                <nav class=nav role=navigation>
                    <div class=left>
                        <a href='{{ url_for('home') }}' title=name 
                        class=navbar-item><strong> Back to Home</strong></a>
                </nav>
            </header>
            <main>
                {{ post.html|safe }}
            </main>
        {% endblock %}

404 errors are handled by a flask method and displayed accordingly when needed.

As a bonus, you can read if you wish the project's source code on my Github page.

🚀 One man deploys an app securely

To solve the hosting puzzle, I relied on this Github project shared by AWS to streamline the deployment process. In short, the project's authors describe the solution’s inner workings using a simple diagram:

AWS secure static website

  1. The viewer requests the website at www.example.com.

  2. If the requested object is cached, CloudFront returns the object from its cache to the viewer.

  3. If the object is not in the CloudFront cache, CloudFront requests the object from the origin (an S3 bucket).

  4. S3 returns the object to CloudFront.

  5. CloudFront caches the object.

  6. The objects is returned to the viewer. Subsequent requests for the object that come to the same CloudFront edge location are served from the CloudFront cache.

I found three main benefits to this solution:

  • It creates an Amazon S3 bucket to host the static website content. To update the site, I simply upload new files to the bucket.

  • It provisions a CloudFront distribution with Origin Access Control (OAC), ensuring content is accessible only through CloudFront — not directly from S3.

  • It sets up an SSL/TLS certificate via AWS Certificate Manager (ACM), enabling secure HTTPS access to the website.

💻 Playing with the daunting AWS ecosystem

The deployment process is far from straightforward despite the quality of the Amazon Web Service's documentation. However, if you meet the required prerequisites and have a working AWS CLI, it should be a piece of cake - a slightly bitter one, perhaps, but cake nonetheless

  • From your working directory, freeze the Flask website
$ python3 minimal.py build
  • Clone and prepare the Amazon CloudFront solution from the Github project
$ git clone https://github.com/aws-samples/amazon-cloudfront-secure-static-site                         
$ make 
$ package-static                                          
$ make build-static
$ cd                                            
$ cp -r <dev repo> .     
  • Create an S3 bucket
$ aws s3 mb s3://your-domain --region us-east-1 --profile yourprofile
  • Generate a template
$ aws cloudformation package --region us-east-1 --template-file templates/main.yaml 
--s3-bucket your-domain-static --output-template-file packaged.template --profile yourprofile 
  • Deploy the website with CloudFormation
$ aws cloudformation deploy --region us-east-1 --stack-name gkaplan-www-stack --template-file 
packaged.template --capabilities CAPABILITY_NAMED_IAM CAPABILITY_AUTO_EXPAND --parameter-overrides 
DomainName=your-domain SubDomain=www HostedZoneId=your_hosted-id --profile yourprofile

By default, your website may return 403 errors for routes other than your homepage. Let’s fix that.

  • Create a function in Cloufront functions with this code to rewrite request URLs:
    async function handler(event) {
    var request = event.request;
    var uri = request.uri;

    // Check whether the URI is missing a file name.
    if (uri.endsWith('/')) {
        request.uri += 'index.html';
    } 

    return request;
}
  • Add this new function in association with the current Distribution using the Viewer request Event type. The cache behavior can remain by default.

  • Delete the default 404 error page response in the current Distribution error pages configuration

  • Routing 403 errors to 404 page. Change the 403 error page response in the current Distribution error pages, as shown below:

    - Response page path: /404.html
    - HTTP response code: 404

And voilà, your static website is online and client-server communication is now TLS encrypted.