A production-grade Flask voting platform where every vote is encrypted, every voter is verified, and every submission is immutable on a tamper-evident ledger.
Fernet vote encryption β’ One-vote enforcement β’ Verifiable audit trail β’ Kubernetes + Helm ready
- Overview
- Features
- Screenshots
- Tech Stack
- Configuration
- Requirements
- Installation
- How It Works
- Security Deep Dive
- Project Structure
- Deployment
- Documentation & Changelog
- Contributing
- License
- Author & Credits
Secure Polling App is a full-stack voting system built with Python / Flask. It was designed to solve a common problem: most polling applications store votes in plaintext, allow duplicate voting, and leave no accountable trail.
Every ballot is encrypted with Fernet before it touches the database, duplicate voting is prevented through layered checks (unique database constraints, IP tracking, and browser cookies), and every submission is appended to a hash-chained audit ledger where tampering with any historical entry breaks the chain.
The stack is containerized, hardened to run as a non-root user, and ships with health/readiness probes plus both Kubernetes (Kustomize) manifests and a parameterized Helm chart for repeatable, production-style deployments.
- Create time-bound polls with configurable start/end windows (admin only)
- Enforce one vote per poll per user or per anonymous browser
- Encrypt every vote at rest with Fernet (AES-128-CBC + HMAC signing)
- Record each ballot in a hash-chained, tamper-evident ledger
- Provide a live admin dashboard with CSV export and ledger integrity verification
- Run locally, in Docker, or on Kubernetes via Kustomize or Helm
| Audience | What they get |
|---|---|
| Developers | Reference architecture for secure Flask + SQLAlchemy apps |
| Security engineers | Zero-trust pattern with Fernet, CSRF, and input hardening |
| DevOps / Platform teams | Docker image, Kubernetes manifests, and a Helm chart with probes + PVCs |
| Privacy-first teams | Self-hosted voting that never stores plaintext ballots |
|
|
π Homepage![]() |
π³οΈ Voting Interface![]() |
π Admin Dashboard![]() |
| Category | Technology | Purpose |
|---|---|---|
| Language | Python 3.11+ | Application runtime |
| Framework | Flask | Web framework, routing, and render engine |
| Data | SQLAlchemy + Flask-SQLAlchemy | ORM over SQLite (default) or a shared DB via DATABASE_URL |
| Flask-Migrate | Alembic-based schema migration support | |
| Security | cryptography (Fernet) |
AES-128-CBC ballot encryption with HMAC signing |
| Flask-Login | Session-based authentication | |
| Flask-WTF / WTForms | CSRF token protection on all forms | |
| Werkzeug | Adaptive, salted password hashing | |
| Server | Gunicorn | Production WSGI server (gthread, env-tunable) |
| Container | Docker | Multi-service local dev via Compose; hardened image |
| Kubernetes | Kustomize + Helm | Cloud-native deployment, PVC, probes, Ingress, HPA |
| CI/CD | GitHub Actions | Docker image build and publish on main |
| Observability | /health, /ready |
Container probe endpoints |
| Variable | Required | Default | Description |
|---|---|---|---|
SECRET_KEY |
β in prod | fallback dev key | Flask session signing key |
ADMINU / ADMINP |
β | β | Bootstrap admin account (container init) |
POLL_ENCRYPTION_KEY |
recommended | generated file | Fernet key; env takes priority over the key file |
DATABASE_URL |
β | sqlite:////data/polls.db |
Optional shared DB (e.g. PostgreSQL) |
DATA_DIR |
β | /data |
Location of the SQLite DB, key file, and ledger |
FLASK_ENV |
β | production |
development or production |
GUNICORN_WORKERS |
β | 4 |
Gunicorn worker processes |
GUNICORN_THREADS |
β | 4 |
Threads per worker |
Key management note:
load_key()resolves the encryption key in priority order βPOLL_ENCRYPTION_KEYenv (Kubernetes Secret, secret manager) before the mounted key file at/data/poll_encryption_key. The key must remain stable for the life of stored data; rotating it makes existing votes unreadable.
| Path (in container) | Contents | Survives restarts |
|---|---|---|
/data/polls.db |
SQLite database (polls, users, votes) | β |
/data/poll_encryption_key |
Fernet key (auto-generated on first boot) | β |
/data/ledger.jsonl |
Hash-chained audit ledger | β |
- Python 3.11+ for local development
- Docker & Docker Compose for the container workflow (recommended)
- Kubectl + Helm 3.14+ and a cluster for Kubernetes deployments
- Git for cloning
git clone https://github.com/Mdskun/secure-polling-app.git
cd secure-polling-app
# Create .env with real secrets
cat > .env << EOF
SECRET_KEY=your-super-secret-key-change-this
ADMINU=admin
ADMINP=YourSecurePass123
FLASK_ENV=production
EOF
# Build and run
docker-compose up --buildAccess the app: http://localhost:5000
The entrypoint automatically generates the Fernet key, initializes database
tables, and creates the admin account from ADMINU/ADMINP on first boot.
git clone https://github.com/Mdskun/secure-polling-app.git
cd secure-polling-app
python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
pip install -r requirements.txt
# Generate a Fernet key file
mkdir -p data
python -c "from cryptography.fernet import Fernet; open('data/poll_encryption_key','wb').write(Fernet.generate_key())"
# Set environment variables
export SECRET_KEY="your-secret-key"
export ADMINU="admin"
export ADMINP="adminpass"
python app.py # development server- Register / Login β username + password validation β Werkzeug hash stored
- Admin creates a poll β question, 2β10 options, optional start/end window
- User votes β option ID encrypted with Fernet β ciphertext stored + ledger block appended
- Duplicate votes blocked β
(poll_id, user_id)DB constraint for logged-in users; IP + cookie checks for anonymous users - Admin reviews results β decrypts ballots on demand β dashboard, CSV export, ledger verify/download
graph LR
U[User] -->|register / login / vote| APP[Flask App]
APP --> AUTH[auth blueprint]
APP --> ADMIN[admin blueprint]
APP --> POLL[poll blueprint]
AUTH -->|hash + verify| DB[(SQLite / shared DB)]
ADMIN -->|poll CRUD| DB
POLL -->|Fernet ciphertext| DB
POLL -->|append block| LEDGER[ledger.jsonl]
ADMIN -->|verify / download| LEDGER
Plaintext option β Fernet.encrypt() β ciphertext bytes β stored in DB
Ledger block β SHA-256(index + timestamp + vote_hash + prev_hash) β append
| Module | Responsibility | Security Notes |
|---|---|---|
utils.py |
Fernet encryption + ledger append | Thread-safe writes via mutex |
models.py |
User / Poll / PollOption / Vote schema | UNIQUE(poll_id, user_id) constraint |
admin.py |
Poll CRUD, results, CSV + ledger export | @admin_only on every route |
auth.py |
Register / login / logout | Password + username validation, hashing |
poll_blueprint.py |
Poll listing + voting | IP + cookie duplicate-vote checks |
cipher = Fernet(key)
encrypted_vote = cipher.encrypt(vote_text.encode()) # write path
decrypted = cipher.decrypt(vote.encrypted_vote) # admin read pathFernet provides sign-then-encrypt semantics: AES-128-CBC with PKCS#7 padding plus an HMAC-SHA256 signature. The key never appears in logs or user-facing errors.
| User type | Primary check | Secondary check | Database constraint |
|---|---|---|---|
| Authenticated | user_id lookup |
β | UNIQUE(poll_id, user_id) |
| Anonymous | ip_address lookup |
Per-poll browser cookie | Application-level only |
Two independent checks for anonymous users prevent both network-wide lockout (IP-only would block whole campuses) and re-voting from a shared IP.
{
"index": 42,
"timestamp": "2024-01-15T10:30:00Z",
"poll_id": 5,
"user_id": null,
"option_id": 2,
"vote_hash": "sha256...",
"prev_hash": "abc123...",
"block_hash": "def456..."
}Every block hashes its own fields and the previous block's hash. Altering
any historical entry breaks the chain; verify_ledger() reports the exact
failing block, and the admin dashboard surfaces the result.
- CSRF tokens required on every POST form
- HTML escaping on all user-generated content (XSS prevention)
- Rate of failure is surfaced to users as generic messages; details go to logs
- Containers run as non-root with dropped capabilities and a read-only root filesystem
secure-polling-app/
β
βββ app.py # Flask app, config, /health + /ready probes
βββ wsgi.py # Gunicorn entry point
βββ entrypoint.sh # Container bootstrap (key gen, DB init, admin)
βββ gunicorn.conf.py # Env-tunable worker/thread count
βββ models.py # SQLAlchemy models: User, Poll, PollOption, Vote
βββ utils.py # Fernet encryption + thread-safe audit ledger
βββ auth.py # Registration / login / logout blueprint
βββ admin.py # Admin dashboard, poll CRUD, CSV & ledger export
βββ poll_blueprint.py # Poll listing and voting logic
βββ init_db.py # Table creation (runs in the init container)
βββ create_admin.py # Bootstrap admin from ADMINU / ADMINP
β
βββ Dockerfile # python:3.11, non-root appuser (UID 10001)
βββ docker-compose.yml # Local orchestration with a data volume
βββ requirements.txt # Python dependencies
βββ .env.example # Documented environment template
β
βββ k8s/ # Kubernetes manifests (Kustomize)
β βββ deployment.yaml # Recreate strategy, probes, securityContext
β βββ persistent-volume-claim.yaml
β βββ configmap.yaml / service.yaml / ingress.yaml
β βββ kustomization.yaml # Secret generation from gitignored secrets.env
β
βββ helm/secure-polling-app/ # Parameterized Helm chart
β βββ Chart.yaml / values.yaml
β βββ templates/ # secret, configmap, deployment, pvc, ...
β
βββ templates/ # 9 Jinja2 templates (base, login, polls, adminβ¦)
βββ static/style.css # Responsive styling
βββ docs/architecture.png # Architecture diagram
βββ screenshots/ # App screenshots
βββ .github/workflows/ # GitHub Actions: build + publish Docker image
docker-compose -f docker-compose.yml up -dProduction manifests live in k8s/:
docker build -t mdskun/secure-polling-app:1.1.0 .
docker push mdskun/secure-polling-app:1.1.0
cp k8s/secrets.env.example k8s/secrets.env # fill in real secrets
kubectl apply -k k8s/
kubectl get pods -n polling -wIncludes a namespace, ConfigMap, PVC, hardened Deployment (non-root, read-only
root FS, idempotent db-init init container, liveness/readiness/startup
probes), Service, optional Ingress, and secret generation. Full guide in
k8s/README.md.
A fully parameterized Helm chart is included:
helm install poll helm/secure-polling-app \
--namespace polling --create-namespace \
--set image.repository=mdskun/poll-app \
--set image.tag=1.1.0 \
--set secrets.SECRET_KEY="$(openssl rand -hex 32)" \
--set secrets.ADMINP='A Very Strong Password! 123' \
--set secrets.POLL_ENCRYPTION_KEY="$(python -c 'from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())')"The chart auto-generates and persists secret values across upgrades, wires
probes, init-container bootstrap, PVC, non-root security contexts, Ingress, and
HPA β and fails fast if you try to scale SQLite beyond one replica without
pointing env.DATABASE_URL at a shared database.
- Set a strong
SECRET_KEYandADMINP - Provide
POLL_ENCRYPTION_KEYfrom a secret manager and keep it stable - Terminate TLS at the Ingress (e.g. cert-manager)
- Back up
/data/polls.db(and the key) on a schedule
docs/architecture.pngβ visual overview of the internal architecturechangelog.mdβ detailed per-release changesk8s/README.mdβ Kustomize deployment guidehelm/secure-polling-app/README.mdβ Helm values reference
Contributions are welcome β security researchers, Flask developers, and DevOps
engineers alike. Please read contributing.md for the
workflow (fork β branch β PR), code guidelines, and how to report security
issues privately.
Distributed under the MIT License. See LICENSE.
Manthan D Soni
π Secure by design. Verified by code. Deployable by Helm.
Questions? Issues? PRs welcome β let's build better voting infrastructure.


