English | 简体中文 | 日本語 | ภาษาไทย
Part of the oh baseline hardening suite — 生死看淡,不服就干
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. 13 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 provenance) — 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
Zero-to-first-image in one command (recommended)
export TENCENTCLOUD_SECRET_ID=AKIDxxxx
export TENCENTCLOUD_SECRET_KEY=xxxx
ohbs-image quickstart --region ap-guangzhou --profile ubuntu2204 \
--ingress-cidr 203.0.113.10/32
# 1. discovers the newest Ubuntu 22.04 public image + an in-stock instance type
# 2. provisions temporary VPC/subnet/security-group (tagged ephemeral=true)
# 3. writes ohbs-image.toml, then runs doctor + plan
# 4. prints the exact build command — or pass --yes to chain build directly
ohbs-image build --config ohbs-image.toml # produce the hardened image
When it creates temporary networking, quickstart requires --ingress-cidr
and opens SSH/WinRM only to that IPv4 network; use the build runner's public
address with /32. It never permits 0.0.0.0/0. quickstart reuses the
networking you already have by passing all three of
--vpc, --subnet, --security-group (never carves a subnet inside someone
else's VPC). Temporary resources are recorded in ohbs-image.toml.quickstart.json
and torn down with ohbs-image quickstart --cleanup. Preview everything
read-only first with ohbs-image quickstart --region … --ingress-cidr … --dry-run.
Prefer the manual path below when you want to learn the configuration model first, or when your networking is shared and you need explicit choices.
Manual path (learn the config model)
# 1. Install
git clone https://github.com/susunola/ohbs-image.git
cd ohbs-image
pip install .
# 2. Generate a minimal configuration (interactive; flags also supported)
ohbs-image configure
# 3. Build
ohbs-image doctor # actionable local/config/cloud diagnosis
ohbs-image doctor --offline --support-bundle ./ohbs-support.zip # redacted support archive
ohbs-image plan # read-only resource, gate, duration, and cost preview
ohbs-image validate # dry-run: render templates + packer validate
ohbs-image build # produce the hardened custom image
ohbs-image clean # remove build artifacts
The support bundle is created with mode 0600, refuses to overwrite an
existing file, and contains only allow-listed redacted diagnostics, system
metadata, and SHA-256 member hashes. See
Troubleshooting and support bundles.
No Tencent Cloud account yet? Try before you build — free.
ohbs-image tryruns the same engine + catalog gates CI runs, then renders a sample single-page HTML compliance report, entirely offline:ohbs-image try # demo report in ./ohbs-image-try/ # ...or inside the shipped container image: docker build --target try -t ohbs-image:try . && \ docker run --rm -v "$(pwd)/out:/demo/out" ohbs-image:tryIt touches no cloud resources and spends nothing; a real
buildadds the ephemeral CVM, remediation, clean-boot verification and signed provenance.
# 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.22.0 — tencentos3 (L1) → ap-guangzhou-4
══════════════════════════════════════════════════════════
[packer] tencentcloud-cvm: Launching instance (S5.MEDIUM2)...
[packer] tencentcloud-cvm: Provisioning with ansible-local...
[packer] tencentcloud-cvm: TASK [cis-tencentos3 : apply CIS Level 1] ***
[packer] tencentcloud-cvm: ok: 142 changed: 38 failed: 0
[packer] tencentcloud-cvm: TASK [cis-tencentos3 : reboot] ************
[packer] tencentcloud-cvm: Instance rebooted — re-auditing pending items
[packer] tencentcloud-cvm: TASK [cis-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 -r requirements-builder.yml (Windows builds only; version locked) |
| 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/5985 (Windows, NTLM message encryption), 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
Not sure where to begin? Ask the CLI for the path that matches your role:
ohbs-image guide # list the four product journeys
ohbs-image guide builder # first hardened image, end to end
ohbs-image guide security # assessment, policy and evidence
ohbs-image guide platform # state, promotion, distribution and recovery
ohbs-image guide consumer # resolve and verify deployable images
guide is offline and configuration-free. It returns task-oriented steps by
default and a stable machine-readable contract with --output json.
Cloud integrations use a versioned plugin contract. Inspect the built-in
Tencent Cloud adapter and any installed third-party adapters with
ohbs-image provider list, then validate one with
ohbs-image provider verify PROVIDER. See
docs/provider-plugin.md for the entry-point API.
Scanner, signer, notifier, vulnerability-feed and distributor integrations use
the separately versioned Extension SDK. Discover them with
ohbs-image extension list and run the mandatory network-free certification
with ohbs-image extension verify KIND NAME; see
docs/extension-sdk.md.
Performance-sensitive controller paths have a reproducible, cloud-free public
benchmark: ohbs-image benchmark run --output benchmark.json. Compare a run
against a same-runner baseline with ohbs-image benchmark compare; methodology
and interpretation guidance live in docs/benchmark.md.
Long-running production claims use a tamper-evident daily ledger. Record and
verify it with ohbs-image proof record|verify|report; incomplete coverage is
reported as incomplete rather than promoted as evidence. See
docs/production-proof.md.
Publish portable acceptance, SLO, proof, benchmark and release artifacts as a
hash-addressed static index with scripts/build_evidence_index.py; see
docs/public-evidence-index.md. Stable CLI,
OpenAPI, schema and plugin boundaries are protected by an explicit contract
snapshot documented in
docs/core-contract-stability.md.
Generate technical 等保 2.0 or 信创 readiness evidence with
ohbs-image compliance assess. Reports preserve missing/manual controls and
never claim certification; scope and limitations are documented in
docs/compliance-packs.md.
Before an upgrade, run ohbs-image upgrade check TARGET_VERSION, back up the
state database, and verify the release attestation, checksums and CycloneDX
SBOM. Production Docker, Compose, systemd and Kubernetes baselines plus the
rollback procedure are documented in
docs/production-deployment.md.
Which command should I use?
Four commands overlap at first glance; they serve four different moments of the image lifecycle — pick by what you are trying to do:
| If you want to… | Use | What it does |
|---|---|---|
| Harden a source image into a golden image (apply remediations, re-audit, sign) | build |
render + packer build → image + provenance + HTML delivery report |
| Check a source image against the benchmark without changing anything | scan |
same engine, audit-only; score gate (default 85%); SARIF/XCCDF/HTML export |
| Independently verify with a third-party tool (OpenSCAP / InSpec / HardeningKitty) | audit |
external audit tool, same --min-score gate, same SARIF/XCCDF export |
| Confirm a produced image still passes after a clean boot | verify image |
boots a probe from the image and re-audits it |
| Check an already-running instance against the baseline | drift |
config drift on a live host vs the image baseline |
build and scan run the identical bundled engine — scan is simply
build with remediation switched off, which is why its score is a fair
"before" snapshot for a migration or a quarterly compliance review.
Choose a path, then use these five commands 90% of the time
New to ohbs-image? Ignore the full reference below; start here:
ohbs-image guide # choose the shortest path for your role
ohbs-image init # generate ohbs-image.toml (interactive)
ohbs-image preflight # validate config + credentials before spending money
ohbs-image build # produce the hardened golden image
ohbs-image images # list what you have built (lineage)
ohbs-image scan # audit-only check when you just need a score
Everything else — config tooling, report evidence, state management,
cleanup-* hygiene, audit cross-checks — is there when you need it, and
documented in the full reference below.
Full command reference
ohbs-image # show help (exits 2)
ohbs-image guide [builder|security|platform|consumer] [--output text|json]
ohbs-image try [-o DIR] [--profile P] [--level 1|2] # zero-cost offline demo: gates + sample HTML report
ohbs-image quickstart --region R --profile P --ingress-cidr IP/32 # temp resources + config + doctor + plan
ohbs-image quickstart --dry-run # read-only: print the plan, create nothing
ohbs-image quickstart --cleanup # delete resources recorded by a previous quickstart
ohbs-image launch [--build --yes] # doctor → plan → preflight → explicitly approved build
ohbs-image init # generate ohbs-image.toml
ohbs-image configure # interactive/non-interactive minimal config generator
ohbs-image discover images --region ap-guangzhou --profile ubuntu2404
ohbs-image config schema # JSON Schema for editor/CI validation
ohbs-image config validate # local-only config check (no cloud access)
ohbs-image config diff a.toml b.toml # field-level config comparison
ohbs-image config get ohbs.level # effective value of one key (defaults applied)
ohbs-image config explain --all # full configuration key reference
ohbs-image config migrate --apply # atomic legacy config migration
ohbs-image config merge base.toml env.toml # deep-merge layered configs (validate result)
ohbs-image report diff --before RUN --after RUN # compare lineage metadata
ohbs-image report list [--profile P] [--status ok|failed] [--limit N]
ohbs-image report show RUN_ID # single-run evidence + run manifest
ohbs-image report html RUN_ID [-o FILE] # re-render one run as a self-contained HTML page
ohbs-image report cost [--hourly-price USD] # aggregate build spend from lineage facts (no billing API; spot at 10%)
ohbs-image run list [--profile P] [--status S] # unified runs across lineage, plans and manifests
ohbs-image run show RUN_ID # all evidence associated with one run
ohbs-image run events RUN_ID [--output json] # immutable, hash-chained state timeline
ohbs-image run resume RUN_ID [--build --yes] # continue from the last safe launch checkpoint
ohbs-image run checkpoints RUN_ID [--output json] # fine-grained build phase checkpoints
ohbs-image report slo [--days 30] [--output json] # run success, retry and latency SLOs
ohbs-image dr drill --help # isolated disaster-recovery/chaos exercise
ohbs-image report metrics --format prometheus # Prometheus exposition format
ohbs-image report metrics --format otlp-json # OTLP/JSON metrics document
Versioned domain contracts live in `schemas/v1/`; the control-plane HTTP contract is
`api/openapi.yaml`. Compatibility rules are documented in `schemas/COMPATIBILITY.md`.
ohbs-image registry rebuild [--output json] # index approved release evidence
ohbs-image registry list [--bucket PROFILE] [--output json]
ohbs-image registry show IMAGE_ID [--output json]
ohbs-image registry verify [--output json]
ohbs-image registry quarantine IMAGE_ID --reason TEXT # isolate + auto-rollback channels
ohbs-image registry revoke IMAGE_ID --reason TEXT # permanent revoke + auto-rollback
ohbs-image ancestry descendants IMAGE_ID [--output json]
ohbs-image ancestry impact IMAGE_ID [--output json] # artifacts + channels blast radius
ohbs-image ancestry revoke IMAGE_ID --reason TEXT # dry run; add --apply to cascade
ohbs-image ancestry link CHILD PARENT [--external] # add a derived-from edge
ohbs-image ancestry verify [--output json] # cycles and malformed edges
ohbs-image event process rebuild-event.json # dry-run impact + rebuild plan
ohbs-image event process rebuild-event.json --apply # quarantine + rollback + queued requests
ohbs-image worker run --handler ./rebuild-handler --once # dry-run queue inspection
ohbs-image worker run --handler ./rebuild-handler --once --apply # lease + retry + dead-letter
ohbs-image worker run --pipeline docs/rebuild-pipeline.example.json --once --apply # built-in 4-stage pipeline
ohbs-image cve sync --inventory artifact-packages.json --apply # OSV feed -> quarantine -> rebuild queue
ohbs-image serve --rbac rbac.json # API + Web Console at http://127.0.0.1:8181/
The service exposes public health checks at `/healthz` and `/api/v1/health`.
Authenticated v1 endpoints include paginated Artifact and Run queries,
Artifact impact analysis, Channel resolution/promotion, rebuild requests, and
Prometheus metrics. Responses are filtered by the caller's authorized Buckets;
errors use a stable `{error: {code, message}}` envelope.
For production, use the hardened systemd and Caddy examples in `deploy/` and
follow `docs/control-plane-production.md`; keep the application server on
loopback and expose it only through the TLS boundary.
ohbs-image channel promote PROFILE stable IMAGE_ID [--expected-generation N] # atomic CAS pointer move
ohbs-image channel promote PROFILE stable IMAGE_ID --operation-id deploy-42 # idempotent worker retry + fencing
ohbs-image channel resolve PROFILE stable [--output json] # verify pointer + artifact integrity
ohbs-image channel list [--bucket PROFILE] [--output json]
ohbs-image policy verify organization-policy.json
ohbs-image policy explain organization-policy.json --environment production --artifact-id IMAGE_ID
ohbs-image policy check IMAGE_ID --bundle organization-policy.json --environment production
ohbs-image policy simulate new-policy.json --environment production # dry run: no writes, no quarantine
ohbs-image policy simulate new-policy.json --environment production --baseline current.json # diff transitions
ohbs-image policy simulate new-policy.json --environment production --fail-on-newly-denied # CI gate
ohbs-image policy exceptions organization-policy.json # active / expiring / expired
ohbs-image policy exceptions organization-policy.json --within-days 14 # flag what lapses soon
ohbs-image policy exceptions organization-policy.json --fail-on-expired # CI gate on lapsed waivers
ohbs-image consumer resolve PROFILE stable --policy organization-policy.json --environment production
ohbs-image consumer resolve PROFILE stable --policy organization-policy.json --output terraform
ohbs-image channel promote PROFILE stable IMAGE_ID --policy organization-policy.json # enforced gate
ohbs-image distribution plan IMAGE_ID --region ap-shanghai --region ap-beijing # no cloud writes
ohbs-image distribution execute IMAGE_ID --region ap-shanghai # dry-run
ohbs-image distribution execute IMAGE_ID --region ap-shanghai --apply # SyncImages; records pending
ohbs-image distribution reconcile IMAGE_ID --timeout-minutes 60 # DescribeImages → ready/failed
ohbs-image distribution record IMAGE_ID --region ap-shanghai --replica-id img-copy --operation-id copy-job-7
ohbs-image engine list # bundled engines: version + sha256 per profile
ohbs-image engine verify # syntax-check every bundled engine (CI gate)
ohbs-image engine version # ohbs-image + per-family engine versions
ohbs-image catalog list # bundled rule catalogs: rules, guidance, sha256
ohbs-image catalog verify [--strict] # catalog JSON + guidance cross-reference gate
ohbs-image catalog lint --report cis-rule-quality.html # quality baseline + top 50 priorities
ohbs-image doctor [--output text|json|sarif] [--only GROUP] [--offline] [--report-path FILE]
ohbs-image plan [--output json] # read-only build/resource/gate preview
ohbs-image state path # print the evidence directory
ohbs-image state status [--output json] # evidence counts + disk usage
ohbs-image state verify [--strict] [--output json] # integrity, references, hashes and leases
ohbs-image state init # create the evidence layout (idempotent)
ohbs-image state prune --keep 30 [--dry-run] # retain recent lineage, drop old per-run evidence
ohbs-image state prune --older-than 90 [--dry-run]
ohbs-image state sync push --backend local --location /shared/ohbs-state
ohbs-image state reconcile [--apply] [--output json] # expired leases + orphan resource records
ohbs-image state sync push --backend cos --location cos://bucket/ohbs-state
ohbs-image state sync push --backend local --location /shared/ohbs-state --check
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 scan --html report.html # ... plus a self-contained HTML compliance page
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 promote --image img-xxx --environment staging --approved-by alice
ohbs-image rollback --image img-xxx --environment staging --reason "deployment issue"
ohbs-image pending # change detection: is a rebuild required? (exit 0/1)
ohbs-image verify provenance --provenance <file> # verify a SLSA provenance signature
ohbs-image verify provenance --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 verify release --image img-xxx # verify portable release-manifest evidence hashes
ohbs-image cleanup images [--older-than 30] # retire old images by lineage age (dry run)
ohbs-image cleanup images --apply # actually delete (default = dry run)
ohbs-image cleanup runs --older-than 24 # find tagged orphaned build/probe CVMs (dry run)
ohbs-image cleanup runs --older-than 24 --apply # actually terminate the tagged CVMs (hours must be > 0)
ohbs-image cleanup runs --include-legacy --apply # explicitly include pre-manifest probes after review
# --- deprecated flat aliases ---
# These names are frozen in contracts/core-contracts.json as top-level commands,
# and docs/core-contract-stability.md requires a new major contract version plus a
# documented migration path to remove one. They therefore stay supported through
# the 0.x line and will be removed in 1.0.0, not in a minor release.
ohbs-image verify --provenance <file> # [deprecated] use `ohbs-image verify provenance`
ohbs-image verify-image --image <img-id> # [deprecated] use `ohbs-image verify image`
ohbs-image verify-release --image img-xxx # [deprecated] use `ohbs-image verify release`
ohbs-image cleanup-images [--older-than 30] # [deprecated] use `ohbs-image cleanup images`
ohbs-image cleanup-runs --older-than 24 # [deprecated] use `ohbs-image cleanup runs`
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/
Policy bundles support relative extends inheritance, environment overrides, and
time-bounded exceptions requiring an owner, approver, reason, and expiry. Start from
docs/policy-bundle.example.json; remove the example
exception before use. Every check writes a hash-protected decision under the registry state.
| Flag | Applies to | Description |
|---|---|---|
--config <path> |
all | Config file path (default ./ohbs-image.toml) |
--overlay <toml> |
all commands that accept --config |
Additional config file layered over --config (repeatable; later files win key-by-key) |
--workdir <dir> |
all | Build output directory (default ./.ohbs-image-build) |
--state-dir <dir> |
all state-aware commands | Evidence state directory (or OHBS_IMAGE_STATE_DIR; it may precede the command) |
--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 |
--result-file <path> |
build | Write one atomic JSON result contract for CI/CD |
--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) |
--html <path> |
scan | Write a self-contained HTML compliance report (single page, no external assets) |
--hourly-price <usd> |
report cost | On-demand USD/hour used to estimate build spend from recorded durations; spot runs are billed at 10% (default: report the facts only, no estimate) |
--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) |
--older-than <hours> |
cleanup runs | Retire tagged ephemeral CVMs older than N hours (default 24) |
--include-legacy |
cleanup runs | Include old probes without a run manifest (off by default) |
--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; the in-use guard expires N days after the lineage record — older shared images are presumed unused and retired anyway (0 = off) |
--apply |
cleanup images | Actually delete (default is a dry run) |
First-success workflow
configure generates the smallest valid configuration. In a TTY it prompts
for missing values; automation can supply --profile, --region, --zone,
--source-image, --vpc, --subnet, and --security-group.
doctor returns all detected problems in one run, with a suggested fix for
each blocker. Checks are grouped into toolchain / config / credentials /
cloud / network / permissions; --only <group> runs a single group,
--offline (or --no-cloud) disables cloud API checks for air-gapped
machines, --output json emits the stable doctor/v1 contract,
--output sarif emits a SARIF 2.1.0 report, and --report-path <file>
persists a redacted copy of the same output. Exit codes are stable:
0 = ready to build, 1 = at least one failing check, 2 = configuration
could not be resolved. Every emitted string is redacted — credential-shaped
values (AKID…, secret_key=…, PEM private keys) never leave the process.
plan never creates or changes cloud resources. It previews the temporary
CVM, placement, maximum duration, release gates, distribution and cost caveat.
Team state and cloud Canary
state manages the evidence directory (OHBS_IMAGE_STATE_DIR or
~/.ohbs-image): state path prints it, state status reports evidence
counts and disk usage, state init creates the layout idempotently (safe in
CI), state verify checks JSON structure, RUN_ID links, release evidence
hashes and active leases, and state prune retains recent lineage while dropping superseded
per-run evidence (runs/plans/provenance — the permanent release approval
trail in releases/ is never pruned). Use --dry-run to preview.
state sync copies the evidence directory to/from a local team directory or
Tencent COS. The COS backend uses the official coscli binary and its
credential/config mechanism, so secrets are never passed on the command line.
--check previews transfers without copying (local backend only).
Orchestration should pull before a state-aware operation and push afterwards.
Set OHBS_IMAGE_COSCLI_CONFIG when CI uses a non-default coscli
configuration file.
The opt-in .github/workflows/cloud-canary.yml runs a weekly real-cloud
acceptance (TencentOS 3 L1 by default) only when repository variable
OHBS_ENABLE_CLOUD_CANARY=true. It is disabled by default because it creates
billed CVMs; manual runs require explicit cost confirmation. Manual dispatch
additionally accepts profile (tencentos3 / ubuntu2404 / win2022),
level (1/2) and
build_instance_type (default SA5.MEDIUM2) so a canary can be targeted
without editing the workflow. Scheduled runs can rotate the representative
profile through repository variable OHBS_CLOUD_CANARY_PROFILE.
Configuration
ohbs-image.toml is the single source of truth — no manual template editing.
Validation is strict about types: list options (rules_include,
rules_exclude, share_accounts, share_org_units, test_components) must
be TOML arrays of non-empty strings, and level, min_score, assume_role_duration, ssh_port
must be integers (floats and booleans are rejected). The hardening section is
[ohbs] (what ohbs-image init generates); the legacy [cis] name is still
accepted — if both exist, [ohbs] wins with a warning.
Configuration files can be layered for environment-specific overrides:
pass --overlay <file> on any command (repeatable) or use
ohbs-image config merge base.toml team.toml local.toml to preview the
merged result. Layers deep-merge: tables merge recursively and later files
win key-by-key; lists and scalars are replaced, not appended. Overlay files
may be partial — only the merged result must be complete and valid.
[build]
profile = "tencentos3"
# Linux: ubuntu2004 | ubuntu2204 | ubuntu2404 |
# rhel8 | rhel9 | rhel10 | rocky9 |
# 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 = false # use a private VPC runner or bastion for builds
# max_build_minutes = 120 # hard Packer wall-clock limit; stops stalled builds and caps cloud cost (15-1440)
# 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 = ["ou-xxxx"] # NOT supported: ModifyImageSharePermission takes
# account IDs only — the tool warns and skips this
[ohbs]
level = 1 # 1 or 2
# min_score = 85 # post-reboot audit gate (0 disables; default 85)
# allow_disruptive = true # apply disruptive remediations during the build (default true)
# rules_include = ["1.5.6"] # run only these rules
# rules_exclude = ["1.1.2.2.4"] # always wins over rules_include
# allow_scoped_approval = true # explicitly permit a subset-built image to be approved (default false)
# Per-control parameter overrides (deep-merged into the catalog at render):
# [ohbs.overrides."5.2.2"]
#
# Explicit evidence for a site-specific Manual control. A control is only
# promoted to pass when all approval fields are present; otherwise it remains
# Manual. The attestation is embedded in the per-rule JSON evidence.
# [site_policy.controls."2.1.24"]
# approved = true
# reason = "Listening services match SEC-NET-004 allowlist"
# owner = "security-platform"
# reviewed_at = "2026-08-27"
# 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
# delivery_report_required = false # optional: fail release if the HTML delivery report cannot be written
# 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 | |
max_build_minutes |
int | Hard wall-clock limit for the complete Packer operation — plugin init retries and build/test/scan — (default 120, range 15–1440); timed-out Packer is terminated and the normal failure/cleanup path runs |
|
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 | Not supported — ModifyImageSharePermission accepts account IDs only; the tool warns and skips this option (use share_accounts) |
|
[ohbs] |
level |
int | 1 (Level 1) or 2 (Level 2) |
min_score |
int | Post-reboot audit gate (default 85; 0 disables) |
|
allow_disruptive |
bool | Apply disruptive remediations during the build — mount options, service removals, etc. (default true; the build VM is ephemeral and rebooted before the audit, so these are safe) |
|
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 |
|
allow_scoped_approval |
bool | Explicitly permit approval of an image built from a rule subset (default false) |
|
overrides |
table | Per-control parameter overrides, keyed by rule ID — deep-merged into the catalog at render time (e.g. [ohbs.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; current TencentOS 3/4 public images: 22) |
|
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) |
|
delivery_report_required |
bool | Fail release when the HTML delivery report cannot be archived (default false) |
|
verify_boot |
bool | After the snapshot, boot a probe instance from the produced image, re-audit every CIS rule on fresh boot and gate (SSH for Linux; NTLM WinRM for Windows; 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) |
[attestation] |
required |
bool | Require a valid provenance signature before approval, sharing or deployment (defaults to true when gpg_key is set) |
Architecture
Linux pipeline
Four phases executed inside the ephemeral CVM. Packer uploads one compressed
Ansible bundle, extracts it, and runs ansible-playbook locally:
- 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>
.---..---.
.-' '-. OHBS IMAGE
.' '. ___ ___ ___ ___
.' '. / __/ _ \/ __|/ __|
/ () () \ | (_| (_) \__ \ (__
| | \___\___/|___/\___|
\ / OHBS-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 | single file upload + local ansible-playbook |
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 13 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.
Local Ansible (Linux). Packer uploads the complete payload as one archive;
playbooks and roles then execute inside the build instance. This avoids recursive
upload stalls while keeping the controller free of an ansible-core dependency.
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 × local Ansible)
| Profile | OS | SSH User | Pkg Manager | Role |
|---|---|---|---|---|
ubuntu2004 |
Ubuntu 20.04 LTS | ubuntu | apt | roles/cis-ubuntu2004/ |
ubuntu2204 |
Ubuntu 22.04 LTS | ubuntu | apt | roles/cis-ubuntu2204/ |
ubuntu2404 |
Ubuntu 24.04 LTS | ubuntu | apt | roles/cis-ubuntu2404/ |
rhel8 |
RHEL 8 | root | dnf | roles/cis-rhel8/ |
rhel9 |
RHEL 9 | root | dnf | roles/cis-rhel9/ |
rhel10 |
RHEL 10 | root | dnf | roles/cis-rhel10/ |
rocky9 |
Rocky Linux 9 | root | dnf | roles/cis-rocky9/ |
tencentos3 |
TencentOS Server 3 | root | dnf | roles/cis-tencentos3/ |
tencentos4 |
TencentOS Server 4 | root | dnf | roles/cis-tencentos4/ |
Windows (WinRM × controller-side ansible)
| Profile | OS | User | Role |
|---|---|---|---|
win2016 |
Windows Server 2016 | Administrator | roles/cis-win2016/ |
win2019 |
Windows Server 2019 | Administrator | roles/cis-win2019/ |
win2022 |
Windows Server 2022 | Administrator | roles/cis-win2022/ |
win2025 |
Windows Server 2025 | Administrator | roles/cis-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.
Full matrix — including the CVM instance type used per build — is in tests/TEST-MATRIX.md.
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
Run the workflow from a self-hosted runner with private connectivity to the build VPC. Keep
associate_public_ip = falseand restrict the build security group to the runner or bastion security group; do not open SSH or WinRM to the public internet for a golden-image build. -
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, and amodefield (build/scan/test). Scan and test images are recorded socleanup imagescan retire them, but they are not hardened builds —--skip-if-unchangedandpendingignore them (onlymode: buildrecords count; records written before the field existed count asbuild). The full per-rule audit JSON is archived alongside it on the build machine at~/.ohbs-image/reports/<image-name>.<run-id>.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 [attestation] required = true # fail closed: never distribute an unsigned build
Verify any signed provenance (audit / compliance):
ohbs-image verify provenance --provenance ~/.ohbs-image/provenance/xxx.provenance.json ohbs-image verify provenance --image img-ekny61ig # auto-locate by image ID ohbs-image verify provenance --provenance ~/.ohbs-image/provenance/xxx.provenance.json \ --trusted-key-fingerprint ABCDEF0123456789ABCDEF0123456789ABCDEF01
Output shows subject (image IDs), profile/level/region/source, builder version, re-audit score, and the GPG signature status (VALID / INVALID / NONE).
--trusted-key-fingerprintmay be repeated to form a signer allowlist. Exit code is non-zero when the signature is missing, invalid, cannot be checked because GPG is unavailable or times out, the signer is not allowlisted, or--imageis not a subject of the provenance.With
[attestation].required = true, a missing/failed/timed-out signature blocks approval, cross-account sharing, and the deploy webhook. For CI/CD,ohbs-image build --result-file result.jsonwrites one atomic JSON document containing the run ID, image IDs, score, evidence paths, and signature state. Evidence is stored with owner-only permissions under~/.ohbs-image; each build receives a UUID run ID, so concurrent builds do not reuse provenance filenames or invocation IDs.Webhook endpoints must use HTTPS and cannot use literal non-public IP addresses. DNS webhook hosts remain administrator-controlled integrations; production networks should also enforce an egress allowlist. Deploy events carry the build
run_idas bothevent_idandIdempotency-Key; delivery retries transient failures three times. Use--state-dir /secure/ohbs-state(orOHBS_IMAGE_STATE_DIR) to isolate CI jobs or retain team evidence outside a runner home directory.WeCom notifications are intentionally human-facing, best-effort messages: they do not gate release and are not a delivery receipt. Automation must use the result JSON and/or deploy webhook, both correlated by
run_id.Every build and clean-boot probe also writes a versioned run manifest under
runs/<run_id>.json. It records the active lease, phase, and known temporary resources. Long-running Packer builds refresh that lease every five minutes.cleanup runsskips unexpired active leases; it only treats age as a fallback for expired or pre-manifest resources.[build.packer]remains a privileged provider-extension escape hatch. Its values are not copied into provenance, but the exact sorted key set and a SHA-256 digest of the complete override map are recorded for audit.An approved image also receives a self-contained HTML delivery report beside the archived audit JSON in
reports/. It summarizes release status, score, image IDs, run identity and evidence paths; when--result-fileis used, its path is returned ashtml_reportfor CI links and artifacts.Every approved image also receives a release manifest under
releases/<image-id>.json. It binds the image to hashes of its audit, provenance and HTML evidence, then records a cloud-agnostic promotion trail.ohbs-image promote --image img-xxx --environment stagingandohbs-image rollback --image img-xxx --environment stagingonly update this auditable release state; application deployment and cloud sharing remain explicit external pipeline actions.Before a downstream pipeline consumes an image, run
ohbs-image verify release --image img-xxxagainst the downloaded evidence state. It verifies that each referenced audit, provenance, and HTML report remains under the state root and matches its recorded SHA-256.
Real cloud acceptance and cleanup drills
Three protected GitHub workflows turn the operational checks into repeatable, reviewable runs without creating surprise cloud spend:
real-cloud-acceptancerequires the protectedcloud-e2eenvironment and an explicit cost confirmation. It runs exactly one profile/level through a real build and clean-boot verification. ConfigureTC_E2E_OIDC_ROLE_ARN,TC_E2E_REGION,TC_E2E_ZONE,TC_E2E_VPC_ID,TC_E2E_SUBNET_ID,TC_E2E_SG_ID, andTC_E2E_SOURCE_IMAGE_IDas environment secrets. Windows also needsTC_E2E_WINRM_PASSWORD.ephemeral-cleanup-drillis dry-run by default. Itsapplyinput is the only path that terminates tagged ephemeral CVMs; protect the same environment with reviewers and inspect the dry-run artifact first.cloud-canaryadds a weekly TencentOS 3 L1 acceptance, but its scheduled job remains inert until repository variableOHBS_ENABLE_CLOUD_CANARY=true. Manual runs require cost confirmation and may select TencentOS 3, Ubuntu 24.04 or Windows Server 2022. ConfigureTC_CANARY_TENCENTOS3_IMAGE_ID,TC_CANARY_UBUNTU2404_IMAGE_IDandTC_CANARY_WIN2022_IMAGE_IDin the same protected environment.
Every acceptance run writes a portable acceptance-result.json plus a GitHub
run summary containing profile, level, commit, timestamps, result and evidence
artifact. Failed builds still publish this diagnostic record before the job is
marked failed. All workflows use short-lived OIDC credentials and keep their
logs/evidence as 90-day artifacts. They are intentionally manual until the
organization has accepted the associated cloud cost and change-control policy.
-
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 successfulmode: buildlineage 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. The probe uses a throwaway ed25519 key pair (created locally, imported viacvm:ImportKeyPair, deleted afterwards) and logs in as the image's built-inohbsimageuser — root login is disabled by the hardening, so no credentials need to be supplied. Windows probes instead set a new, per-probe Administrator password throughRunInstances, authenticate with NTLM WinRM, and run the exact engine/catalog retained in the image; the password is never logged or persisted.[meta].verify_boot = truechains either path automatically after every successful build.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, 2 = API state unknown); schedule it on a timer ahead ofbuild --skip-if-unchanged. Treat exit 2 as a failed check rather than silently skipping a rebuild: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 not retired while it is fresh. The guard expires N days after the image's lineage record: a shared image whose record is older than N days (undated records count as ancient) is presumed unused since then and is retired anyway;0disables the guard entirely. 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 (
[ohbs].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 within the guard window) - Sharing guard (
[image].share_org_unitsis rejected with a warning — the API accepts account IDs only; useshare_accounts) - Rule-set versioning (
ohbs-image list --versions) - Vendor image refresh detection (
ohbs-image check-source) - Config tooling (
config validate/diff/get/explain/migrate/schema) - Layered configuration (
config merge+ repeatable--overlaydeep-merge; tables recurse, lists/scalars replace) - Report evidence index (
report list/report show/report difflineage trail) - Self-contained HTML compliance report (
report html RUN_IDre-render from evidence,scan --html) - Zero-cost demo (
ohbs-image try— offline engine + catalog gates, deterministic sample audit, real HTML report; also Docker--target try) - Build cost tracking (
report cost— instance type / spot / build duration facts in lineage; optional spend estimate, no billing API) - State management (
state path/status/init/prune,sync --check) - Engine tooling (
engine list/engine verify— syntax + SHA-256 drift gate) - Catalog tooling (
catalog list/catalog verify— supply-chain integrity gate) - CLI supply-chain gates in CI (
engine verify+catalog verifyon real bundled data) - Pre-PR gate checklist synced with CI (CONTRIBUTING runs the full nine-gate sequence)
- SLSA L2: reproducible release builds (pinned build environment — fixed runner image, exact Python patch, pinned build backend,
--no-isolation) - 等保 2.0 (MLPS) mapping — CIS rules ↔ 等保 2.0 安全通用要求 control map (海外 CIS / 国内等保 dual view: coverage map, gap analysis, 等保 section in the HTML report)
- STIG benchmark profiles (same engine, DISA content — deferred; lower priority than the 等保 2.0 mapping)
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,
and that the translated quick-start READMEs (zh-CN / ja / th) never reference
a command that no longer exists (.github/workflows/ci.yml →
scripts/check_readme.py). When you add, remove, or rename a ohbs-image
subcommand or a profile, update the relevant sections, then verify locally
before pushing:
python3 scripts/check_readme.py --check-tests --check-translations
# exit 0 = docs current, 1 = missing items / stale translation references
Translations are deliberately abridged quick-starts, so the translation guard
only flags backticked ohbs-image <cmd> references that do not exist in
the CLI — it does not require every command to appear in every language.
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).
To evaluate the deliverable itself — no cloud account, no spend — the same
image also ships a try stage that runs the offline demo and writes the
sample report into a bind-mounted directory:
docker build --target try -t ohbs-image:try .
docker run --rm -v "$(pwd)/out:/demo/out" ohbs-image:try
# → ./out/demo-report.html + demo-audit.json + ohbs-image.toml
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.
Metadata
Release files for ohbs-image 0.22.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| ohbs_image-0.22.0.tar.gz | 2.3 MB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| ohbs_image-0.22.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 4.5 MB
Release files / ohbs_image-0.22.0.tar.gz
| Download URL | ohbs_image-0.22.0.tar.gz |
|---|---|
| Size | 2.3 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
55351e13702942ff0538032cd74905011991671346185c436e852511f259296f
|
|
BLAKE2b-256 checksum How to use checksums |
fa69fe8eeaa08b079dda36daf888b73c581afca8528dcf9b9585022f17cc9b0e
|
| 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 Sep 2, 2026.
Transparency logRelease files / ohbs_image-0.22.0-py3-none-any.whl
| Download URL | ohbs_image-0.22.0-py3-none-any.whl |
|---|---|
| Size | 2.2 MB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
766f64b091c7f8cf6b722684170a1457a6f59dd05fd1b7ecfa6ae32f2fd6b410
|
|
BLAKE2b-256 checksum How to use checksums |
9800bad45608d7c25151b65e0fde3feef31e3f427e25ec20b68da8929cf12c47
|
| 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 Sep 2, 2026.
Transparency log