⚠️ Not affiliated with, endorsed by, or sponsored by the Center for Internet Security (CIS). See DISCLAIMER.md. This project implements hardening aligned with the CIS Benchmarks™; it references CIS as a standard only.
English | 简体中文 | 日本語 | ภาษาไทย
part of the ohbs-* family: ohbs-image · ohbs-host · ohbs-cloud
oh baseline image
Repository / CLI / package:
ohbs-image· Full name: oh baseline image — part of the oh baseline (ohbs) family. Config-driven golden-image builder for Tencent Cloud. ohbs-image launches a short-lived CVM, applies CIS hardening from its bundled ohbs-os engine, re-audits against a configurable score gate, and captures the result as a custom image — fully repeatable and auditable, every time. Built for DevOps and security teams that need hardened base images they can trust in CI pipelines, Auto Scaling launch templates, and Terraform image references.
Zero pip dependencies. 12 OS profiles across Linux and Windows. Build-time gate with configurable score threshold. All roles ship inside the package — no Galaxy, no network drift.
Beyond the build itself, ohbs-image covers the full build → test → distribute governance loop:
- Instance-level smoke test before the snapshot — a broken image never ships
- Image lineage (
ohbs-image images) — source → image IDs, score, version history - WeCom notifications — pair with cron/systemd timer for scheduled rebuilds
- SLSA-style signed provenance (
ohbs-image verify) — tamper-evident build records - OIDC / STS credentials — zero long-lived AK/SK in CI;
assume_rolefor group accounts
Table of Contents
- Quick Start
- Installation
- Commands
- Configuration
- Architecture
- Profiles
- Test Matrix
- CI/CD Integration
- Security model (for enterprise review)
- Group accounts (organization)
- Troubleshooting
- Roadmap
- Contributing
- CIS Benchmarks Disclaimer
- License
Quick Start
# 1. Install
git clone https://github.com/susunola/ohbs-image.git
cd ohbs-image
pip install .
# 2. Generate and edit configuration
ohbs-image init
# Edit ohbs-image.toml — fill in VPC, subnet, security group, and source_image_id
# 3. Build
ohbs-image preflight # validate credentials and prerequisites
ohbs-image validate # dry-run: render templates + packer validate
ohbs-image build # produce the hardened custom image
ohbs-image clean # remove build artifacts
# Set credentials (environment variables only — never in config files)
export TENCENTCLOUD_SECRET_ID=AKIDxxxx
export TENCENTCLOUD_SECRET_KEY=xxxx
export WINRM_PASSWORD=xxxx # Windows builds only
Example output (build)
══════════════════════════════════════════════════════════
ohbs-image 0.14.1 — tencentos3 (L1) → ap-guangzhou-4
══════════════════════════════════════════════════════════
[packer] tencentcloud-cvm: Launching instance (S5.MEDIUM2)...
[packer] tencentcloud-cvm: Provisioning with ansible-local...
[packer] tencentcloud-cvm: TASK [ohbs-tencentos3 : apply CIS Level 1] ***
[packer] tencentcloud-cvm: ok: 142 changed: 38 failed: 0
[packer] tencentcloud-cvm: TASK [ohbs-tencentos3 : reboot] ************
[packer] tencentcloud-cvm: Instance rebooted — re-auditing pending items
[packer] tencentcloud-cvm: TASK [ohbs-tencentos3 : gate] **************
[packer] tencentcloud-cvm:
[packer] tencentcloud-cvm: ═══ CIS Hardening Results ═══
[packer] tencentcloud-cvm: Mode: apply
[packer] tencentcloud-cvm: Profile: L1
[packer] tencentcloud-cvm: Total: 142
[packer] tencentcloud-cvm: Passed: 142
[packer] tencentcloud-cvm: Failed: 0
[packer] tencentcloud-cvm: Score: 100% ≥ 85% ✓ PASS
[packer] ==> tencentcloud-cvm: smoke test: sshd config parses ... ok
[packer] ==> tencentcloud-cvm: smoke test: /dev/shm noexec ... ok
[packer] ==> tencentcloud-cvm: smoke test PASSED — image is buildable
[packer] ==> tencentcloud-cvm: Creating custom image...
[packer] ==> tencentcloud-cvm: Image created: img-abc123def456
[packer] ==> tencentcloud-cvm: Terminating build instance...
✔ Build complete — image-id: img-abc123def456
✔ Output image ID(s): img-abc123def456
✔ Re-audit score: 100%
✔ Lineage recorded -> ~/.ohbs-image/lineage.jsonl
✔ Provenance signed with GPG key 0123ABCD -> ...provenance.json.sig
Not installed? Replace
ohbs-imagewithpython3 -m ohbs_imagein any command.
Installation
Prerequisites
| Requirement | Details |
|---|---|
| Python | 3.11+ (stdlib only — zero pip dependencies) |
| Packer | 1.12+ |
| ansible-core | 2.15+ (controller — required for Windows builds) |
| ansible.windows | ansible-galaxy collection install ansible.windows (Windows builds only) |
| Tencent Cloud | Sub-account with cvm:RunInstances, cvm:CreateImage, cvm:DescribeImages; cvm:CopyImage for cross-region copy |
| Network | Dedicated VPC + subnet + security group — SSH/22 (Linux) or WinRM/5986 (Windows), source-restricted to build machine egress IP |
| Source Image | Public image ID for the target OS |
Install from source
git clone https://github.com/susunola/ohbs-image.git
cd ohbs-image
pip install .
ohbs-image --version
Commands
ohbs-image # show help
ohbs-image init # generate ohbs-image.toml
ohbs-image preflight # validate config, credentials, prerequisites
ohbs-image validate # render templates + packer validate
ohbs-image build # render + packer build → custom image
ohbs-image build --skip-if-unchanged # ... skip when inputs are unchanged (change detection)
ohbs-image scan [--min-score 85] # audit-only build (no remediation) + score gate
ohbs-image scan --sarif out.sarif # ... plus a SARIF 2.1.0 failure report
ohbs-image scan --xccdf out.xml # ... plus an XCCDF 1.2 TestResult (GRC ingestion)
ohbs-image test --idempotency # re-run apply, fail if 2nd pass changes anything
ohbs-image list # enumerate available profiles with metadata
ohbs-image images [--latest] [-n N] # list recorded builds (lineage)
ohbs-image pending # change detection: is a rebuild required? (exit 0/1)
ohbs-image cleanup-images [--older-than 30] # retire old images by lineage age
ohbs-image cleanup-images --apply # actually delete (default = dry run)
ohbs-image verify --provenance <file> # verify a SLSA provenance signature
ohbs-image verify --image <img-id> # ... or locate provenance by image ID
ohbs-image verify-image --image <img-id> # clean-boot verification of a produced image
ohbs-image drift --host <ip> [--image <id>] # config drift on a running instance vs image baseline
ohbs-image drift --host <ip> --save-baseline # save the current host scan as a drift baseline
ohbs-image check-source # vendor image refresh detection (rebuild needed?)
ohbs-image audit --tool oscap ... # independent audit: OpenSCAP (RHEL-family SCAP content)
ohbs-image audit --tool inspec ... # independent audit: Chef InSpec (dev-sec baselines)
ohbs-image audit --tool kitty --parse out.csv # independent audit: HardeningKitty (Windows) CSV
ohbs-image clean # remove .ohbs-image-build/
| Flag | Applies to | Description |
|---|---|---|
--config <path> |
all | Config file path (default ./ohbs-image.toml) |
--workdir <dir> |
all | Build output directory (default ./.ohbs-image-build) |
--quiet |
validate, build, scan | Suppress packer output |
--debug |
validate, build, scan | Enable PACKER_LOG=1 |
-y / --yes |
build | Skip confirmation prompt |
--log-file <path> |
build | Write full build log to file |
--skip-if-unchanged |
build | Skip when inputs (source image, rules, benchmark, level) are unchanged |
--min-score <pct> |
scan, audit, verify-image | Gate threshold (default 85; below it → exit 1) |
--sarif <path> |
scan, audit | Write findings as SARIF 2.1.0 |
--xccdf <path> |
scan, audit | Write findings as XCCDF 1.2 (enterprise GRC ingestion) |
--host <ip> |
audit | Target host to audit (oscap/inspec) |
--datastream <path> |
audit | oscap SCAP datastream on the target (e.g. /usr/share/xml/scap/ssg/content/ssg-rhel9-ds.xml) |
--baseline <name> |
audit | inspec baseline (default dev-sec/linux-baseline) |
--parse <csv> |
audit --tool kitty | HardeningKitty audit CSV export to parse |
--older-than <days> |
cleanup-images | Retire builds older than N days (default 30) |
--keep-latest <n> |
cleanup-images | Always keep the newest N builds (default 1) |
--unused-since <days> |
cleanup-images | Only delete images NOT shared with other accounts (in-use guard; 0 = off) |
--apply |
cleanup-images | Actually delete (default is a dry run) |
Configuration
ohbs-image.toml is the single source of truth — no manual template editing.
[build]
profile = "tencentos3"
# Linux: ubuntu2004 | ubuntu2204 | ubuntu2404 |
# rhel8 | rhel9 | rhel10 |
# tencentos3 | tencentos4
# Windows: win2016 | win2019 | win2022 | win2025
region = "ap-guangzhou"
zone = "ap-guangzhou-4"
instance_type = "S5.MEDIUM2"
source_image_id = "img-xxxxxxxx"
vpc_id = "vpc-xxxxxxxx"
subnet_id = "subnet-xxxxxxxx"
security_group_id = "sg-xxxxxxxx"
associate_public_ip = true
# spot = true # use a spot instance for the build VM (up to ~90% cheaper)
# instance_name = "my-build-cvm" # optional explicit name for the temporary build CVM ("" = plugin auto)
# # [build.packer] — passthrough of arbitrary tencentcloud-cvm Packer builder
# # args (inherits the full Packer capability set). E.g. some SA-series
# # instance types do not support the default CLOUD_PREMIUM root disk:
# # [build.packer]
# # disk_type = "CLOUD_SSD"
# # disk_size = 100
[image]
name_prefix = "tencentos3-cis"
# name = "my-ohbs-image" # optional: fixed image name (empty = auto prefix-level-timestamp)
copy_regions = ["ap-shanghai"] # [] to disable cross-region copy
# share_accounts = ["uin/1234567890"] # optional: share the built image with other accounts
# share_org_units = ["uin/1234567890"] # optional: org-level sharing (same API)
[cis]
level = 1 # 1 or 2
# min_score = 85 # post-reboot audit gate (0 disables; default 85)
# rules_include = ["1.5.6"] # run only these rules
# rules_exclude = ["1.1.2.2.4"] # always wins over rules_include
# Per-control parameter overrides (deep-merged into the catalog at render):
# [cis.overrides."5.2.2"]
# ssh_max_auth_tries = 4
[cloud]
secret_id_env = "TENCENTCLOUD_SECRET_ID"
secret_key_env = "TENCENTCLOUD_SECRET_KEY"
# winrm_password_env = "WINRM_PASSWORD" # Windows only
# Group-account (organization) cross-account builds — assume a CAM role in
# the target account using the local AK/SK:
# assume_role_arn = "qcs::cam::uin/1234567890:roleName/CrossAccountBuilder"
# assume_role_session = "ohbsimage-build" # optional, default "ohbs-image"
# assume_role_duration = 3600 # optional, default 7200, range 0-43200
# OIDC / STS temporary credentials (CI, no long-lived AK/SK):
# security_token_env = "TENCENTCLOUD_SECURITY_TOKEN" # Packer default
# Build notifications (WeCom group-robot webhook). Empty webhook = off.
# [notify]
# webhook = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxxx"
# on = "failure" # always | success | failure
# deploy_webhook = "https://ci.example.com/api/images" # POST image metadata on success (EventBridge-style)
# SLSA-style provenance signing (GPG). Empty = provenance unsigned.
# [sign]
# gpg_key = "ABCDEF0123456789"
[meta]
os_tag = "tencentos-3"
benchmark = "CIS-v1.0.0"
# smoke_test = true # instance-level checks before the image snapshot
# cve_scan = false # optional: trivy vulnerability gate before the snapshot
# sbom = false # optional: emit an SBOM into the image + provenance
# verify_boot = false # optional: boot a probe from the produced image and re-audit
# test_components = ["scripts/app-check.sh"] # optional: user test scripts run before snapshot
Full reference
| Section | Field | Type | Notes |
|---|---|---|---|
[build] |
profile |
string | Profile name from supported list |
region |
string | e.g. ap-guangzhou |
|
zone |
string | e.g. ap-guangzhou-4 |
|
instance_type |
string | e.g. S5.MEDIUM2 |
|
source_image_id |
string | OS public image ID | |
vpc_id |
string | VPC identifier | |
subnet_id |
string | Subnet identifier | |
security_group_id |
string | Must start with sg- |
|
associate_public_ip |
bool | Assign public IP | |
spot |
bool | Use a spot instance for the build VM (instance_charge_type=SPOTPAID; up to ~90% cheaper, may be repossessed mid-build, default false) |
|
instance_name |
string | Optional explicit name for the temporary build CVM (empty = Packer auto-generates) | |
packer |
table | Passthrough of arbitrary tencentcloud-cvm Packer builder args (e.g. disk_type, disk_size, data_disks), injected verbatim into the generated HCL source block |
|
[image] |
name_prefix |
string | Output image name prefix |
name |
string | Fixed image name (empty = auto prefix-level-timestamp) |
|
copy_regions |
[]string | Regions to replicate (empty = skip) | |
share_accounts |
[]string | Share the built image with other accounts (uin/…) after build (empty = off) |
|
share_org_units |
[]string | Org-level sharing — same ModifyImageSharePermission API, merged with share_accounts (empty = off) |
|
[cis] |
level |
int | 1 (Level 1) or 2 (Level 2) |
min_score |
int | Post-reboot audit gate (default 85; 0 disables) |
|
rules_include |
[]string | Rule-ID filter — when set, ONLY these rules run (empty = all) | |
rules_exclude |
[]string | Rule-ID filter — always wins over rules_include |
|
overrides |
table | Per-control parameter overrides, keyed by rule ID — deep-merged into the catalog at render time (e.g. [cis.overrides."5.2.2"]) |
|
[cloud] |
secret_id_env |
string | Env var for Secret ID |
secret_key_env |
string | Env var for Secret Key | |
security_token_env |
string | STS session-token env var (default TENCENTCLOUD_SECURITY_TOKEN; used with OIDC/STS credentials) |
|
winrm_password_env |
string | Windows admin password env var | |
assume_role_arn |
string | Group-account CAM role ARN (empty = off). e.g. qcs::cam::uin/12345:roleName/X |
|
assume_role_session |
string | AssumeRole session name (default ohbs-image) |
|
assume_role_duration |
int | Session seconds, 0-43200 (default 7200) | |
[meta] |
os_tag |
string | Tag value for output image |
benchmark |
string | CIS benchmark version tag (pinned in lineage/provenance for auditability) | |
ssh_port |
int | SSH port (default 22; TencentOS: 36000) |
|
ssh_timeout |
string | Packer SSH timeout (default "15m") |
|
ssh_debug_password |
string | Root password for VNC debug (default empty) | |
smoke_test |
bool | Instance-level checks before snapshot (default true) |
|
cve_scan |
bool | Trivy CRITICAL-severity vulnerability gate before the snapshot (default false) |
|
sbom |
bool | Emit an SBOM (/opt/ohbs-image-SBOM.jsonl) into the image, hash it and pin it in lineage + provenance (default false) |
|
verify_boot |
bool | After the snapshot, boot a probe instance from the produced image, re-audit on fresh boot and gate (Linux only, default false) |
|
test_components |
[]string | User-defined test scripts run sequentially before the snapshot (Image Builder test-component style); non-zero exit aborts the build (empty = off) | |
[notify] |
webhook |
string | WeCom group-robot webhook URL (empty = off) |
on |
string | always | success | failure (default failure) |
|
deploy_webhook |
string | POST {image_id, score, profile} on build success to trigger downstream CI/CD (EventBridge-style; empty = off) |
|
[sign] |
gpg_key |
string | GPG key id/fingerprint for provenance signing (empty = unsigned) |
Architecture
Linux pipeline
Four phases executed inside the ephemeral CVM via ansible-local:
- Install — provisions
ansible-corevia the OS package manager + pip. - Harden — runs the bundled ohbs-os engine (
ohbs_engine.py+rules.json). Variables:cis_mode: apply,cis_profile: L1/L2. - Reboot + re-audit — reboots the instance and re-runs only the rules that were pending a reboot. Catches kernel parameters, audit daemon configs, and other settings that only take effect after restart.
- Gate — final score check against a configurable threshold (default 85%). If the score falls below,
ansible-playbookexits non-zero and Packer fails the build — the image is never created.
SSH access safety net
CIS rules can disable root SSH login (PermitRootLogin no — TencentOS 3 rule
5.1.22 / TencentOS 4 rule 5.2.10). Because the builder itself connects as
root, this would lock the build out after the reboot. ohbs-image therefore
adds two orchestration-layer guarantees that are regenerated on every build
(they can never go stale):
- Dedicated build user
ohbsimage— created byinstall-ansible.shwith passwordless sudo and the sameauthorized_keysas the current SSH user, so it can reconnect even if root login is fully disabled. - SSH guard — opens the live SSH port in firewalld / nftables /
iptables, and if a CIS rule set
PermitRootLogin no, temporarily restores key-based root login so Packer can reconnect.
The final image ships hardened: the cleanup provisioner re-applies
PermitRootLogin no before the snapshot is taken. To administer a built
image, use the ohbsimage user (sudo -i for root), or create your own user —
root password login is disabled by design per CIS.
What ships in the image (Linux)
Every Linux build leaves a ohbs-image paper trail inside the image so admins know exactly what was done and which admin channel to use:
| Path | Purpose |
|---|---|
/etc/ohbs-image/banner |
ASCII banner with the ohbs-image logo + image metadata (colored). |
/etc/motd |
The same banner + build summary, shown after SSH login. |
/etc/issue, /etc/issue.net |
Plain-text version for serial / network console. |
/etc/ssh/sshd_config.d/99-ohbs-image-banner.conf |
Wires the SSH Banner directive. |
/opt/ohbs-image-REPORT.md |
Full hardening report (what was done, score, follow-ups). |
/opt/ohbs-image-AUDIT-RESULT.json |
Raw re-audit JSON (the gate result). |
/usr/local/bin/ohbs-image-info |
One-shot summary command: ohbs-image-info. |
Windows builds ship the same audit evidence at
C:\ProgramData\ohbs-image\AUDIT-RESULT.json (raw engine result.json from the
build-time audit — the Windows counterpart of /opt/ohbs-image-AUDIT-RESULT.json).
$ ssh ohbsimage@<host>
.---..---.
.-' '-. CIS IMAGE
.' '. ___ ___ ___ ___
.' '. / __/ _ \/ __|/ __|
/ () () \ | (_| (_) \__ \ (__
| | \___\___/|___/\___|
\ / CIS-HARDENED IMAGE BUILDER
'. .'
'. .'
'---.------.---'
Image: t3-cis-level1-20260806-173729
Source: img-test-abc123
OS/Level: tencentos-3 / level1-server
Built: 2026-08-06T17:37:29Z by ohbs-image 0.10.0
[ REPORT ] cat /opt/ohbs-image-REPORT.md (or run: ohbs-image-info)
[ ADMIN ] ssh ohbsimage@<host> (root login disabled per CIS 5.1.22)
[ ESCALATE] sudo -i (NOPASSWD via /etc/sudoers.d/ohbsimage-build)
The report at /opt/ohbs-image-REPORT.md documents what ohbs-image did to the base
image (per-rule counts, outstanding failures, how to re-run the scan) so
the next admin does not have to guess.
Windows pipeline
Windows builds use the Packer ansible provisioner (controller-side) over WinRM. The bundled role includes ohbs_engine.ps1 (PowerShell). The controller requires ansible-core locally.
| Linux | Windows | |
|---|---|---|
| Communicator | SSH | WinRM |
| Packer provisioner | ansible-local (runs in the CVM) |
ansible (controller-side) |
| Engine | ohbs_engine.py |
ohbs_engine.ps1 |
| Controller requirement | none — engine runs on the instance | ansible-core on the build machine |
| Reboot safety net | ohbsimage build user + SSH guard |
WinRM direct (no reboot lockout risk) |
Design
Bundled roles. All 12 ohbs-os engine roles ship inside ohbs_image/roles/. At build time the tool copies the selected role into the workspace. No Galaxy, no network dependency, no version drift.
ansible-local (Linux). Playbooks and roles execute inside the build instance — the Packer controller does not need SSH access into the cloud VPC.
ansible (Windows). Controller-driven over WinRM. The controller must have ansible-core installed locally.
Build-time gate. The gate runs inside the Ansible role (cis_fail_on_findings). Configurable score threshold ensures the image is good enough to ship, or no image is produced.
Credentials. AK/SK via environment variables only (sensitive = true in HCL). Ephemeral instances are tagged and auto-recycled. Image tags record CIS level, OS, and benchmark.
Profiles
Linux (SSH × ansible-local)
| Profile | OS | SSH User | Pkg Manager | Role |
|---|---|---|---|---|
ubuntu2004 |
Ubuntu 20.04 LTS | ubuntu | apt | roles/ohbs-ubuntu2004/ |
ubuntu2204 |
Ubuntu 22.04 LTS | ubuntu | apt | roles/ohbs-ubuntu2204/ |
ubuntu2404 |
Ubuntu 24.04 LTS | ubuntu | apt | roles/ohbs-ubuntu2404/ |
rhel8 |
RHEL 8 | root | dnf | roles/ohbs-rhel8/ |
rhel9 |
RHEL 9 | root | dnf | roles/ohbs-rhel9/ |
rhel10 |
RHEL 10 | root | dnf | roles/ohbs-rhel10/ |
tencentos3 |
TencentOS Server 3 | root | dnf | roles/ohbs-tencentos3/ |
tencentos4 |
TencentOS Server 4 | root | dnf | roles/ohbs-tencentos4/ |
Windows (WinRM × controller-side ansible)
| Profile | OS | User | Role |
|---|---|---|---|
win2016 |
Windows Server 2016 | Administrator | roles/ohbs-win2016/ |
win2019 |
Windows Server 2019 | Administrator | roles/ohbs-win2019/ |
win2022 |
Windows Server 2022 | Administrator | roles/ohbs-win2022/ |
win2025 |
Windows Server 2025 | Administrator | roles/ohbs-win2025/ |
To switch profiles, change [build].profile and source_image_id in ohbs-image.toml.
Test Matrix
Validated CIS-hardened images across the supported OS × level grid.
All builds ran on Tencent Cloud Guangzhou region with cis_allow_disruptive: false;
every image below was re-verified in the console as NORMAL on 2026-08-14.
| OS | Source image | L1 | L2 |
|---|---|---|---|
| RHEL 8 | img-kp3mv36j |
img-8zfwvl9g (93.5%) |
img-4d6jxfe2 (93.3%) |
| RHEL 9 | img-02j8jprl |
img-25hwnzl8 (95.3%) |
img-8mjw35cy (95.2%) |
| RHEL 10 | img-29guuzjp |
img-1idroc9y (96.3%) |
img-lzha2io2 (95.3%) |
| Ubuntu 20.04 | img-22trbn9x |
img-9xyvohdy (92.0%) |
img-gut6728y (90.0%) |
| Ubuntu 22.04 | img-487zeit5 |
img-jd3gct8o (91.5%) |
img-rx4n84w4 (92.1%) |
| Ubuntu 24.04 | img-mmytdhbn |
img-7ncjcq10 (95.9%) |
img-j9m1fn0u (96.5%) |
| TencentOS 3 | img-eb30mz89 |
img-ip62dj1k (95.7%) |
img-joo4xcis (94.2%) |
| TencentOS 4 | img-9qrfy1xt |
img-ipw57gea (96.9%) |
img-fs0hh75w (96.7%) |
| Windows Server 2016 | EN img-1eckhm4t · CN img-9id7emv7 |
EN img-lw9onsqo (99.7%) · CN img-bm2kusug (99.7%) |
EN img-gnedt90i (99.7%) · CN img-4t7nd0ne (99.7%) |
| Windows Server 2019 | EN img-bhvhr6pr · CN img-mmy6qctz |
EN img-9dfarngo (99.6%) · CN img-2h1qdi5c (99.6%) |
EN img-5gfx1ybo (99.7%) · CN img-8u7us60c (99.7%) |
| Windows Server 2022 | EN img-9tzezztj · CN img-m07ny34j |
EN img-b9iwlu30 (99.7%) · CN img-5fwbryp2 (99.7%) |
EN img-8r09mpwq (99.7%) · CN img-q5zih0bo (99.7%) |
| Windows Server 2025 | EN img-eb87lxi3 · CN img-6jb5wacd |
EN img-4obl2vj4 (99.7%) · CN img-pqx9opsw (99.7%) |
EN img-cvoolqiu (99.7%) · CN img-2e5x3xhg (99.7%) |
Scores are the post-reboot re-audit results (all assessed rules, gate ≥ 85). kmod rules are applied via persistent modprobe install-overrides — no rule exclusions are needed at build time. Windows images are member-server builds from the Tencent Cloud Datacenter EN/CN public images, built and re-audited on 2026-08-14; WinRM is re-locked (Basic/unencrypted off, Administrator password randomized) before snapshot. The single remaining Windows fail on every build is "Deny access to this computer from the network → include S-1-5-114" (2.2.2x), which is deliberately skipped as disruptive: applying it would cut off the very WinRM session the build runs on. Enable it post-boot with
cis_allow_disruptive: true.
CI/CD Integration
export TENCENTCLOUD_SECRET_ID=xxx
export TENCENTCLOUD_SECRET_KEY=xxx
ohbs-image build --log-file build.log
Point downstream CVM / Auto Scaling / Terraform at the output image_id. Pin the build machine to a dedicated VPC and security group.
Security model (for enterprise review)
What auditors usually ask about, and where each control lives:
- No long-lived credentials for humans. The person (or pipeline)
triggering a build holds only cloud API permissions — OIDC/STS
short-lived credentials in CI, a least-privilege sub-account, or an
assume_rolechain for group accounts. Nobody needs a VM password or SSH key to run a build. See Group accounts. - Ephemeral, isolated build VM. The build instance lives for the duration of one build (~10 min), sits in a dedicated VPC with a security group source-restricted to the build machine's egress IP, uses a Packer-generated throwaway keypair that is deleted when the build ends, and is terminated automatically — success or failure.
- The build VM runs as root — deliberately. This follows AWS EC2
Image Builder / Azure Image Builder / Packer's own examples, where the
ephemeral build instance is driven as root (or a NOPASSWD-sudo default
user, which is the same privilege set under another name). There is no
production data, no multi-user access, and no persistence on this VM;
least-privilege controls apply to who can trigger the pipeline, not
to a throwaway VM nobody can log into. The shipped artifact is what
matters — and it is hardened: root SSH login is disabled
(
PermitRootLogin no) before the snapshot, per CIS. - Auditable output. Every build records lineage (source image →
output image IDs, score, version), can emit a GPG-signed SLSA-style
provenance statement, an SBOM pinned into the provenance, and
SARIF/XCCDF reports for GRC ingestion. The image itself carries the
audit result (
/opt/ohbs-image-AUDIT-RESULT.json) and a full report (/opt/ohbs-image-REPORT.md).
Group accounts (organization)
ohbs-image supports the Tencent Cloud group-account (企业组织) pattern for cross-account golden image builds — build once from a central account, distribute everywhere:
-
Build as a target account: set
[cloud].assume_role_arnto a CAM role created in the target account. Packer assumes that role with the local AK/SK (STSAssumeRole), so the instance and image are created in the target account while credentials stay in the central account.[cloud] assume_role_arn = "qcs::cam::uin/1234567890:roleName/CrossAccountBuilder" assume_role_session = "ohbsimage-build" # optional assume_role_duration = 3600 # optional, default 7200
The role needs the same CAM permissions the builder requires (
cvm:RunInstances,cvm:CreateImage,cvm:DescribeImages) plus a trust policy allowing the central account to assume it. -
Build then share: keep
assume_role_arnempty, build in the central account, and share the resulting image to business accounts via the Tencent Cloud console or theimage_share_accountsPacker option.
When assume_role_arn is empty (the default) builds behave exactly as
before — no group-account setup required.
OIDC / STS credentials (no long-lived AK/SK)
For CI pipelines (GitHub Actions etc.) you can build without storing any AK/SK: the runner obtains short-lived STS credentials via OIDC federation, and ohbs-image hands the session token straight to Packer.
-
CAM side (one-time): create an OIDC identity provider pointing at
https://token.actions.githubusercontent.com, then create a CAM role whose trust conditions pinoidc:iss,oidc:aud(the client ID you configured) andoidc:sub(e.g.repo:susunola/ohbs-image: ref:refs/heads/main). Attach the builder permissions (cvm:RunInstances,cvm:CreateImage,cvm:DescribeImages, ...). -
Workflow: exchange the OIDC token for STS credentials with
everpcpc/tencentcloud-oidc-auth@v1, which exportsTENCENTCLOUD_SECRET_ID,TENCENTCLOUD_SECRET_KEYandTENCENTCLOUD_SECURITY_TOKEN— Packer reads all three natively:permissions: id-token: write # required for OIDC steps: - uses: everpcpc/tencentcloud-oidc-auth@v1 with: role-arn: qcs::cam::uin/1234567890:roleName/ci-builder oidc-provider-id: github region: ap-guangzhou - run: ohbs-image build --config ohbs-image.toml
-
ohbs-image side: nothing to configure — the default
security_token_env = "TENCENTCLOUD_SECURITY_TOKEN"is picked up automatically. Override it only if your CI exports the token under a different name:[cloud] security_token_env = "MY_CI_STS_TOKEN"
Note: security_token and assume_role are independent — STS credentials
can themselves be scoped to the OIDC role, so you typically do not need
both at once.
Build → test → distribute (image governance)
Beyond building the image, ohbs-image covers the governance loop that Packer itself leaves to you (mirroring AWS Image Builder's build → test → distribute pipeline):
-
Test (before snapshot) — after finalize + re-audit, an instance-level smoke test runs on the live VM before Packer snapshots it:
sshd -Tparses, sshd/auditd active,/dev/shmcarriesnoexec, no weak SSH crypto, journal-upload active (when configured). Any failure aborts the build — no image is produced. Disable with[meta].smoke_test = false. -
Lineage (distribute metadata) — every build appends a record (
~/.ohbs-image/lineage.jsonl): source image → output image IDs, level, region, score, version, timestamp. The full per-rule audit JSON is archived alongside it on the build machine at~/.ohbs-image/reports/<image-name>.json. Query it with:ohbs-image images # recent builds, newest first ohbs-image images --latest # the most recent record ohbs-image cleanup-images --older-than 30 # dry-run: what would be retired ohbs-image cleanup-images --older-than 30 --apply # actually delete
cleanup-imagesretires golden images older than N days (default 30), always keeping the newest build (--keep-latest, default 1). It uses the lineage records to find the image IDs, verifies them viacvm:DescribeImages, deletes viacvm:DeleteImages(stdlib TC3-signed — no extra dependencies), and marks the lineage entriesretired. Credentials come fromTENCENTCLOUD_SECRET_ID/TENCENTCLOUD_SECRET_KEY(optionallyTENCENTCLOUD_SECURITY_TOKEN). Pair with cron/systemd timer for fully automatic retirement. -
Notify (scheduling companion) — post build results to a WeCom group robot. Combine with cron / systemd timer / SCF for scheduled rebuilds:
[notify] webhook = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxxx" on = "failure" # always | success | failure
# systemd timer / cron example — rebuild monthly, only notify on failure 0 3 1 * * ohbs-image build --config /etc/ohbs-image/ohbs-image.toml -y
-
SLSA-style provenance — after a successful build ohbs-image writes a signed provenance statement (
~/.ohbs-image/provenance/…provenance.json) describing exactly what produced the image (source image, profile, level, region, ohbs-image version, score). Tencent CVM images areimg-*artifacts, not OCI images, so cosign container signing does not apply — instead the provenance file is GPG-detached-signed ([sign].gpg_key), giving an auditable, tamper-evident record (SLSA L1 + signed provenance). Verified end-to-end with a real GPG key — tampering with the provenance makes verification fail (gpg: BAD signature).[sign] gpg_key = "ABCDEF0123456789" # your GPG key id/fingerprint
Verify any signed provenance (audit / compliance):
ohbs-image verify --provenance ~/.ohbs-image/provenance/xxx.provenance.json ohbs-image verify --image img-ekny61ig # auto-locate by image ID
Output shows subject (image IDs), profile/level/region/source, builder version, re-audit score, and the GPG signature status (VALID / INVALID / NONE). Exit code is non-zero when the signature is missing or invalid.
-
SBOM + change detection (supply chain) — with
[meta].sbom = truethe build emits a zero-dependency SBOM (/opt/ohbs-image-SBOM.jsonl, native rpm/dpkg query) into the image, and its SHA-256 + package count are pinned in lineage and the provenance statement (sbomSha256/sbomPackageCount) — SLSA L2-style evidence of what exactly shipped.ohbs-image build --skip-if-unchanged/ohbs-image pendingcompare a deterministic input fingerprint (source image, rule catalog hash, benchmark, level, filters) against the last successful lineage record and skip the rebuild when nothing changed — a scheduled-pipeline cost saver.ohbs-image build --skip-if-unchanged # skip if inputs unchanged ohbs-image pending # exit 0 = no rebuild needed, 1 = rebuild
-
Clean-boot verification (
verify-image) — AWS Image Builder runs its test phase on the output image, not the build instance.ohbs-image verify-image --image img-xxxboots a probe instance from the produced image, runs the bundled engine in scan mode on the FRESH boot (catching SELinux relabel stalls, first-boot services, cloud-init reconfiguration), gates on the score, and always terminates the probe.[meta].verify_boot = truechains it automatically after every successful build (Linux only).ohbs-image verify-image --image img-ekny61ig --min-score 85
-
Independent audit (
audit) — the score is no longer only self- reported by the engine that applied the hardening.ohbs-image auditruns a third-party tool and gates on the result, exactly like dev-sec (InSpec) / RHEL (oscap + SCAP content) / ansible-lockdown (Goss):# OpenSCAP — RHEL-family: use the scap-security-guide datastream on target ohbs-image audit --tool oscap --host 1.2.3.4 --ssh-user root \ --datastream /usr/share/xml/scap/ssg/content/ssg-rhel9-ds.xml \ --profile xccdf_org.ssgproject.content_profile_cis --min-score 85 # Chef InSpec — dev-sec baselines (Linux) ohbs-image audit --tool inspec --host 1.2.3.4 --ssh-user root \ --baseline dev-sec/linux-baseline --min-score 85 # HardeningKitty — Windows cross-check (audit runs on the Windows host, # export the CSV, parse it here) ohbs-image audit --tool kitty --parse kitty-audit.csv --min-score 85
Every audit can emit SARIF / XCCDF for GRC ingestion (
--sarif out.sarif --xccdf out.xml).
Post-delivery lifecycle (drift / refresh / deploy trigger)
-
Drift detection — an image is correct at build time, but instances launched from it drift (configs tweaked, packages patched, services changed).
ohbs-image driftre-scans a LIVE instance over SSH and diffs the result against the baseline — the audit result shipped inside the image (/opt/ohbs-image-AUDIT-RESULT.json) or a saved one:ohbs-image drift --host 1.2.3.4 --image img-ekny61ig --min-score 85 # reports: new failing rules / recovered rules / score delta; exit 1 = drift ohbs-image drift --host 1.2.3.4 --save-baseline # persist a custom baseline
-
Vendor image refresh — when the upstream OS image is updated, the golden image should be rebuilt.
ohbs-image check-sourcecompares the source image'sCreatedTimeagainst the last build's lineage record (exit 0 = unchanged, 1 = refreshed); schedule it on a timer ahead ofbuild --skip-if-unchanged:ohbs-image check-source && echo "source unchanged" || ohbs-image build -y
-
Deploy trigger —
[notify].deploy_webhookPOSTs{event: "image.ready", image_id, score, profile, region}on build success, so a new image automatically drives the downstream release (ASG launch-template update, Terraform, CI pipeline) instead of waiting for a human to read the WeCom message. -
Cost control —
[build].spot = truelaunches the ephemeral build VM as a spot (竞价) instance (instance_charge_type=SPOTPAID, up to ~90% cheaper); repossess risk is acceptable for a short-lived build machine. -
Safe cleanup —
cleanup-images --unused-since Nonly deletes images that are NOT shared with other accounts (viaDescribeImageSharePermission), so an image still referenced downstream is never accidentally retired. Fails open (keeps the image) on API errors.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
preflight fails with credential error |
AK/SK not exported | export TENCENTCLOUD_SECRET_ID=... |
validate fails with plugin download |
packer init failed (offline?) |
Run with internet access — Packer caches plugins after first download |
| Packer times out waiting for SSH | SG doesn't allow TCP/22 from build IP | Add inbound rule for your egress IP — preflight now checks this and warns proactively when it can resolve the SG rules and your public IP |
ansible-playbook can't find python3 |
Source image has no Python | Python 3.6+ must be pre-installed |
| Windows build WinRM error | Password not set or TCP/5986 blocked | Export WINRM_PASSWORD + open inbound rule |
| Build passes but score below 85% | Gate threshold too strict for this OS | Adjust cis_min_score in the role, or use Level 1 |
TencentOS 4 apply fails: Module result deserialization failed + missing /tmp/ansible_...payload.zip |
ansible-core ≥ 2.16 (modular ansiballz) caches module payloads in /tmp, which TencentOS 4 sweeps / backs with tmpfs; the reused payload vanishes mid-run |
Fixed in v0.14.4 — the venv wrapper exports TMPDIR=/opt/ohbs-image-ansible/tmp so payloads live on stable root-disk storage |
TencentOS 4 reboot → Packer reconnect i/o timeout for 5+ min |
ssh-guard runs before apply; CIS firewall rules (3.4.x) reload firewalld / switch the active zone, and the new zone has no SSH allow rule → port 22 is DROPped after reboot | Fixed in v0.14.8 — ssh-guard is re-run right before the reboot provisioner, and rules are persisted (nft list ruleset > /etc/sysconfig/nftables.conf, iptables-save > /etc/sysconfig/iptables) |
packer build fails at prepare: Unsupported argument "ansible_env_vars" |
ansible_env_vars only exists on the ansible (non-local) provisioner, not ansible-local |
Fixed in v0.14.4 — TMPDIR is injected via the ansible-playbook venv wrapper instead of an HCL argument |
packer build fails at parse: Missing item separator in main.pkr.hcl |
A missing comma between inline = [...] items — Python silently concatenates the two adjacent strings, HCL then sees one unterminated item |
Fixed in v0.14.14 — comma restored; regression test scans every rendered inline list for missing separators |
TencentOS 4 reboot → i/o timeout even with all-zone firewall rules |
/.autorelabel left by the SELinux-disabled boot; once SELINUX=permissive is written the next boot runs a full early-boot relabel (before sshd) — a multi-minute-to-infinite stall |
Fixed in v0.14.17 — the guard deletes the stale /.autorelabel before reboot (permissive needs no relabel; the mark service only recreates it during a disabled boot) |
Post-reboot scp: /opt/...: Read-only file system (then /root/...) |
TencentOS 4 ships ro entries in fstab; first SELinux enable also makes systemd-remount-fs fail, leaving the whole root fs ro while sshd still comes up |
Fixed in v0.14.18/19 — guard strips ro from /opt and / fstab lines + remounts rw; the boot oneshot force-remounts / before sshd; post-reboot uploads moved to /root |
Smoke test SMOKE FAIL: auditd / /dev/shm / weak SSH crypto on L1 |
Assertions gated on file/unit existence (TOS4 ships many units) or a hand-written "weak" blacklist that contradicts CIS 1.6.5/1.6.6 (hmac-sha1/umac-64/chacha20/aes*-cbc are allowed) | Fixed in v0.14.20-22 — assertions now gate on is-enabled / fstab-applied; crypto check only flags CIS-forbidden algorithms (md5/3des/rc4/blowfish/cast/salsa20) |
Roadmap
- CI pipeline (GitHub Actions + OIDC, zero long-lived AK/SK)
- Image governance loop: smoke test / lineage / notifications / SLSA signing
-
ohbs-image list— enumerate available profiles with metadata -
ohbs-image scan— audit-only mode (no remediation, gate on findings) - Custom rule selection (
rules_include/rules_excludeinohbs-image.toml) - PyPI package (
pip install ohbs-image) — publish workflow included - Automatic image cleanup (retire old images by lineage age)
- Independent audit tool (
ohbs-image audit— oscap / inspec / kitty) - Benchmark-pinned rule IDs in engine output + SARIF (CIS-CAT cross-reference)
- Clean-boot verification (
ohbs-image verify-image/[meta].verify_boot) - Per-control overrides (
[cis].overridesinohbs-image.toml) - CVE scan gate + SBOM emission (
[meta].cve_scan/[meta].sbom) - Change detection (
ohbs-image pending/build --skip-if-unchanged) - XCCDF 1.2 report export (
scan --xccdf, audit--xccdf) - Cross-account image sharing (
[image].share_accounts) - SBOM pinning in provenance + lineage (SLSA L2-style evidence)
- Windows cross-check via HardeningKitty CSV (
audit --tool kitty) - Config drift detection (
ohbs-image driftvs the image baseline) - User test components (
[meta].test_components, Image Builder style) - Deploy trigger webhook (
[notify].deploy_webhook, EventBridge style) - Spot-instance build VM (
[build].spot, up to ~90% cheaper) - Safe cleanup (
cleanup-images --unused-since, shared images kept) - Org-level sharing (
[image].share_org_units) - Rule-set versioning (
ohbs-image list --versions) - Vendor image refresh detection (
ohbs-image check-source) - SLSA L2: fully reproducible builds (pinned build environment)
- STIG benchmark profiles (same engine, DISA content — roadmap)
Contributing
Bug reports and pull requests are welcome. See CONTRIBUTING.md for the development setup, lint/type-check/test commands, the project's hard constraints (zero third-party runtime dependencies, no long-lived credentials), and the guide for adding a new CIS profile.
Keep the docs in sync with the CLI
CI enforces that README.md always documents every subcommand and OS profile
(.github/workflows/ci.yml → scripts/check_readme.py). When you add, remove,
or rename a ohbs-image subcommand or a profile, update the relevant section
of README.md, then verify locally before pushing:
python3 scripts/check_readme.py # exit 0 = docs current, 1 = missing items
The script reports exactly which subcommands/profiles README.md is missing, so you can fix the docs in one pass rather than watching CI fail.
Validate in a clean Docker environment
To avoid depending on your local Python state, you can also run the same check in an isolated container (installs ohbs-image from a freshly built wheel):
# Build the image; the build itself runs check_readme.py, so it succeeds only
# if README.md is current.
docker build -t ohbs-image:check-readme .
# Re-check a modified checkout without rebuilding:
docker run --rm -v "$(pwd):/app" ohbs-image:check-readme
The container exit code matches the script: 0 = docs current, 1 = missing
items (the missing subcommands/profiles are printed to stderr).
CIS Benchmarks Disclaimer
Independent project — ohbs-image is not affiliated with, sponsored by, or endorsed by the Center for Internet Security (CIS).
This tool applies hardening rules from CIS Benchmark recommendations. CIS Benchmarks are developed and maintained by the Center for Internet Security (CIS). The ohbs-os engine roles bundled in this repository are derived from susunola/ohbs-os and are provided under their respective licenses.
Running CIS hardening in apply mode modifies system configuration and may affect application compatibility. Always test hardened images in a staging environment before production use. Neither the CIS organization nor the authors of this tool guarantee complete compliance — official audit requires independent assessment using CIS-CAT or equivalent tools.
License
MIT — see LICENSE.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file ohbs_image-0.16.26.tar.gz.
File metadata
- Download URL: ohbs_image-0.16.26.tar.gz
- Upload date:
- Size: 1.8 MB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b3bd8e5114582d4eaad3d6eb24ab99da242ed12f5667c8b906b620de49581378
|
|
| MD5 |
baace435a4495a43593a7bca9b932159
|
|
| BLAKE2b-256 |
28155c2cb96190a22fa62ed36e7bccc53c747b2474d20e145ae06042d0c1d804
|
File details
Details for the file ohbs_image-0.16.26-py3-none-any.whl.
File metadata
- Download URL: ohbs_image-0.16.26-py3-none-any.whl
- Upload date:
- Size: 1.7 MB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
359294bfb6f19efa0618dc7ae245a2c271bbb59235210940d37b36b45fdc4b6d
|
|
| MD5 |
fe7a655ec3cc37d4f88b69c9ef1a8537
|
|
| BLAKE2b-256 |
06ee08a73ae046115a95151eb1f57d9590c9b6f268c5718a23d398195d1459cd
|