EC2 Patcher
A small, local, single-user web GUI (FastAPI + Jinja2 + SQLite) for recurring security patching of EC2 servers. You keep an inventory of servers, upload the security team's CVE report, get a read-only per-server analysis, and then patch an approved plan on one server or on all eligible servers of an analysis (Patch All), with an optional reboot afterwards.
- Ubuntu: analysis (Canonical security data) with an exact package /
.debplan, and patching of the approved plan. - Amazon Linux 2023: read-only analysis (Amazon's ALAS advisories); patching is not supported yet.
- The OS is detected automatically from
/etc/os-release; any other OS is listed as OS not supported yet.
pipx install ec2patcher
ec2patcher
EC2Patcher never runs apt upgrade / apt dist-upgrade: it installs only the exact .deb
files of a plan you approved.
Workflow at a glance
- Servers: add each server (name, IP, SSH user, login type: PEM key or password, optional tags) and run the SSH test.
- Reports: pick or drop the CVE report JSON; it is uploaded and validated automatically.
- Analyze Report: read-only analysis of every server in the report.
- Review each server report (Severity, CVSS, Ubuntu priority, package plan, reboot expectation); export it to Excel if needed. Re-analyze or Retry where needed.
- Approve & Patch one server, or Patch All for the whole analysis. Optionally tick Skip reboot.
- History keeps every decision, execution, verification and reboot result.
Target server requirements
- Ubuntu (analysis + patching) or Amazon Linux 2023 (analysis only), reachable over SSH as
the server's SSH user (per server, default
ubuntu;ec2-useron Amazon Linux) with a PEM key (default; the PEM path is stored, its contents are never read, stored or logged) or with username + password (see below). Other operating systems (including end-of-life Amazon Linux 2) are detected and listed as OS not supported yet / OS not supported. - Passwordless sudo for that user for patching: every privileged command uses
sudo -n, and patching aborts ifsudo -n truefails. Analysis needs no sudo at all. - Ubuntu:
dpkg,apt-get,sha256sumand write access to/tmp(the standard Ubuntu image has them). Amazon Linux 2023:rpm(read-only queries;needs-restartingfromdnf-utilsis optional and used only for the reboot status).
Features
Servers and tags
- Add, edit, delete and SSH-test servers. Clear All Servers requires typing
DELETE SERVERS. - Names may contain letters, digits,
.,_and-and are unique regardless of case. CVE reports are always matched by name. - Optional key/value tags per server (e.g.
display_name = Billing API,env = prod): up to 50 per server, keys unique per server (case-insensitive).display_nameis shown in reports but never used for matching. - Each server has an SSH user (default
ubuntu, e.g.ec2-useron Amazon Linux) used for every ssh/scp to it. It must be a plain POSIX user name ([a-z_][a-z0-9_.-]*, max 32). - SSH test runs
ssh -i <pem> <user>@<ip>withBatchMode=yes,ConnectTimeout=10and a 30 s overall limit, and shows hostname, OS release and architecture or a short error. New host keys are accepted on first connect (accept-new); a changed host key is an error. - Login type per server (dropdown on the server form): PEM key (default) or
username + password. The password is stored per server, encrypted (Fernet, see
Secrets at rest); it is never shown, logged or exported, and the form
field is always empty: when editing, leave it empty to keep the stored password. Switching a
server to PEM, deleting it, Clear All Servers or Reset Database removes its password.
Password login runs
sshpass -e ssh|scp ...; the password reaches sshpass only through theSSHPASSenvironment variable of the child process, never the command line. Installsshpasson the workstation (sudo apt install sshpass); without it the connection fails with a clear message. Analysis or patching of a password server without a usable stored password is refused with a clear message.sudoon the server must still be passwordless (sudo -n).
Report upload
On Reports, choosing or dropping a .json file uploads it immediately (no extra click; a
plain Upload Report button is shown without JavaScript). Max 1 MiB; structural validation
only. The latest accepted report is kept; a failed upload does not replace it.
{
"app-prod-01": ["CVE-2026-12345", "CVE-2026-67890"],
"database-prod-01": ["CVE-2026-22222"]
}
Each key must be a configured server name (an unknown server rejects the whole report); each
value is an array of CVE-YYYY-NNNN… strings (case-insensitive, normalized and de-duplicated).
Analysis (read-only)
Analyze Report starts a background run; the run page refreshes itself and shows each server as Waiting, Analyzing, Complete, Failed (with the reason) or Not supported. Servers are analyzed one after another; one failure never affects the others.
- On the server: one fixed read-only command as the server's SSH user, without sudo:
hostname,
/etc/os-release, architecture, running kernel,/run/reboot-required(.pkgs),dpkg-query(binary → source package and versions) anddpkg --audit. Nothing is downloaded, copied, installed or restarted. - OS detection: the OS is taken from
/etc/os-release; everything OS-specific (inventory, security data lookups, planning, install, reboot check) sits behind anOsAdapter(services/os_adapters/). Adapters: Ubuntu (analysis + patching) and Amazon Linux 2023 (analysis only). For Amazon Linux a second fixed read-only command collects therpminventory and the dnf releasever. Any other OS is shown asOS not supported yet: <name> <version>(not a failure, never patchable). - On the workstation: APT candidates and the
.debplan are resolved against a private APT state per release and architecture (<data dir>/apt/<codename>-<arch>/, pockets<codename>,-updates,-security). Everyapt-get/apt-cachecall overridesDir::State,Dir::CacheandDir::Etc, so the workstation's own APT is never used or changed.apt-get -sand--print-urisgive exact versions, URIs, sizes and SHA256 without downloading anything. - Canonical (
https://ubuntu.com/security/cves/<CVE>.json) decides applicability, fixed version and status, per source package and Ubuntu release, with Debian version comparison. Kernel CVEs are checked against the running kernel. - Amazon Linux 2023: the repository
updateinfo.xml(ALAS advisories) of the server's releasever and of the latest release is fetched on the workstation (viacdn.amazonlinux.commirror lists) and compared with the installed rpm versions (EVR comparison). Statuses: patch available, fix only in a newer releasever, already fixed, package not installed, or no advisory (investigate). No plan is built and the server is never patchable (Patching not supported yet for Amazon Linux 2023). - NVD (CVE API 2.0) supplies only the CVSS Severity (Critical/High/Medium/Low/Unknown) and score; it never changes a finding's status or plan. Canonical's priority is shown as Ubuntu Priority.
- A server with unconfigured/half-installed packages (
dpkg --audit) gets a blocker ("run sudo dpkg --configure -a") instead of a plan. Packages already at or above their target version are never planned (shown as an "already at target" warning). - Findings are grouped into Action required, Investigate and No action. Anything that cannot be decided is shown as such, never as safe.
- Every run is stored as a snapshot (report, server facts, findings, plan); older runs stay
viewable. Export to Excel builds an
.xlsx(Summary, CVE Findings, Package Plan) from that snapshot only, without any network or SSH access.
Re-analyze and Retry
- Retry failed lookups (run page): re-runs only the Canonical lookups that failed in that run.
- Retry these CVEs (server report, Investigate bucket): fetches those CVEs again and re-analyzes the server.
- Re-analyze (server report): analyzes the server again, fetching all of its CVEs from Canonical again.
Patching one server
Each server report has Reject and Approve & Patch (after a confirmation page with a Skip reboot checkbox, unchecked by default). Only the latest analysis of a server can be executed, and each approved analysis only once. The pipeline stops at the first failing step:
- Revalidate hostname, release, architecture and installed versions against the analysis
(drift → PATCH ABORTED — SERVER STATE CHANGED); check
sudo -n true. Packages already at target are dropped; if nothing is left the result is ALREADY PATCHED. - Download each approved
.debfrom its recorded URI; keep it only if size and SHA256 match the plan. - Transfer with
scpto/tmp/<server name>and verify size +sha256sum(one retry). - Simulate
apt-get -s install <explicit .deb paths>: must install exactly the approved packages/versions, with no removal or downgrade. - Install
sudo -n apt-get install -y <explicit .deb paths>with no remote APT sources,--no-remove,DEBIAN_FRONTEND=noninteractive,NEEDRESTART_MODE=l. - Verify installed versions,
dpkg --audit, each CVE against Canonical's fixed version, and/run/reboot-required. - Clean up local and remote staging.
- Reboot, only after a verified patch, only if
/run/reboot-requiredexists and Skip reboot is unchecked:sudo -n reboot, wait up to 10 minutes for a new boot id, record uptime and kernel. The reboot result is recorded separately from the patch result.
A failed or interrupted install is never retried or rolled back; if the connection drops the server is inspected once more and the result is proven or recorded as EXECUTION STATE UNKNOWN. A new analysis is required before trying again.
Patch All
The analysis run page has Patch All with a Skip reboot checkbox. The confirmation page lists the eligible servers in order and the SKIPPED ones with their reasons. A server analyzed again after this run is patched from its latest analysis. Servers are patched one at a time with the pipeline above; the queue stops at the first failure and the rest are shown as NOT RUN. Only one patch execution or queue runs at a time.
Settings
- Caches (top of the page): one badge per cache (NVD cache: CVEs, oldest , TTL
, same for Canonical and Amazon updateinfo; blue when the latest analysis answered
lookups from it, gray when unused or empty), the Cache TTL of each cache and Clear
Security Cache, which empties all three caches (
nvd_cache,cve_metadata_cache,amazon_updateinfo_cache) and the in-memory lookup state. TTLs are entered as hours (24h) or days (30d), between 1 hour and 365 days, and stored in the database. Changing a TTL deletes nothing; expired entries are refreshed on their next lookup. The same badges appear next to the NVD API key badge on the Pre-Patch Analysis pages and link to this section. - Local Patch Download Directory: template, default
/tmp/${server_name}; must contain${server_name}and resolve to a safe absolute path. - Logging: the Log directory (default: the per-user log directory from platformdirs, or
$EC2PATCHER_LOG_DIR) and the current log file path. The directory is created if needed and must be writable, or it is not saved. Logs rotate at 5 MB, keeping 5 old files. - Security Data: Canonical request settings (timeout, pacing, retries, proxy).
- Reset Database: type
RESETto remove all stored data (refused while an analysis or patch is running).
Caching and NVD API key
All caches live in the application SQLite database; failed lookups are never cached and a
cache error never fails a lookup. The TTLs below are the defaults (Settings → Caches; a
saved Canonical TTL also overrides EC2PATCHER_CANONICAL_CACHE_TTL_HOURS). Each analysis run
records, per cache, how many lookups came from the cache and how many were live (NVD: x from
cache / y live, shown on the run page and the server report; re-analyses and retries add to
it).
- Canonical (
cve_metadata_cache): reused for 24 h (1 h while a release is under investigation); older entries are refreshed and used as a marked fallback when ubuntu.com is unreachable. - Amazon updateinfo (
amazon_updateinfo_cache): per repository, reused for 24 h. - NVD (
nvd_cache): raw CVSS metrics per CVE, reused for 30 days; older entries are refreshed and used as a stale cache fallback when NVD is unreachable. Without any data the severity is Unknown. (The old on-disk NVD cache under~/.cache/ec2patcher/nvd/is no longer read or written and can be deleted.)
NVD requests are spaced 6 s apart (public limit). With an API key they are spaced 0.6 s apart. Enter the key on Settings → NVD API Key (stored encrypted; shown only as its last 4 characters, with Replace / Clear), or provide it through the environment — never in a file in this repository:
export NVD_API_KEY=... # your own key; it is sent only in the apiKey request header
Request a free key at https://nvd.nist.gov/developers/request-an-api-key.
A key saved in Settings overrides NVD_API_KEY; Clear falls back to the environment. The
key is never logged, exported or shown in full. Saving a key, and the Test key button, send
one keyed request to NVD right away (cached CVSS lookups send none). The result and its time are
kept in the settings table (never the key itself) and updated by every keyed lookup, so they
survive restarts. The Pre-Patch Analysis pages show only the state and source: NVD API key: not
set, unknown (not checked yet, or NVD could not be reached), valid (checked ) (green,
HTTP 200) or NVD API key rejected (red, HTTP 403, or HTTP 404 with NVD's invalid-apiKey
message), each followed by from Settings (DB) or from NVD_API_KEY env var whenever a key is
configured. For unknown the reason (HTTP status or network error, or not checked yet) is in
the badge tooltip and the Settings notice; HTTP 429 / 5xx is retried once (after Retry-After)
before a check ends as unknown. At start the app checks the key again in the background if its
last check is unknown or older than 24 h. A rejected key never
marks a CVE as unknown to NVD and is never cached. If the saved key cannot be decrypted the badge
is red and asks to enter it again in Settings (no key is sent until then).
Secrets at rest
Server passwords and the Settings NVD API key are encrypted with Fernet (cryptography). The
key file is ~/.config/ec2patcher/secret.key (or $EC2PATCHER_CONFIG_DIR/secret.key), created
with mode 0600 on first use and never stored in the database or the repository. If it is
missing or does not match, nothing fails silently: the Servers / Settings pages, Test SSH,
analysis and patching show a clear error asking you to enter the secret again (it is then
encrypted with the current key). Back up the key file together with the database if you move
them to another machine.
Requirements (workstation)
- Python 3.10+
- OpenSSH client (
ssh,scp) onPATH - APT (
apt-get,apt-cache) and/usr/share/keyrings/ubuntu-archive-keyring.gpg sshpassonly for servers with password login (sudo apt install sshpass)- Internet access to the Ubuntu archive (
archive.ubuntu.com/security.ubuntu.com, orports.ubuntu.comfor arm64),ubuntu.com,cdn.amazonlinux.com(Amazon Linux servers) andservices.nvd.nist.gov(optional: without it severities are Unknown).HTTPS_PROXY/NO_PROXYare honoured.
Install
With pipx (recommended; installs the ec2patcher command in its own
virtual environment):
pipx install ec2patcher # from PyPI
pipx install git+https://github.com/Amiri83/EC2Patcher.git # latest main from GitHub
pipx upgrade ec2patcher # later updates
From a checkout, for development:
python3 -m venv .venv
.venv/bin/pip install -e ".[dev]" # or without [dev] if you don't run tests
Run
ec2patcher # from a checkout: .venv/bin/ec2patcher or .venv/bin/python -m ec2patcher
ec2patcher --version
Open http://127.0.0.1:8080/ (a browser opens automatically when a desktop session is available). Stop with Shutdown App in the sidebar or Ctrl+C.
| Option | Default | Description |
|---|---|---|
--host |
127.0.0.1 |
Bind address (no authentication: keep it local) |
--port |
8080 |
Port |
--data-dir |
per-user data dir | Database and log location (also $EC2PATCHER_DATA_DIR) |
--apt-state-dir |
<data dir>/apt |
Private APT state (also $EC2PATCHER_APT_STATE_DIR) |
--apt-max-age-hours |
6 |
Refresh the private APT lists when older; 0 = every run (also $EC2PATCHER_APT_MAX_AGE_HOURS) |
--no-browser |
off | Do not open a browser |
--version |
Print the version and exit |
Other environment variables: NVD_API_KEY (a key saved in Settings overrides it),
EC2PATCHER_CONFIG_DIR (location of the secret key file), EC2PATCHER_LOG_DIR (default log directory),
EC2PATCHER_CANONICAL_TIMEOUT_SECONDS (20),
EC2PATCHER_CANONICAL_CACHE_TTL_HOURS (24), EC2PATCHER_CANONICAL_BREAKER_THRESHOLD (3).
Data location (Linux defaults)
| What | Path |
|---|---|
| SQLite database (incl. Canonical and NVD caches) | ~/.local/share/ec2patcher/ec2patcher.db |
| Secret key file (encrypts stored passwords / NVD key; mode 0600) | ~/.config/ec2patcher/secret.key |
| Log file (rotating, 5 × 5 MB; directory configurable in Settings) | ~/.local/state/ec2patcher/log/ec2patcher.log |
| Private APT state | ~/.local/share/ec2patcher/apt/<codename>-<arch>/ |
The schema is created and migrated automatically on startup (PRAGMA user_version); existing
data is kept. Run one EC2Patcher process per database.
Test and lint
.venv/bin/python -m pytest -q
.venv/bin/ruff check .
.venv/bin/ruff format --check .
The tests mock ssh, scp, downloads, Canonical, NVD and the local APT backend; they never
need a real server, PEM key, internet access or package installs.
Integration tests (local containers, no AWS)
scripts/test-targets/ holds two SSH targets with key-only auth on localhost: Ubuntu 24.04
(user ubuntu, port 2201) and Amazon Linux 2023 (user ec2-user, port 2202). The key pair is
generated at runtime into scripts/test-targets/.keys/ (git-ignored). Docker runs via sudo
(set DOCKER=docker to change that).
sh scripts/test-targets/up.sh # build + start, generate the key on first use
.venv/bin/python -m pytest -q -m integration # deselected by default
sh scripts/test-targets/down.sh
Overrides: EC2P_IT_HOST, EC2P_IT_UBUNTU_PORT, EC2P_IT_AMAZON_PORT, EC2P_IT_KEY.
GitHub Actions (.github/workflows/tests.yml) runs ruff, the test suite (without integration
tests) and a package build check on every pull request.
Release
The version lives only in pyproject.toml (ec2patcher --version reads the installed
metadata). To release:
- Bump
versioninpyproject.tomland merge it tomain. - Check locally:
.venv/bin/python -m build && .venv/bin/twine check dist/*. - Push a tag
v<version>(e.g.v1.0.0)..github/workflows/publish.ymlchecks that the tag matches the version, runs the tests, builds the sdist + wheel and publishes them to PyPI with Trusted Publishing (OIDC, environmentpypi): no API token is stored in the repository or in GitHub secrets. The trusted publisher must be configured once on PyPI for this repository and workflow.
Security notes
- Binds to
127.0.0.1by default; requests with a non-localHostheader and cross-site POSTs are rejected (CSRF / DNS rebinding). - Subprocesses never use
shell=True; the remote analysis command is fixed; package names, versions and paths are validated against strict patterns. - Analysis uses no sudo and changes nothing. Patching uses
sudo -nonly for the sudo check, theapt-getsimulation/install of the explicit.debfiles and the optional reboot. - PEM contents are never read; the NVD API key and SSH passwords are stored only encrypted
(key file outside the database, mode 0600) and never appear in HTML, logs, exports, error
messages or command lines (passwords reach sshpass via
SSHPASSonly). Unexpected errors show a generic message in the GUI; details go to the log. - The Fernet key file lives in the config directory, never in the package, the database or this repository; the built wheel and sdist contain no secrets, keys or server data.
License
MIT, see LICENSE.
Metadata
Release files for ec2patcher 1.0.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| ec2patcher-1.0.1.tar.gz | 326.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| ec2patcher-1.0.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 534.6 kB
Release files / ec2patcher-1.0.1.tar.gz
| Download URL | ec2patcher-1.0.1.tar.gz |
|---|---|
| Size | 326.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
329c6d500f6959c6c66dfbe11ecfe8b524578578f3f255ac63433ff7746e8907
|
|
BLAKE2b-256 checksum How to use checksums |
a1653878cd02b96fdb0f5c3ae65307d5d315fef852e6641faf5f9bdce50fc1e0
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 4, 2026.
Transparency logRelease files / ec2patcher-1.0.1-py3-none-any.whl
| Download URL | ec2patcher-1.0.1-py3-none-any.whl |
|---|---|
| Size | 208.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
486f4406f9ea215962148b6b7a86ee11ae4841c1290947833ad842210d2bf192
|
|
BLAKE2b-256 checksum How to use checksums |
d1b172ef60d83c875074bd3bddc224b3b9eed58c5ca5611c07cb32f8b2b34a62
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 4, 2026.
Transparency log