LocalStack moved core services behind a paid plan, breaking teams that relied on it for local development and CI/CD. MiniStack is a free, MIT-licensed alternative that emulates 40+ AWS services on a single port (4566) with real databases and Terraform compatibility.
What This Does
MiniStack is a Python-based AWS emulator (130 Python files) with a Flask-style HTTP server in ministack/app.py. It implements service handlers across ministack/services/ — S3, EC2, Lambda, Cognito, RDS, CloudFormation, and others. The core/ directory holds shared infrastructure: responses.py, persistence.py, and router.py.
Real infrastructure support is notable: RDS spins up actual Postgres/MySQL containers, ElastiCache runs Redis, Athena uses DuckDB, and ECS/Lambda execute real Docker containers. Testcontainers bindings exist for Go, Java, and Python in the Testcontainers/ directory. The README claims ~270MB image and ~21MB RAM idle, versus LocalStack's ~1GB image.
How It Is Wired
Execution starts in ministack/app.py — app (line 1026) reaches 426 functions, _handler (line 126) reaches 402. The shortest paths to effects: _handler -> handle_request reaches crypto via os.urandom; main -> _cleanup touches the filesystem. Network calls leave via client.get from handle_oauth2_authorize in Cognito.
The critical hub is ministack/core/responses.py — 65 modules import it, and its json_response is called from 520 places, error_response_json from 409. Change that file and you change the entire system. persistence.py (46 importers) and services/__init__.py (21 importers) are similar blast radii.
The lambda_svc.py file (124 functions, 3,559 lines) is the most complex — it makes outbound network calls, runs external commands, and participates in an import cycle with core/lambda_runtime.py and cloudformation/__init__.py. The cycle means changes ripple unpredictably.
How To Use It
# Option 1: PyPI
pip install ministack
ministack
# Option 2: Docker
docker run -p 4566:4566 ministackorg/ministack
# Option 3: Clone and build
git clone https://github.com/moses-y/ministack
cd ministack
docker compose up -d
# Verify
curl http://localhost:4566/_ministack/health
Configuration is runtime-based via POST /_ministack/config (e.g., {"lambda_svc.LAMBDA_EXECUTOR": "docker"}). State resets via POST /_ministack/reset. The Makefile and pyproject.toml suggest standard Python tooling.
Real-World Use
For CI pipelines that need AWS integration tests without cloud costs: run MiniStack in a Docker container, point AWS_ENDPOINT_URL=http://localhost:4566 at it, and execute Terraform plans or boto3 tests against it. The internal reset endpoint lets you wipe state between test runs — something real AWS can't do.
Critical — golang.org/x/crypto@0.45.0 carries CVE-2026-42508 (critical); the pinned version is exactly what installs. Upgrade and commit the lockfile.
Critical — .github/workflows/docker-publish-on-pr.yml checks out PR code in a privileged workflow with DOCKERHUB_TOKEN exposed. This is the most reliably exploited Actions pattern.
Critical — Same workflow exposes publish-scoped Docker Hub credentials to fork-triggered jobs.
High — Third-party Actions pinned to tags (@v4, @v6) instead of commit SHAs; tags can be moved (the tj-actions pattern).
High — docker-compose.yml mounts the Docker socket with privileged mode and host networking.
High — ministack/app.py sets Access-Control-Allow-Origin: "*", enabling cross-site authenticated requests.
High — No lockfile for pyproject.toml; transitive dependencies can drift.
High — containerd@1.7.29 and docker/docker@27.1.1 carry published advisories.
Medium — No Dependabot/Renovate configured; no permissions: declarations in CI workflows.
Deep nesting (depth 6) and oversized files (3,559 lines in lambda_svc.py) make the code hard to follow. The import cycle compounds this.
The Bottom Line
A genuinely useful LocalStack alternative with real infrastructure support and a small footprint. The architecture is sound but the security posture needs immediate attention — fix the workflow privileges and pinned dependencies before adopting it in production CI. Best suited for teams that want free local AWS emulation and can tolerate the maintenance overhead of a young project.
What the analyser found
Deployment readiness
7/7
✓Container image
✓CI pipeline
✓Lockfile committed
✓Test suite
✓README
✓License
✓No committed secrets
Composition
159 files
Python130
YAML7
Markdown7
Go1
Java1
TOML1
FlaskSpringDocker
Module dependencies
The 10 most depended-upon modules of 130, from static import analysis. Red outline marks a module in an import cycle.
Ranked by severity × confidence × production reach. Reach is the honest discriminator across a collection that is mostly other people's code: the same finding matters more in something that ships.
critical3
high5
medium7
low2
Upgrade the pinned dependency carrying a critical advisorycritical27 occurrences
golang.org/x/crypto@0.45.0 CVE-2026-42508 (critical) +26 more
The manifest pins the exact version the advisory covers, so this is not a warning about drift: the vulnerable code is what installs, and a critical rating means it is reachable over the network without credentials or user interaction.
Fix: Upgrade to the fixed version named in the advisory, then commit the lockfile.
Do not check out pull request code in a privileged workflowcritical
.github/workflows/docker-publish-on-pr.yml
privileged trigger checks out the untrusted ref
This is the most reliably exploited Actions pattern: anyone opening a pull request gets code execution with your repository secrets.
Fix: Split into an unprivileged pull_request build and a workflow_run job that only consumes its artifact.
Keep secrets out of workflows a fork can triggercritical2 occurrences
.github/workflows/docker-publish-on-pr.yml
DOCKERHUB_USERNAME, DOCKERHUB_TOKEN (publish or cloud scope)
Contributor-controlled input reaches a job holding a publish token or cloud credential, so a pull request becomes credential exfiltration.
Fix: Move secret-using steps into a workflow_run job that never checks out pull request code, or gate on an environment with required reviewers.
Pin third-party GitHub Actions to a commit SHAhigh12 occurrences
A tag can be moved, so the action running with your token and secrets is whatever its owner last pushed; this is how tj-actions/changed-files leaked secrets from thousands of repos.
Fix: Replace each @vN with the 40-character commit SHA, keep # vN as a comment, and let Dependabot bump the SHAs.
Upgrade the pinned dependency carrying a published advisoryhigh20 occurrences
github.com/containerd/containerd@1.7.29 CVE-2026-46680 (high) +8 more; github.com/docker/docker@27.1.1+incompatible CVE-2026-42306 (high) +10 more
The pinned version is the one that installs, so a published advisory against it describes this deployment rather than a hypothetical one, and the advisory is also a public description of how to exploit it.
Fix: Upgrade to the fixed version named in the advisory, then commit the lockfile.
Commit a lockfile beside the manifesthigh
pyproject.toml
manifest with no lockfile in scope
An unlocked range means the artifact you tested and the artifact you ship can contain different transitive code, so a malicious patch release reaches production with no diff.
Fix: Run the package manager once and commit the generated lockfile.
Drop privileged mode and host networkinghigh
docker-compose.yml
the docker socket mounted
privileged hands the container every capability including direct device access, which makes escaping to the host a documented one-liner, and host networking removes the network boundary so every port the container opens is open on the host.
Fix: Grant the specific capability the workload needs with cap_add, and publish ports explicitly.
Replace the wildcard CORS origin with an explicit allow listhigh
ministack/app.py
Access-Control-Allow-Origin": "*"
Any page on the internet can call the API with the browser's cookies attached, so a logged-in visitor to an unrelated site performs authenticated requests without knowing, and the wildcard combined with credentials is the exact configuration browsers refuse for that reason.
Fix: List the origins the API actually serves, and never pair a wildcard with credentials.
Declare least-privilege permissions for GITHUB_TOKENmedium2 occurrences
.github/workflows/ci.yml
2 workflow(s) declare no permissions, 1 of them reference secrets
With no declaration the token inherits the repository default, so any injected step can push commits or mint releases from inside your own CI.
Fix: Add permissions: contents: read at the top of the workflow and widen per job only where needed.
Enable Dependabot or Renovatemedium
4 manifest(s), no update bot configured
Without a bot a published advisory sits unpatched until someone audits by hand, which across 1,322 repositories means never.
Fix: Commit .github/dependabot.yml covering the repo ecosystems plus github-actions.
Pin the container base image by digestmedium2 occurrences
Dockerfile
python:3.12-alpine, python:3.12-alpine
An untagged or mutable base means today's build and last month's contain different libc and a different CVE set, with no record of which shipped.
Fix: Use image:tag@sha256:<digest> and enable Dependabot's docker ecosystem.
Gate pull requests on a dependency vulnerability scanmedium
.github/workflows
no dependency scan in CI
This is the one gate that would catch a known-vulnerable package before it reaches a build, and no repository in the sample had it.
Fix: Add dependency-review-action on pull_request, or osv-scanner on push and a schedule.
5 further finding(s) are listed in the full report.
Checked deterministically against the repository tree and a bounded set of its files: committed credentials, unpinned actions and base images, missing lockfiles and update bots, workflows that discard failures, published advisories against the declared dependencies, runtime configuration, licensing and notebook reproducibility. No language model is involved in this section.