nwarila-platform/secure-rockylinux9-template
GitHub: nwarila-platform/secure-rockylinux9-template
Stars: 0 | Forks: 0
# Secure Rocky Linux 9 Template
[](https://github.com/nwarila-platform/secure-rockylinux9-template/actions/workflows/packer.yaml)
[](https://github.com/nwarila-platform/secure-rockylinux9-template/actions/workflows/security.yaml)
[](https://github.com/pre-commit/pre-commit)
[](https://conventionalcommits.org)
[](LICENSE)
A security-hardened Rocky Linux 9 consumer profile for
[Proxmox VE](https://www.proxmox.com/en/proxmox-virtual-environment/overview). This repository
does not ship standalone `.pkr.hcl` builders. Instead, it owns the consumer inputs under
`packer/` that are copied into
[proxmox-packer-framework](https://github.com/nwarila-platform/proxmox-packer-framework), where the
composed validation and privileged `packer build` steps run. Formatting stays in the repo-local
and PR-gated path instead of the trusted deployment runner. The
tracked Ansible playbook here is the consumer bootstrap entrypoint; reusable Ansible roles remain
upstream in [ansible-framework](https://github.com/nwarila-platform/ansible-framework).
## How This Repo Fits
1. This repository owns the consumer profile inputs:
`packer/systems.auto.pkrvars.hcl`, `packer/ks.pkrtpl.hcl`, and `packer/rocky-linux-9.yml`.
2. CI checks out `proxmox-packer-framework` and `ansible-framework` at exact SHAs.
3. CI copies this repo's consumer files into `proxmox-packer-framework/packer`.
4. Packer validation and builds run in the framework checkout, not in this repo alone.
## Validated With
As of May 4, 2026, the composed CI path is pinned to:
| Component | Exact version / pin | Source of truth |
|---|---|---|
| Packer | `1.15.0` | `proxmox-packer-framework/packer/packer.pkr.hcl` |
| `proxmox-packer-framework` | `9b7701638f7fc091abd19412b84f162ba0dc65ac` (`v0.0.1`) | `.github/workflows/packer.yaml` |
| `ansible-framework` | `e1b52f33d9270b14ba55cdb5810a7a3de0c83b90` | `.github/workflows/packer.yaml` |
| Rocky install media | `Rocky-9.8-x86_64-dvd.iso` | `terraform/terraform.tfvars` |
The pinned `ansible-framework` commit currently requires `Ansible Core >= 2.17, < 2.19` and
`Python >= 3.12` as its baseline. Rocky Linux 9 reusable hardening roles are still upstream work,
so this repo currently owns the bootstrap playbook entrypoint only.
## Features
| Category | Current state |
|---|---|
| **OS hardening** | CIS-oriented partitioning and mount options in this repo, SELinux enforcing, firewalld enabled, locked root account |
| **Install-time compliance** | DISA STIG profile applied through Kickstart `%addon com_redhat_oscap` |
| **Provisioning** | Consumer-owned Kickstart template and tracked Ansible bootstrap playbook entrypoint; reusable roles remain in `ansible-framework` |
| **Infrastructure** | Proxmox VE template profile via `proxmox-packer-framework`, UEFI/OVMF, Secure Boot, TPM 2.0, template-side Cloud-Init disk |
| **Formatting** | `packer fmt`, `yamllint`, `markdownlint-cli`, `.editorconfig`, VS Code settings |
| **Security** | Trivy, Gitleaks, CodeQL, release-pinned `secure-packer-bootstrapper` runtime credentials, `detect-private-key`, SHA-pinned GitHub Actions |
| **Dependency management** | Dependabot for GitHub Actions and `pre-commit`, plus PR automation for the tracked `secure-packer-bootstrapper` release pin |
## Prerequisites
### Composed Build Contract
| Tool / dependency | Version | Notes |
|---|---|---|
| [Packer](https://developer.hashicorp.com/packer) | `1.15.0` | Exact requirement from the pinned `proxmox-packer-framework` contract |
| [Ansible Core](https://pypi.org/project/ansible-core/) | `>= 2.17, < 2.19` (e.g. `2.18.x`) | Exact constraint from the pinned `ansible-framework` `requirements-dev.txt`; `2.20.x` is out of range |
| Python | `3.12.x` | Required by the current `ansible-framework` / `ansible-core` baseline |
| Rocky ISO | `Rocky-9.8-x86_64-dvd.iso` | SHA256 is pinned in `terraform/terraform.tfvars` after GPG-verifying Rocky's signed `CHECKSUM` |
### Local Contributor Tooling
| Tool / pin | Version | Notes |
|---|---|---|
| [pre-commit](https://pre-commit.com/) | `4.5.1` minimum | Enforced by `.pre-commit-config.yaml` |
| `pre-commit-hooks` | `v6.0.0` | Hook pin in `.pre-commit-config.yaml` |
| [Gitleaks](https://github.com/gitleaks/gitleaks) | `v8.30.1` | Hook pin in `.pre-commit-config.yaml` |
| [yamllint](https://pypi.org/project/yamllint/) | `1.38.0` | Hook pin; VS Code task also expects the CLI locally |
| [markdownlint-cli](https://github.com/igorshubovych/markdownlint-cli) | `v0.48.0` | Hook pin; VS Code task also expects the CLI locally |
| [conventional-pre-commit](https://github.com/compilerla/conventional-pre-commit) | `v4.4.0` | Commit-msg hook pin |
## Getting Started
### 1. Clone This Repo and the Required Sibling Frameworks
git clone https://github.com/nwarila-platform/secure-rockylinux9-template.git
git clone https://github.com/nwarila-platform/proxmox-packer-framework.git
git clone https://github.com/nwarila-platform/ansible-framework.git
The default VS Code tasks and the composed local validation path expect these sibling directories:
../secure-rockylinux9-template
../proxmox-packer-framework
../ansible-framework
### 2. Install Pre-Commit Hooks
pip install pre-commit
pre-commit install --hook-type pre-commit --hook-type pre-push --hook-type commit-msg
### 3. Configure GitHub Settings and Runtime Bootstrap Inputs
The settings below are split between static repo-owned inputs and runtime-generated bootstrap
material:
- GitHub repo secrets / variables use the human-facing names in the first column.
- The reviewed `secure-packer-bootstrapper` release pin lives in
`.github/pins/secure-packer-bootstrapper.env` and is refreshed in pull requests by workflow.
- The reviewed Rocky ISO pin lives in `terraform/terraform.tfvars`; refresh it with
`terraform/scripts/fetch_rocky_iso_sha256.sh` so the committed SHA256 is extracted only after
verifying Rocky's signed `CHECKSUM`.
- Terraform state for the ISO manager root uses HCP Terraform remote state and locking in the
`secure-rockylinux9-template-iso` workspace.
- Local manual Packer runs can either export the exact `PKR_VAR_*` names directly or `eval` the
`secure-packer-bootstrapper` release bundle in the same shell as `packer validate` /
`packer build`.
Until the first published `secure-packer-bootstrapper` release exists and PR automation refreshes
the committed pin file, the privileged build intentionally fails closed instead of guessing at a
bundle URL or checksum.
| GitHub setting | Local equivalent | Purpose / precedence |
|---|---|---|
| `PROXMOX_HOSTNAME` | `PKR_VAR_proxmox_hostname` | Proxmox API endpoint |
| `PROXMOX_PACKER_FRAMEWORK_TOKEN_ID` | `PKR_VAR_proxmox_api_token_id` | Proxmox API token ID |
| `PROXMOX_PACKER_FRAMEWORK_SECRET` | `PKR_VAR_proxmox_api_token_secret` | Proxmox API token secret |
| `PROXMOX_SKIP_TLS_VERIFY` | `PKR_VAR_proxmox_skip_tls_verify` | Top-level override. When set, it wins over `packer_image.insecure_skip_tls_verify`. This repo keeps `true` in the committed profile as a documented lab exception. |
| `PROXMOX_NODE` | `PKR_VAR_proxmox_node` | Top-level override. When set, it wins over `packer_image.node`. |
| `DEPLOY_USER_NAME` | `PKR_VAR_deploy_user_name` | Guest deploy account name |
| `PROXMOX_VE_ENDPOINT` | provider env | Proxmox API endpoint consumed by the Terraform ISO manager |
| `PROXMOX_VE_API_TOKEN` | provider env | Proxmox API token consumed by the Terraform ISO manager |
| `TF_API_TOKEN` | `TF_TOKEN_app_terraform_io` | HCP Terraform token for remote state and locking |
| `.github/pins/secure-packer-bootstrapper.env` | tracked release pin file | Reviewed release repo, tag, asset URLs, and SHA256 consumed by CI |
| `terraform/terraform.tfvars` | tracked ISO pin file | Reviewed Rocky ISO URL, SHA256, and filename consumed by Terraform |
The runtime bootstrap step generates these values immediately before `packer validate` and
`packer build`:
- `PKR_VAR_deploy_user_password`
- `PKR_VAR_deploy_user_password_hash`
- `PKR_VAR_deploy_user_key`
- `SPB_DEPLOY_USER_PASSWORD`
- `SPB_SSH_PRIVATE_KEY_FILE`
- `SPB_SSH_KEY_PASSPHRASE`
CI uses the generated SSH key for first-hop login, the generated password hash for Kickstart
`user --iscrypted`, and retains the plaintext password only for sudo / Ansible `become`.
### 4. Customize the Consumer Inputs
- `packer/systems.auto.pkrvars.hcl` carries real owner defaults for VM ID, node, storage, IP, and
VLAN allocation. These are reserved owner defaults, not portable examples.
- `packer/ks.pkrtpl.hcl` owns the install-time guest baseline: partitioning, mount options,
OpenSCAP STIG invocation, user creation, and post-install hardening commands.
- `packer/rocky-linux-9.yml` owns the consumer bootstrap playbook entrypoint. Reusable roles stay
upstream in `ansible-framework`.
- `terraform/terraform.tfvars` pins the exact Rocky ISO URL, filename, and SHA256 consumed by
`terraform-proxmox-iso-manager-framework`.
To bump the Rocky ISO pin, update `ROCKY_ISO_FILENAME` if the exact filename changed, run
`terraform/scripts/fetch_rocky_iso_sha256.sh`, copy the printed SHA256 into `terraform.tfvars`,
update `iso_pin.url` and `iso_pin.filename`, then run Terraform validation before opening a PR.
## Project Structure
.
|-- .github/
| |-- pins/ # Reviewed secure-packer-bootstrapper release pin
| |-- scripts/ # CI helper scripts
| `-- workflows/ # GitHub Actions workflows
|-- .config/ # Markdown/YAML lint configuration
|-- .vscode/ # Editor tasks/settings for the multi-repo workflow
|-- packer/
| |-- ks.pkrtpl.hcl # Consumer-owned Kickstart template
| |-- rocky-linux-9.yml # Consumer-owned bootstrap playbook entrypoint
| `-- systems.auto.pkrvars.hcl # Consumer-owned framework input profile
|-- terraform/
| |-- scripts/ # ISO checksum provenance helpers
| `-- terraform.tfvars # Reviewed Rocky ISO pin consumed by Terraform
|-- tests/ # Workflow-policy and pin-refresh unit tests
|-- .editorconfig
|-- .gitattributes
|-- .gitignore
|-- .pre-commit-config.yaml
|-- CHANGELOG.md
|-- CONTRIBUTING.md
|-- README.md
|-- SECURITY.md
`-- SUPPORT.md
## Validation Strategy
### Repo-local checks
- `pre-commit run --all-files`
- `terraform/scripts/fetch_rocky_iso_sha256.sh`
- `yamllint` / `markdownlint-cli` / `packer fmt` against committed consumer files
### Composed framework validation
The real contract test for this repo happens after the consumer files are copied into
`../proxmox-packer-framework/packer` (local tasks) or
`${{ github.workspace }}/proxmox-packer-framework/packer` (CI). That composed path performs:
1. Ansible syntax check against the pinned `../ansible-framework/ansible.cfg`
2. `packer init`
3. `packer validate`
### Branch-promotion gates
The PR workflow path owns the non-privileged gates:
1. Verify the tracked `secure-packer-bootstrapper` release pin is current
2. Run repo-native automation unit tests for the workflow contract and pin-refresh helper
3. Run `pre-commit run --all-files`
4. Verify the pinned Packer download against the tracked SHA256 before installing it
5. Check out the pinned framework repositories
6. Run Ansible syntax validation plus composed `packer init` / `packer validate`
### Full integration build
Full build verification requires the self-hosted runner, Proxmox access, and the privileged CI
path in `.github/workflows/packer.yaml`.
### Native `packer test`
This repo does not currently ship local `.pkr.hcl` build definitions, so repo-root native
`packer test` is not the canonical test path here.
## VS Code Tasks
Press `Ctrl+Shift+B` to run the default
**Full Validation (Requires sibling frameworks)** task.
Framework-aware tasks are labeled explicitly and expect adjacent `proxmox-packer-framework` and
`ansible-framework` checkouts. Repo-local linting tasks remain available on their own.
## CI/CD Pipeline
| Workflow | Trigger | Purpose |
|---|---|---|
| **PR Verify** | Pull requests to `main` | Run branch-promotion gates: pin freshness, automation unit tests, repo-local checks, verified tool bootstrap, Ansible syntax validation, and composed `packer validate` |
| **Refresh secure-packer-bootstrapper Pin** | PR sync, weekly schedule, manual | Refresh the tracked release URLs and SHA256 in `.github/pins/secure-packer-bootstrapper.env` |
| **Packer Build** | Push to `main`, manual on `main` only | Run the trusted deployment path only: load the reviewed release pin, mint runtime credentials, perform final `packer validate`, and build |
| **Security Scan** | Push, PR, weekly schedule | Trivy filesystem/secret scan plus Gitleaks history scan |
| **CodeQL Analysis** | Push, PR, weekly schedule | Static analysis of GitHub Actions workflows |
| **Release Please** | Push to `main` | Semantic versioning and changelog generation for repo source/config only |
## GitHub Repository Controls
These protections live in GitHub settings rather than this checkout, but they are part of the
repository contract and should remain aligned with the workflow set:
- active rulesets on the default branch for signed commits, linear history, protected PR merges,
and required status checks: `Branch Promotion Gates`, `Trivy (Filesystem & Secrets)`,
`Gitleaks (Secret Scan)`, and `CodeQL (actions)`
- a real `packer-build` environment with deployment branch restrictions and reviewer protection
- repository-level SHA pinning required for GitHub Actions
- default `GITHUB_TOKEN` permissions set to read-only, with workflow review approval disabled
## VM Template Specifications
| Component | Configuration |
|---|---|
| **OS** | Rocky Linux 9.7 (x86_64) |
| **Boot** | UEFI (OVMF), Secure Boot, TPM 2.0 |
| **CPU** | 2 cores, host passthrough |
| **Memory** | 4096 MB |
| **Disk** | 100 GB LVM layout defined in `packer/ks.pkrtpl.hcl` |
| **Network** | virtio adapter, static owner-reserved IP/VLAN in the committed profile |
| **Provisioning** | Consumer-owned Kickstart template, OpenSCAP STIG install profile, bootstrap playbook entrypoint |
| **Cloud-Init** | Template-side Cloud-Init disk enabled; guest image installs the `cloud-init` package, but datasource behavior is not separately CI-validated |
### Disk Partitioning
| Partition | Size | Mount | Options |
|---|---|---|---|
| EFI | 1024 MB | `/boot/efi` | `nodev,nosuid` |
| Boot | 1024 MB | `/boot` | `nodev,nosuid` |
| Root | 10240 MB | `/` | - |
| Home | 4096 MB | `/home` | `nodev,nosuid,noexec` |
| Opt | 2048 MB | `/opt` | `nodev` |
| Tmp | 4096 MB | `/tmp` | `nodev,noexec,nosuid` |
| Var | 2048 MB | `/var` | `nodev,nosuid` |
| Var-Tmp | 1000 MB | `/var/tmp` | `nodev,noexec,nosuid` |
| Var-Log | 4096 MB | `/var/log` | `nodev,noexec,nosuid` |
| Var-Audit | 500 MB | `/var/log/audit` | `nodev,noexec,nosuid` |
## Security Controls Snapshot
### Direct controls implemented in this repo
| Control | Evidence | Notes |
|---|---|---|
| Locked root account | `packer/ks.pkrtpl.hcl` | `rootpw --lock` |
| SELinux enforcing | `packer/ks.pkrtpl.hcl` | Install-time baseline |
| firewalld enabled | `packer/ks.pkrtpl.hcl` | SSH explicitly allowed |
| Fixed LVM layout and mount options | `packer/ks.pkrtpl.hcl` | Partition table above reflects current Kickstart |
| Deploy user creation and SSH access | `packer/ks.pkrtpl.hcl` | Uses `user --iscrypted`, installs the runtime-generated public key, and disables SSH password authentication |
| SSH/SFTP normalization | `packer/ks.pkrtpl.hcl` and `packer/rocky-linux-9.yml` | Keeps the communicator working during bootstrap |
| Bootstrap playbook readiness checks | `packer/rocky-linux-9.yml` | Pre-task probes, connection reset, fact gathering |
| `cloud-init` package installation | `packer/ks.pkrtpl.hcl` | Guest package installed from Kickstart |
### Controls delegated at install time
| Control | Owner | Notes |
|---|---|---|
| DISA STIG profile application | OpenSCAP / SCAP Security Guide | Applied through `%addon com_redhat_oscap` |
| Additional STIG-managed guest settings | OpenSCAP / SCAP Security Guide | Version-sensitive and not fully enumerated here |
### Controls inherited from framework / platform wiring
| Control | Owner | Status |
|---|---|---|
| Template-side Cloud-Init disk attachment | `proxmox-packer-framework` + Proxmox VE | Enabled in the consumer profile |
| Ansible provisioner wiring and `roles_path` handoff | `proxmox-packer-framework` + `ansible-framework` | Active |
### Controls not yet evidenced here
| Control | Expected owner | Current state |
|---|---|---|
| Reusable Rocky Linux 9 hardening roles | `ansible-framework` | Not yet evidenced at the pinned upstream commit |
| Repo-owned runtime compliance validation | downstream operator or future CI | Not implemented in this repo |
## Release Semantics
Git tags and GitHub Releases in this repository version the consumer profile source and
configuration only. They do not, by themselves, attest a built Proxmox VM template artifact.
Auto-generated source archives are filtered by `.gitattributes`; they are not published VM
artifacts or a complete operator bundle.
## Security
See [SECURITY.md](SECURITY.md) for vulnerability disclosure, runner trust-boundary notes, and
secret-detection coverage limits.
## License
This project is licensed under the [MIT License](LICENSE).