This project provides a complete guide and configuration scripts for setting up a secure Linux server on AWS EC2 . It walks through every step required to transform a fresh EC2 instance into a production-ready environment capable of hosting web applications.
The configuration covers:
- 🔐 Security Hardening → User management, SSH key authentication, firewall (UFW) setup, and disabling root login.
- ⚙️ Server Setup → Package updates, timezone configuration, Apache2 + WSGI setup, PostgreSQL installation, and Python dependencies.
- 🌐 Web Deployment → Cloning a Flask application, configuring Apache Virtual Hosts for HTTP/HTTPS, enabling SSL, and setting up PostgreSQL for persistence.
- 🛠 Automation & Monitoring → Essential commands, log checks, and configurations to keep the server stable and secure.
This repository is ideal for students, developers, and system administrators who want a ready-made reference for deploying Flask or other Python web apps on a hardened Linux environment with AWS EC2 .
-
Create EC2 instance.
-
Connect to the instance through the AWS EC2 console, or access the default user
ubuntu
locally by using the SSH key (.pem) downloaded from your AWS EC2 account.ssh ubuntu@<public-ip> -i <downloaded-aws-ec2-key.pem> -p 22
sudo apt update
sudo apt full-upgrade -y
sudo apt autoremove -y
⚡ Breakdown:
apt update
→ refresh package indexapt full-upgrade -y
→ upgrade all packages with dependency handlingapt autoremove -y
→ remove unneeded packages
sudo reboot
reboot
→ restart the system to apply updates (like kernel upgrades)
sudo adduser <user-name>
-
Create a file in the given directory with the user name. You can create and edit the file in one go with
nano
:sudo nano /etc/sudoers.d/<user-name>
-
Type:
<user-name> ALL=(ALL) NOPASSWD:ALL
ctrl-o
to save.ctrl-x
to exit.
sudo su <user-name>
-
Generate an SSH Key on local Machine
ssh-keygen -t rsa -b 4096 -C <your_email@example.com>
Note the filename and file location used (I used the default that was created at .ssh/id_rsa). When prompted, create a secure passphrase for your SSH key (do not share or document your passphrase).
mkdir -p ~/.ssh
nano ~/.ssh/authorized_keys
⚡ Breakdown:
mkdir -p ~/.ssh
→ creates the .ssh directory if it doesn’t already exist (no need for sudo if you’re logged in as the new user).nano ~/.ssh/authorized_keys
→ directly creates/edits the file.
Copy public key from local machine (.ssh/id_rsa.pub) and paste into .ssh/authorized_keys file on remote machine server.
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys
sudo chown -R <user-name>:<user-name> .ssh
⚡ Breakdown:
sudo
→ run the command as superuser (root), needed if the current user doesn’t own the directory.chown
→ change ownership of files or directories.-R
→ recursive option, meaning apply the change to the directory (.ssh
) and everything inside it.<user-name>:<user-name>
→- first
<user-name>
= user who should own the files - second
<user-name>
= group that should own the files (often same as username)
- first
.ssh
→ the target directory (inside the current user’s home directory, typically~/.ssh
).
sudo service ssh restart
-
Login as new user on local machine terminal
ssh <user-name>@<public-ip> -i <generated-private-key-on-local-machine> -p 22
sudo nano /etc/ssh/sshd_config
- Change:
PasswordAuthentication
tono
.ctrl-o
to save.ctrl-x
to exit.
-
Enter the following commands to configure defaults:
sudo ufw default deny incoming sudo ufw default allow outgoing
-
Enter the following to allow/deny only specified ports:
sudo ufw allow ssh sudo ufw allow 2200/tcp sudo ufw allow 80/tcp sudo ufw allow 123/udp sudo ufw allow 443/tcp sudo ufw deny 22/tcp
-
Before enable Firewall make sure port
22
is disabled:sudo nano /etc/ssh/sshd_config
-
Open editor and change port number from
22
to2200
, setPermitRootLogin
tono
.sudo ufw enable sudo service ufw restart sudo ufw status
Note: If using Amazon EC2, you must configure both the AWS Security Groups (in the AWS console) and the server's UFW firewall to allow the same ports (e.g., 2200, 80, 443, 123) for proper connectivity.
-
Open linux time zone configuration:
sudo dpkg-reconfigure tzdata
- Navigate and Select
None of the above
- Navigate and Select
UTC
- Navigate and Select
sudo apt install -y git
sudo apt install -y python3-pip
sudo apt install -y apache2
sudo apt install -y libapache2-mod-wsgi-py3
sudo apt install -y postgresql
sudo apt install -y postgresql-contrib
📂 Step 16: Clone the Application Repository & Install Python dependencies
-
Change the directory.
cd /var/www
-
Inside that directory run:
sudo git clone https://github.com/fix8developer/udacity-buid-an-item-catalog-application.git catalog
catalog
is the "project-name".
-
Get inside the clone repository.
cd /var/www/catalog
-
Set Owner and Group to New User
sudo chown -R <user-name>:<user-name> /var/www/catalog
-
Install Python dependencies
# Navigate to the project directory cd /var/www/catalog # Create and activate a Python virtual environment [catalog-venv] python3 -m venv catalog-venv source catalog-venv/bin/activate # Upgrade pip and install required dependencies pip install --upgrade pip pip install flask SQLAlchemy oauth2client passlib requests psycopg2-binary # Deactivate the virtual environment deactivate
OPTIONAL: Remove only pip-installed packages
# Only show your user-installed packages (doesn’t show system or venv) pip3 list --user # Remove user-installed packages pip3 freeze --user | xargs pip3 uninstall -y
If the application was cloned from (build-an-item-catalog-application), the following modifications are required:
nano /var/www/catalog/project.py
-
Edit the project.py file and move the app.secret_key out of ...
if __name__ == '__main__': app.secret_key = 'super_secret_key' app.run()
-- by moving it to the following line:
app = Flask(__name__) app.secret_key = 'super_secret_key'
-
Also update the path to client_secrets.json in
project.py
to use the absolute file path (e.g.,/var/www/catalog/client_secrets.json
), since the working directory on the remote machine is different from your local machine.CLIENT_ID = json.loads( open('client_secrets.json', 'r').read())['web']['client_id']
-- to this form:
CLIENT_ID = json.loads( open('/var/www/catalog/client_secrets.json', 'r').read())['web']['client_id']
Edit project.py
and database_setup.py
in clone repository to use postgresql database instead of sqlite
# engine = create_engine('sqlite:///catalog.db')
engine = create_engine(
'postgresql+psycopg2://catalog:catalog@localhost/catalog')
-
Connect to PostgreSQL as the Default Superuser
sudo -u postgres psql postgres
-
Create database user
"catalog"
CREATE DATABASE catalog; CREATE USER catalog; ALTER ROLE catalog with PASSWORD 'catalog'; GRANT ALL PRIVILEGES ON DATABASE catalog TO catalog; \q
-
Create Database Tables and Populate with Sample Data
# Navigate to the project directory cd /var/www/catalog # Activate the Python virtual environment source catalog-venv/bin/activate # Run script to create database tables python3 database_setup.py # Populate the database with sample dat python3 lotsofitems.py # Exit the virtual environment deactivate
-
Create new project-name.wsgi file inside the downloaded repository which will serve my flask application.
sudo nano /var/www/catalog/catalog.wsgi
You can create and edit the file in one go with
nano
. -
Add the following content:
import sys import logging logging.basicConfig(stream=sys.stderr) # Add your project directory to sys.path sys.path.insert(0, "/var/www/catalog") # Import your Flask app from project import app as application
The phrase
from project
refers to the name of main Python file, which in my case isproject.py
.
DuckDNS gives you a free subdomain (something.duckdns.org
).
Steps:
- Go to
DuckDNS.org
- Sign in (GitHub, Google, etc.)
- Create a subdomain → e.g.,
pythonflask.duckdns.org
-
Create and open the Apache configuration file
sudo nano /etc/apache2/sites-available/catalog.conf
-
Add the following content:
<VirtualHost *:80> ServerName pythonflask.duckdns.org ServerAlias www.pythonflask.duckdns.org ServerAdmin email@address.com WSGIDaemonProcess catalog_http python-home=/var/www/catalog/catalog-venv python-path=/var/www/catalog WSGIProcessGroup catalog_http WSGIScriptAlias / /var/www/catalog/catalog.wsgi <Directory /var/www/catalog> Options -Indexes +FollowSymLinks AllowOverride None Require all granted WSGIApplicationGroup %{GLOBAL} </Directory> ErrorLog ${APACHE_LOG_DIR}/catalog_error.log CustomLog ${APACHE_LOG_DIR}/catalog_access.log combined </VirtualHost>
ctrl-o
to save.ctrl-x
to exit.
⚡Breakdown:
Options -Indexes +FollowSymLinks
# Security (no directory listing, allow symlinks)AllowOverride None
# Ignores .htaccess (faster, safer)Require all granted
# Lets Apache serve your appWSGIApplicationGroup %{GLOBAL}
# Keeps Python isolated and stable
-
✅ Check configuration syntax errors
sudo apache2ctl configtest
-
Open the existing Apache configuration file
sudo nano /etc/apache2/sites-available/catalog.conf
-
Add the following content:
<VirtualHost *:80> ServerName pythonflask.duckdns.org ServerAlias www.pythonflask.duckdns.org Redirect permanent / https://pythonflask.duckdns.org/ ServerAdmin email@address.com WSGIDaemonProcess catalog_http python-home=/var/www/catalog/catalog-venv python-path=/var/www/catalog WSGIProcessGroup catalog_http WSGIScriptAlias / /var/www/catalog/catalog.wsgi <Directory /var/www/catalog> Options -Indexes +FollowSymLinks AllowOverride None Require all granted WSGIApplicationGroup %{GLOBAL} </Directory> ErrorLog ${APACHE_LOG_DIR}/catalog_error.log CustomLog ${APACHE_LOG_DIR}/catalog_access.log combined </VirtualHost>
ctrl-o
to save.ctrl-x
to exit.
-
✅ Check configuration syntax errors
sudo apache2ctl configtest
sudo nano /etc/apache2/sites-available/catalog-le-ssl.conf
-
Add the following content
<VirtualHost *:443> ServerName pythonflask.duckdns.org ServerAlias www.pythonflask.duckdns.org ServerAdmin email@address.com WSGIDaemonProcess catalog_https python-home=/var/www/catalog/catalog-venv python-path=/var/www/catalog WSGIProcessGroup catalog_https WSGIScriptAlias / /var/www/catalog/catalog.wsgi <Directory /var/www/catalog> Options -Indexes +FollowSymLinks AllowOverride None Require all granted WSGIApplicationGroup %{GLOBAL} </Directory> SSLEngine on Include /etc/letsencrypt/options-ssl-apache.conf ErrorLog ${APACHE_LOG_DIR}/catalog_ssl_error.log CustomLog ${APACHE_LOG_DIR}/catalog_ssl_access.log combined SSLCertificateFile /etc/letsencrypt/live/pythonflask.duckdns.org/fullchain.pem SSLCertificateKeyFile /etc/letsencrypt/live/pythonflask.duckdns.org/privkey.pem </VirtualHost>
ctrl-o
to save.ctrl-x
to exit.
-
✅ Check configuration syntax errors
sudo apache2ctl configtest
Disable the default Apache site and enable your flask app.
-
Disable the default configuration file:
sudo a2dissite 000-default.conf
-
Enable the catalog.conf (Flask app configuration for HTTP):
sudo a2enmod wsgi sudo a2ensite catalog.conf
-
Enable the catalog-ssl.conf (Flask app configuration for HTTPS):
sudo a2enmod ssl sudo a2ensite catalog-le-ssl.conf
-
To active the new configuration we need to run:
sudo systemctl reload apache2
-
Double-check Apache's current status
sudo systemctl status apache2
-
Install Certbot to easily obtain and manage SSL certificates.
sudo apt update sudo apt install certbot python3-certbot-apache -y
-
Run Certbot with the Apache plugin:
# sudo certbot --apache -d your_domain.com -d www.your_domain.com sudo certbot --apache -d pythonflask.duckdns.org -d www.pythonflask.duckdns.org
- Follow prompts:
- Enter email (for renewal notices)
- Agree to terms
- Certbot will auto-configure Apache for HTTPS
- Follow prompts:
-
Configure Certbot to automatically renew your SSL certificates. Let’s Encrypt certificates are valid for 90 days. Certbot can auto-renew:
sudo systemctl status certbot.timer
-
Or test renewal manually:
sudo certbot renew --dry-run
To view server side error for HTTP:
sudo tail -n 30 /var/log/apache2/catalog_error.log
To view server side error for HTTPS:
sudo tail -n 30 /var/log/apache2/catalog_ssl_error.log
To view server side error for ALL:
sudo tail /var/log/apache2/error.log
deploy-flask-postgresql-ubuntu-aws-ec2
is Copyright ©️ 2025 Kashif Iqbal. It is free, and may be redistributed under the terms specified in the LICENSE file.