Secure Cloud Deployment and Environment Hardening

Deploying a web application securely to the cloud is a critical skill for any developer looking to build production-ready systems. This chapter guides you through taking your locally containerized, secure Flask application and deploying it to a public-facing cloud environment, focusing on hardening the infrastructure and configuration.

By the end of this milestone, you will have your secure Flask application running on Azure App Service, configured to use a managed PostgreSQL database, with all sensitive configurations handled through environment variables. This practical experience is vital for understanding how to maintain the security posture of an application once it leaves the development environment.

Project Overview: Hardening Cloud Deployments

This chapter focuses on the secure deployment of our Flask application. We will move our application from a local Docker Compose setup to a managed cloud environment, specifically Azure. The core objective is to ensure that the security measures implemented in previous chapters (secure authentication, input validation) are extended to the deployment phase, mitigating risks associated with public exposure and cloud infrastructure.

Specific Security Aspects Addressed:

  • Secure Configuration: Using environment variables for sensitive data instead of hardcoding.
  • Network Isolation: Restricting database access to only authorized application instances.
  • Least Privilege: Running containers as non-root users and granting minimal necessary permissions to cloud resources.
  • Managed Services: Leveraging cloud provider security features for databases and application hosting.
  • Production-Ready Containers: Optimizing Docker images for security and efficiency.

Tech Stack

For this deployment milestone, we will leverage a combination of our existing stack and specific Azure services:

  • Python (v3.13): The primary language for our Flask application.
  • Flask (v3.x): The web framework.
  • PostgreSQL (v16): Our relational database.
  • Docker: For containerizing our application, ensuring consistent environments.
  • Azure CLI: The command-line interface for interacting with Azure services.
  • Azure App Service (Web App for Containers): A fully managed platform for running containerized web applications. Chosen for its ease of deployment, scalability, and integration with other Azure security features.
  • Azure Database for PostgreSQL (Flexible Server): A managed database service that handles patching, backups, and high availability, reducing operational overhead and enhancing security compared to self-hosting.

📌 Key Idea: Using managed cloud services like Azure App Service and Azure Database for PostgreSQL offloads significant operational and security responsibilities to the cloud provider, allowing developers to focus more on application-level security.

Milestones: Cloud Deployment Plan

To ensure a structured and verifiable deployment, we’ll follow these steps:

  1. Prerequisites Setup: Install and configure necessary tools (Azure CLI, Docker).
  2. Container Image Hardening: Optimize the Dockerfile for production, including multi-stage builds and non-root users.
  3. Image Build & Push: Build the production Docker image and push it to a container registry (Docker Hub).
  4. Azure Resource Provisioning: Create an Azure Resource Group, a managed PostgreSQL Flexible Server, and an Azure App Service Plan and Web App.
  5. Secure Configuration: Configure Azure App Service application settings to inject sensitive environment variables into the Flask container.
  6. Application Code Update: Modify the Flask application to read configuration from environment variables.
  7. Network Security Hardening: Restrict PostgreSQL firewall access to only the Azure App Service outbound IPs.
  8. Verification: Test the deployed application’s functionality and inspect security headers.

Planning & Design: Cloud Architecture

Deploying a web application securely to the cloud requires a thoughtful approach to infrastructure, configuration, and secrets management. Our goal is to host our Dockerized Flask application and its PostgreSQL database on Azure, focusing on minimizing attack surface and using platform features for security.

Deployment Architecture Overview

The core idea is to separate our application code from sensitive configurations and data. The Flask application, running in a Docker container, will fetch its database credentials and other secrets from environment variables provided by the App Service. The App Service itself will connect to the managed PostgreSQL instance over a secure channel.

flowchart LR User -->|HTTPS Request| Azure_App_Service[Azure App Service] subgraph App_Service_Container["Web App for Containers"] Flask_App[Flask Application] end Azure_App_Service --> Flask_App Flask_App -->|Secure Connection TLS| Azure_PostgreSQL[Azure Database for PostgreSQL] Azure_PostgreSQL --> Data[Application Data] style Azure_App_Service fill:#f9f,stroke:#333,stroke-width:2px style Azure_PostgreSQL fill:#bbf,stroke:#333,stroke-width:2px

Key Security Principles for Cloud Deployment

  • Principle of Least Privilege: Configure Azure resources with only the necessary permissions. For instance, the App Service will have specific network access to the PostgreSQL database, and the database user will have minimal privileges.
  • Secrets Management: Avoid hardcoding sensitive information. We’ll use Azure App Service’s application settings, which inject values as environment variables into the container. For more advanced scenarios, Azure Key Vault would be integrated using Managed Identities.
  • Network Security: Utilize cloud-native networking features like firewall rules and Virtual Network (VNet) integration to restrict access to our application and database.
  • Container Security: Ensure our Docker image is production-ready, running as a non-root user and minimizing unnecessary components.
  • Defense in Depth: Implement multiple layers of security controls, from the network edge to the application code, to protect against various attack vectors.

Step-by-Step Implementation

This section guides you through preparing your application for cloud deployment and configuring the necessary Azure resources.

1. Prerequisites Setup

Ensure you have the following installed and configured. Always refer to official documentation for the most up-to-date installation instructions.

Authenticate your Azure CLI:

az login

2. Container Image Hardening for Production

We need to optimize our Dockerfile for production environments. This includes using a multi-stage build to reduce image size and running the application as a non-root user to minimize potential container escape vulnerabilities.

File: Dockerfile

# Stage 1: Build dependencies
# Using python:3.13-slim-bookworm, a lightweight base image.
# As of 2026-07-13, Python 3.13 is a reasonable expectation for a recent stable version.
# Always verify the latest stable Python release when building.
FROM python:3.13-slim-bookworm AS builder

# Set environment variables for non-interactive operations and Python specifics
ENV PYTHONUNBUFFERED 1
ENV PYTHONDONTWRITEBYTECODE 1

# Set the working directory inside the container
WORKDIR /app

# Install build dependencies required for psycopg2 (PostgreSQL client)
# --no-install-recommends reduces the number of installed packages
RUN apt-get update && apt-get install -y --no-install-recommends \
    build-essential \
    libpq-dev \
    && rm -rf /var/lib/apt/lists/*

# Copy and install Python dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Stage 2: Create final runtime image
# Use the same slim base image for consistency and minimal footprint
FROM python:3.13-slim-bookworm

# Set the working directory
WORKDIR /app

# Copy only runtime dependencies from the builder stage
# This significantly reduces the final image size and attack surface
COPY --from=builder /usr/local/lib/python3.13/site-packages /usr/local/lib/python3.13/site-packages
COPY --from=builder /usr/local/bin/gunicorn /usr/local/bin/gunicorn

# Copy the application code
COPY . .

# Create a dedicated non-root user for running the application
# This adheres to the principle of least privilege, enhancing security
RUN adduser --system --group appuser
USER appuser

# Expose the port our Flask app will run on (Gunicorn default)
EXPOSE 8000

# Command to run the application using Gunicorn, binding to all interfaces
# Ensure gunicorn is listed in your requirements.txt
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "app:app"]

Explanation of changes:

  • Multi-stage build: Separates build-time dependencies (like build-essential) from runtime dependencies, resulting in a smaller, more secure final image.
  • Lightweight base image: python:3.13-slim-bookworm (or the then-current stable version) is used to minimize the OS footprint.
  • Non-root user: adduser --system --group appuser && USER appuser creates and switches to a dedicated user appuser. This is a critical security practice, as it prevents the application from having root privileges inside the container, limiting damage in case of a compromise.
  • Gunicorn: Used as the production-grade WSGI server. Make sure gunicorn is included in your requirements.txt.
  • Environment variables: PYTHONUNBUFFERED and PYTHONDONTWRITEBYTECODE are set for better logging and performance in containers.

Ensure your requirements.txt includes gunicorn and psycopg2-binary (or psycopg2 if you prefer to build it).

File: requirements.txt (partial example)

Flask==3.0.3
Werkzeug==3.0.1
psycopg2-binary==2.9.9
gunicorn==22.0.0
python-dotenv==1.0.1
... (other dependencies)

As of 2026-07-13, these are hypothetical versions based on current stable releases. Always verify the latest stable versions for your project.

3. Build and Push Your Docker Image

Now, build your production-optimized Docker image and push it to Docker Hub. Replace YOUR_DOCKERHUB_USERNAME and your-secure-flask-app with your actual username and desired image name.

docker build -t YOUR_DOCKERHUB_USERNAME/your-secure-flask-app:latest .
docker push YOUR_DOCKERHUB_USERNAME/your-secure-flask-app:latest

This makes your container image available for Azure App Service to pull.

4. Azure Resource Provisioning

We’ll use the Azure CLI to provision the necessary services.

4.1. Create a Resource Group

A resource group is a logical container for your Azure resources.

RESOURCE_GROUP_NAME="SecureFlaskRG"
LOCATION="eastus" # Choose an Azure region close to you for lower latency

az group create --name $RESOURCE_GROUP_NAME --location $LOCATION

4.2. Create Azure Database for PostgreSQL - Flexible Server

This command sets up a managed PostgreSQL instance. For production, always use a strong, unique password.

POSTGRES_SERVER_NAME="secure-flask-db-$(head /dev/urandom | tr -dc a-z0-9 | head -c 8)" # Unique name
POSTGRES_ADMIN_USER="dbadmin"
POSTGRES_ADMIN_PASSWORD="YourStrongPassword123!" # <--- REPLACE with a STRONG, COMPLEX password!

az postgres flexible-server create \
    --resource-group $RESOURCE_GROUP_NAME \
    --name $POSTGRES_SERVER_NAME \
    --location $LOCATION \
    --version "16" \
    --sku-name Standard_D2ds_v4 \
    --tier Burstable \
    --storage-size 32 \
    --storage-iops 500 \
    --admin-user $POSTGRES_ADMIN_USER \
    --admin-password $POSTGRES_ADMIN_PASSWORD \
    --public-access 0.0.0.0 --action Add # Temporarily enable public access for initial setup. We will restrict this immediately after deployment.

🧠 Important: For production, never use --public-access 0.0.0.0 --action Add as a permanent solution. This exposes your database to the entire internet. We use it here temporarily for initial setup simplicity, but the critical next step is to restrict access. The best practice for production is to use Azure Virtual Network (VNet) integration for your App Service and place the PostgreSQL server within the VNet, or use Private Link for private connectivity.

4.3. Create Azure App Service Plan and Web App

An App Service Plan defines the underlying compute resources. The Web App then runs on this plan.

APP_PLAN_NAME="SecureFlaskPlan"
WEB_APP_NAME="secure-flask-app-$(head /dev/urandom | tr -dc a-z0-9 | head -c 8)" # Unique name
DOCKER_IMAGE="YOUR_DOCKERHUB_USERNAME/your-secure-flask-app:latest"

# Create App Service Plan (Basic tier for tutorial, use Premium V2/V3 for production)
az appservice plan create \
    --resource-group $RESOURCE_GROUP_NAME \
    --name $APP_PLAN_NAME \
    --location $LOCATION \
    --is-linux \
    --sku B1 

# Create Web App for Containers, pulling from your Docker Hub image
az webapp create \
    --resource-group $RESOURCE_GROUP_NAME \
    --plan $APP_PLAN_NAME \
    --name $WEB_APP_NAME \
    --deployment-container-image-name $DOCKER_IMAGE \
    --output json

5. Secure Configuration with Application Settings

This is a critical step for secure configuration. We’ll set environment variables for our Flask application, including the database connection string and SECRET_KEY, using Azure App Service’s application settings. These settings are securely injected into your container at runtime.

First, retrieve the PostgreSQL connection string. You’ll need the server name, admin user, and the strong password you set earlier.

# Get PostgreSQL server hostname
POSTGRES_HOST=$(az postgres flexible-server show --resource-group $RESOURCE_GROUP_NAME --name $POSTGRES_SERVER_NAME --query "fullyQualifiedDomainName" --output tsv)

# Construct the connection string. Replace POSTGRES_ADMIN_PASSWORD with your actual password.
# The default database for Azure Flexible Server is 'postgres'.
DB_CONNECTION_STRING="postgresql://${POSTGRES_ADMIN_USER}:${POSTGRES_ADMIN_PASSWORD}@${POSTGRES_HOST}:5432/postgres"

Now, set the application settings on your Azure Web App.

# Generate a new, strong SECRET_KEY for production. Never hardcode this.
FLASK_SECRET_KEY=$(python -c 'import secrets; print(secrets.token_hex(32))')

az webapp config appsettings set \
    --resource-group $RESOURCE_GROUP_NAME \
    --name $WEB_APP_NAME \
    --settings \
        DATABASE_URL="$DB_CONNECTION_STRING" \
        FLASK_ENV="production" \
        SECRET_KEY="$FLASK_SECRET_KEY" \
        DEBUG="False" \
        PORT="8000" # Important for Gunicorn running on port 8000 inside container

Explanation:

  • DATABASE_URL: Our Flask application will read this to connect to PostgreSQL.
  • FLASK_ENV="production": Ensures Flask runs in production mode.
  • SECRET_KEY: A randomly generated, strong key crucial for session security and other cryptographic operations. Never hardcode this or commit it to version control.
  • DEBUG="False": Disables debug mode in production to prevent sensitive information leaks.
  • PORT="8000": Azure App Service needs to know which port your container is listening on. Since Gunicorn defaults to 8000, we specify this.

6. Update Flask Application to Use Environment Variables

Ensure your Flask application reads these environment variables for configuration. This makes your application adaptable to different environments without code changes.

File: app.py (or config.py if you have one)

import os
from flask import Flask, session, request, jsonify
from datetime import timedelta
from werkzeug.security import generate_password_hash, check_password_hash
import psycopg2
from psycopg2 import extras
from functools import wraps

# --- Configuration using Environment Variables ---
# Use os.getenv() to fetch environment variables.
# Provide insecure default values for local development to prevent accidental production use.
DATABASE_URL = os.getenv("DATABASE_URL", "postgresql://dev_user:dev_pass@localhost:5432/dev_db")
SECRET_KEY = os.getenv("SECRET_KEY", "INSECURE_DEV_KEY_CHANGE_ME_IN_PROD_IMMEDIATELY")
FLASK_ENV = os.getenv("FLASK_ENV", "development")
DEBUG_MODE = os.getenv("DEBUG", "True").lower() == "true"

app = Flask(__name__)

# --- Flask App Configuration ---
app.config["SECRET_KEY"] = SECRET_KEY
app.config["SESSION_COOKIE_SECURE"] = (FLASK_ENV == "production") # Only send cookie over HTTPS in production
app.config["SESSION_COOKIE_HTTPONLY"] = True # Prevent client-side script access to cookie
app.config["SESSION_COOKIE_SAMESITE"] = "Lax" # CSRF protection (consider "Strict" for higher security)
app.config["PERMANENT_SESSION_LIFETIME"] = timedelta(minutes=30) # Session expiry
app.debug = DEBUG_MODE
app.env = FLASK_ENV

# Database connection function
def get_db_connection():
    """Establishes a connection to the PostgreSQL database."""
    try:
        conn = psycopg2.connect(DATABASE_URL)
        return conn
    except psycopg2.Error as e:
        print(f"Database connection error: {e}")
        # In production, log this error but avoid exposing details to the user
        raise ConnectionError("Could not connect to the database.")

# Example: Initialize database (run once on deployment or use a separate migration tool)
def init_db():
    """Initializes the database schema if tables do not exist."""
    try:
        conn = get_db_connection()
        cur = conn.cursor()
        cur.execute("""
            CREATE TABLE IF NOT EXISTS users (
                id SERIAL PRIMARY KEY,
                username VARCHAR(80) UNIQUE NOT NULL,
                password_hash VARCHAR(255) NOT NULL
            );
        """)
        conn.commit()
        cur.close()
        conn.close()
        print("Database initialized successfully.")
    except Exception as e:
        print(f"Error initializing database: {e}")
        # Log this error for debugging but don't stop the app on startup in prod
        # if the table already exists (e.g., in subsequent deployments)

# Call init_db on application startup.
# In a real-world scenario, you'd use a dedicated migration tool (e.g., Alembic).
with app.app_context():
    init_db()

# --- Authentication Decorator (from previous chapter) ---
def login_required(f):
    @wraps(f)
    def decorated_function(*args, **kwargs):
        if 'username' not in session:
            return jsonify({"message": "Unauthorized"}), 401
        return f(*args, **kwargs)
    return decorated_function

# --- Routes (from previous chapter) ---
@app.route('/')
def index():
    return "Welcome to the Secure Flask App! Access /register, /login, or /protected."

@app.route('/register', methods=['POST'])
def register():
    username = request.json.get('username')
    password = request.json.get('password')

    if not username or not password:
        return jsonify({"message": "Username and password are required"}), 400

    # Basic input validation
    if len(username) < 3 or len(password) < 8:
        return jsonify({"message": "Username must be at least 3 chars, password at least 8"}), 400

    hashed_password = generate_password_hash(password, method='pbkdf2:sha256') # Use a strong hashing algorithm

    conn = None
    try:
        conn = get_db_connection()
        cur = conn.cursor()
        cur.execute("INSERT INTO users (username, password_hash) VALUES (%s, %s)", (username, hashed_password))
        conn.commit()
        cur.close()
        conn.close()
        return jsonify({"message": "User registered successfully"}), 201
    except psycopg2.IntegrityError:
        return jsonify({"message": "Username already exists"}), 409
    except Exception as e:
        print(f"Registration error: {e}")
        return jsonify({"message": "An error occurred during registration"}), 500
    finally:
        if conn:
            conn.close()

@app.route('/login', methods=['POST'])
def login():
    username = request.json.get('username')
    password = request.json.get('password')

    if not username or not password:
        return jsonify({"message": "Username and password are required"}), 400

    conn = None
    try:
        conn = get_db_connection()
        cur = conn.cursor(cursor_factory=extras.DictCursor)
        cur.execute("SELECT id, username, password_hash FROM users WHERE username = %s", (username,))
        user = cur.fetchone()
        cur.close()
        conn.close()

        if user and check_password_hash(user['password_hash'], password):
            session['username'] = user['username']
            session['user_id'] = user['id']
            return jsonify({"message": "Logged in successfully", "username": user['username']}), 200
        else:
            return jsonify({"message": "Invalid credentials"}), 401
    except Exception as e:
        print(f"Login error: {e}")
        return jsonify({"message": "An error occurred during login"}), 500
    finally:
        if conn:
            conn.close()

@app.route('/logout', methods=['POST'])
@login_required
def logout():
    session.pop('username', None)
    session.pop('user_id', None)
    return jsonify({"message": "Logged out successfully"}), 200

@app.route('/protected', methods=['GET'])
@login_required
def protected_route():
    return jsonify({"message": f"Hello, {session['username']}! This is a protected resource."}), 200

# --- Error Handling ---
@app.errorhandler(404)
def not_found_error(error):
    return jsonify({"message": "Resource not found"}), 404

@app.errorhandler(500)
def internal_error(error):
    # In production, do not expose internal error details
    app.logger.error('Server Error: %s', (error))
    return jsonify({"message": "An unexpected server error occurred"}), 500


if __name__ == '__main__':
    # When running locally, you can pass environment variables like this:
    # DATABASE_URL="postgresql://user:password@localhost:5432/yourdb" SECRET_KEY="dev_key" python app.py
    app.run(host='0.0.0.0', port=8000) # Ensure this matches EXPOSE and PORT env var in Azure

Explanation of changes:

  • os.getenv(): All sensitive and environment-specific configurations (database URL, secret key, debug mode, environment type) are now read from environment variables using os.getenv().
  • Insecure Defaults: For local development, default values are provided to make the application runnable. These defaults are deliberately insecure (e.g., INSECURE_DEV_KEY_CHANGE_ME_IN_PROD_IMMEDIATELY) to prevent accidental use in production.
  • SESSION_COOKIE_SECURE: This is conditionally set to True only when FLASK_ENV is “production,” ensuring the session cookie is only sent over HTTPS.
  • init_db() call: The init_db() function is called within an app.app_context() block to ensure it runs on application startup. In a larger project, a dedicated database migration tool (like Alembic) would manage schema changes.
  • Error Handling: The 500 error handler is updated to log the error but return a generic message to the user, preventing information leakage.
  • PORT environment variable: Azure App Service will set a PORT environment variable that Gunicorn will respect. While Gunicorn’s default --bind 0.0.0.0:8000 is often sufficient, explicitly setting PORT=8000 in Azure App Service settings (as done in step 5) is a good practice for clarity.

7. Network Security Hardening: PostgreSQL Firewall

After confirming your application is deployed, immediately update your PostgreSQL firewall rules to allow access only from your Azure App Service. This is a critical security measure to protect your database from unauthorized access.

First, get the outbound IP addresses of your App Service:

az webapp show --resource-group $RESOURCE_GROUP_NAME --name $WEB_APP_NAME --query outboundIpAddresses --output tsv

This command returns a comma-separated list of IP addresses. You’ll need to add these as firewall rules to your PostgreSQL server.

# Get current firewall rules (optional, to verify what's there)
az postgres flexible-server firewall list --resource-group $RESOURCE_GROUP_NAME --name $POSTGRES_SERVER_NAME --output json

# Remove the broad public access rule (if you added it in step 4.2)
# Replace 'AllowAllPublicAccess' with the actual rule name if it differs.
az postgres flexible-server firewall delete \
    --resource-group $RESOURCE_GROUP_NAME \
    --name $POSTGRES_SERVER_NAME \
    --rule-name AllowAllPublicAccess

# Add a rule for your App Service's outbound IP(s).
# Replace <APP_SERVICE_OUTBOUND_IP> with the actual IP address you retrieved.
# If the 'az webapp show' command returned multiple IPs, repeat the 'firewall create' command
# for each IP, giving each rule a unique --rule-name (e.g., AllowWebAppAccess1, AllowWebAppAccess2).
# Example for a single IP:
az postgres flexible-server firewall create \
    --resource-group $RESOURCE_GROUP_NAME \
    --name $POSTGRES_SERVER_NAME \
    --rule-name AllowWebAppAccess1 \
    --start-ip-address <APP_SERVICE_OUTBOUND_IP> \
    --end-ip-address <APP_SERVICE_OUTBOUND_IP>

⚠️ What can go wrong: Failing to restrict database access is one of the most common and severe security vulnerabilities. An attacker could potentially connect directly to your database if it’s exposed to the internet, bypassing your application’s security controls.

Testing & Verification

Once the deployment commands are complete, it may take a few minutes for the App Service to pull the Docker image and start the application.

  1. Get the Web App URL:

    az webapp show --resource-group $RESOURCE_GROUP_NAME --name $WEB_APP_NAME --query defaultHostName --output tsv

    This will output a URL like secure-flask-app-xxxxxx.azurewebsites.net.

  2. Access the Application: Open the URL in your web browser. You should see your Flask application’s welcome page.

  3. Test Functionality:

    • User Registration: Try to register a new user via POST request to /register. This verifies the application can connect to the PostgreSQL database and write data.
    • User Login: Log in with the newly created user via POST request to /login. This tests authentication and session management.
    • Authenticated Endpoints: Access the /protected API endpoint after logging in.
    • Error Handling: Try to trigger an expected error (e.g., duplicate username registration) and observe that no sensitive information is leaked in the response (generic 409 or 500 message).
  4. Inspect Session Cookies:

    • Using your browser’s developer tools (e.g., “Application” tab in Chrome, “Storage” in Firefox), inspect the session cookie.
    • Verify that the Secure and HttpOnly flags are set. The Secure flag ensures the cookie is only sent over HTTPS, and HttpOnly prevents client-side JavaScript access.
  5. Check Azure Logs:

    • In the Azure portal, navigate to your App Service.
    • Under “Monitoring,” click “Log stream” or “App Service logs” to view application logs and diagnose any startup issues or connection errors. This is your first line of defense for debugging deployed applications.
  6. Verify PostgreSQL Firewall:

    • Attempt to connect to your PostgreSQL database directly from your local machine using psql or a database client. If you have correctly configured the firewall rules, this connection attempt should fail, as your local IP is not allowed. This confirms the database is isolated.

Common Issues & Solutions

  1. “Application Error” or 500 Internal Server Error:
    • Cause: The container failed to start, or your Flask app crashed during initialization.
    • Solution: Check the Azure App Service “Log stream” in the Azure portal. Look for Python tracebacks or Gunicorn errors. Common culprits include incorrect environment variables (DATABASE_URL, SECRET_KEY), missing requirements.txt dependencies, or issues with your CMD in the Dockerfile. Ensure the PORT environment variable is set to 8000 in App Service settings.
  2. Cannot connect to PostgreSQL database:
    • Cause: Incorrect DATABASE_URL, PostgreSQL server not running, or firewall blocking access.
    • Solution:
      • Verify the DATABASE_URL in App Service settings matches the PostgreSQL server’s details (hostname, user, password).
      • Confirm the PostgreSQL server is running in Azure.
      • Crucially: Check your PostgreSQL flexible server’s firewall rules. Ensure the outbound IP addresses of your App Service are explicitly allowed and that any broad public access rules have been removed.
  3. SECRET_KEY error or session issues:
    • Cause: SECRET_KEY not set as an environment variable in App Service, or mismatch between local and deployed configuration.
    • Solution: Double-check that SECRET_KEY is set in your App Service application settings. Ensure your Flask app reads it correctly using os.getenv(). Also, verify SESSION_COOKIE_SECURE is True in production and your site is accessed via HTTPS.

Summary & Next Step

You’ve successfully deployed your secure Flask application to Azure, configured it with environment variables, and integrated it with a managed PostgreSQL database. This is a significant step towards building production-ready, secure applications. You’ve also hardened the environment by restricting database access and using production-grade container images. This hands-on experience demonstrates the practical application of secure configuration and cloud network principles.

What’s next? With your application running in a cloud environment, the next logical step is to perform more advanced security testing and establish continuous security practices. This includes vulnerability scanning, penetration testing simulations, and integrating security into your development pipeline to maintain a strong security posture over time.

This page is AI-assisted and reviewed. It references official documentation and recognized resources where relevant.

References