Compare commits
6
Commits
917c94fd65
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c07f37c78 | ||
|
|
b098132595 | ||
|
|
1dbe372714 | ||
|
|
1063fa4dae | ||
|
|
3f2dd6a5a1 | ||
|
|
a5b84f7566 |
@@ -159,7 +159,6 @@ jobs:
|
|||||||
gpg --batch --yes --delete-secret-keys "${FINGERPRINT}"
|
gpg --batch --yes --delete-secret-keys "${FINGERPRINT}"
|
||||||
gpg --batch --yes --delete-keys "${FINGERPRINT}"
|
gpg --batch --yes --delete-keys "${FINGERPRINT}"
|
||||||
fi
|
fi
|
||||||
rm -f ~/.ssh/id_xbps
|
|
||||||
|
|
||||||
- name: Determine version
|
- name: Determine version
|
||||||
id: version
|
id: version
|
||||||
@@ -204,9 +203,21 @@ jobs:
|
|||||||
esac
|
esac
|
||||||
|
|
||||||
- name: Publish XBPS to distribution repository
|
- name: Publish XBPS to distribution repository
|
||||||
if: github.event.inputs.publish_xbps == 'true'
|
id: publish-xbps
|
||||||
|
if: ${{ github.event.inputs.publish_xbps == true || github.event.inputs.publish_xbps == 'true' }}
|
||||||
|
env:
|
||||||
|
GITEA_PUBLISH_TOKEN: ${{ secrets.GITEAPACKAGETOKEN }}
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
if [ -z "${GITEA_PUBLISH_TOKEN}" ]; then
|
||||||
|
echo "::error::GITEAPACKAGETOKEN repository secret is not configured"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
git config user.name "xavierk"
|
||||||
|
git config user.email "xavierk@bongbetic.com"
|
||||||
|
export GIT_CONFIG_COUNT=1
|
||||||
|
export GIT_CONFIG_KEY_0='http.https://git.bongbetic.com/.extraheader'
|
||||||
|
export GIT_CONFIG_VALUE_0="Authorization: token ${GITEA_PUBLISH_TOKEN}"
|
||||||
VERSION=${{ steps.version.outputs.version }}
|
VERSION=${{ steps.version.outputs.version }}
|
||||||
XBPS_FILE="fenris-${VERSION}_1.x86_64.xbps"
|
XBPS_FILE="fenris-${VERSION}_1.x86_64.xbps"
|
||||||
if [ ! -f "${XBPS_FILE}" ]; then
|
if [ ! -f "${XBPS_FILE}" ]; then
|
||||||
@@ -218,6 +229,7 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
bash scripts/xbps-publish.sh --publish
|
bash scripts/xbps-publish.sh --publish
|
||||||
|
echo "xbps_published=true" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Track format availability
|
- name: Track format availability
|
||||||
id: formats
|
id: formats
|
||||||
@@ -227,7 +239,7 @@ jobs:
|
|||||||
DEB_EXISTS=$([ -f "dist/fenris_${VERSION}_amd64.deb" ] && echo "true" || echo "false")
|
DEB_EXISTS=$([ -f "dist/fenris_${VERSION}_amd64.deb" ] && echo "true" || echo "false")
|
||||||
RPM_EXISTS=$([ -f "dist/fenris-${VERSION}-1.x86_64.rpm" ] && echo "true" || echo "false")
|
RPM_EXISTS=$([ -f "dist/fenris-${VERSION}-1.x86_64.rpm" ] && echo "true" || echo "false")
|
||||||
XBPS_EXISTS=$([ -f "fenris-${VERSION}_1.x86_64.xbps" ] && echo "true" || echo "false")
|
XBPS_EXISTS=$([ -f "fenris-${VERSION}_1.x86_64.xbps" ] && echo "true" || echo "false")
|
||||||
XBPS_PUBLISHED=$([ "${{ github.event.inputs.publish_xbps }}" = "true" ] && echo "true" || echo "false")
|
XBPS_PUBLISHED=$([ "${{ steps.publish-xbps.outputs.xbps_published }}" = "true" ] && echo "true" || echo "false")
|
||||||
echo "deb_available=${DEB_EXISTS}" >> "$GITHUB_OUTPUT"
|
echo "deb_available=${DEB_EXISTS}" >> "$GITHUB_OUTPUT"
|
||||||
echo "rpm_available=${RPM_EXISTS}" >> "$GITHUB_OUTPUT"
|
echo "rpm_available=${RPM_EXISTS}" >> "$GITHUB_OUTPUT"
|
||||||
echo "xbps_available=${XBPS_EXISTS}" >> "$GITHUB_OUTPUT"
|
echo "xbps_available=${XBPS_EXISTS}" >> "$GITHUB_OUTPUT"
|
||||||
@@ -318,7 +330,8 @@ jobs:
|
|||||||
"https://git.bongbetic.com/api/v1/repos/xavierk/Fenris/releases/tags/v${VERSION}")
|
"https://git.bongbetic.com/api/v1/repos/xavierk/Fenris/releases/tags/v${VERSION}")
|
||||||
RELEASE_ID=$(printf '%s' "${RELEASE_JSON}" \
|
RELEASE_ID=$(printf '%s' "${RELEASE_JSON}" \
|
||||||
| python3 -c "import sys,json; print(json.load(sys.stdin)['id'])")
|
| python3 -c "import sys,json; print(json.load(sys.stdin)['id'])")
|
||||||
# Attach deb, rpm, clearsigned checksums, and XBPS artifacts once.
|
PUBLISH_XBPS="${{ steps.formats.outputs.xbps_published }}"
|
||||||
|
# Attach package artifacts; refresh XBPS assets after publication.
|
||||||
ARTIFACTS=(
|
ARTIFACTS=(
|
||||||
"dist/fenris_${VERSION}_amd64.deb"
|
"dist/fenris_${VERSION}_amd64.deb"
|
||||||
"dist/fenris-${VERSION}-1.x86_64.rpm"
|
"dist/fenris-${VERSION}-1.x86_64.rpm"
|
||||||
@@ -336,12 +349,24 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
for FILE in "${ARTIFACTS[@]}"; do
|
for FILE in "${ARTIFACTS[@]}"; do
|
||||||
ASSET_NAME="${FILE##*/}"
|
ASSET_NAME="${FILE##*/}"
|
||||||
if python3 -c 'import json,sys; name=sys.argv[1]; sys.exit(0 if any(a.get("name") == name for a in json.load(sys.stdin).get("assets", [])) else 1)' "${ASSET_NAME}" <<<"${RELEASE_JSON}"; then
|
EXISTING_ASSET_ID=$(python3 -c 'import json,sys; name=sys.argv[1]; print(next((a["id"] for a in json.load(sys.stdin).get("assets", []) if a.get("name") == name), ""))' \
|
||||||
echo "${ASSET_NAME}: already attached"
|
"${ASSET_NAME}" <<<"${RELEASE_JSON}")
|
||||||
else
|
if [ -n "${EXISTING_ASSET_ID}" ]; then
|
||||||
curl --fail --silent --show-error -X POST \
|
if [ "${PUBLISH_XBPS}" = "true" ] && [[ "${ASSET_NAME}" = "${XBPS_FILE}" || "${ASSET_NAME}" = "${XBPS_FILE}.sig2" ]]; then
|
||||||
-H "Authorization: token ${GITEA_PUBLISH_TOKEN}" \
|
curl --fail --silent --show-error -X DELETE \
|
||||||
-F "attachment=@${FILE}" \
|
-H "Authorization: token ${GITEA_PUBLISH_TOKEN}" \
|
||||||
"https://git.bongbetic.com/api/v1/repos/xavierk/Fenris/releases/${RELEASE_ID}/assets"
|
"https://git.bongbetic.com/api/v1/repos/xavierk/Fenris/releases/${RELEASE_ID}/assets/${EXISTING_ASSET_ID}"
|
||||||
|
else
|
||||||
|
echo "${ASSET_NAME}: already attached"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
curl --fail --silent --show-error -X POST \
|
||||||
|
-H "Authorization: token ${GITEA_PUBLISH_TOKEN}" \
|
||||||
|
-F "attachment=@${FILE}" \
|
||||||
|
"https://git.bongbetic.com/api/v1/repos/xavierk/Fenris/releases/${RELEASE_ID}/assets"
|
||||||
done
|
done
|
||||||
|
|
||||||
|
- name: Remove XBPS signing key
|
||||||
|
if: always()
|
||||||
|
run: rm -f ~/.ssh/id_xbps
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ and destruction.
|
|||||||
| UID | `Fenris Packaging <packaging@bongbetic.com>` |
|
| UID | `Fenris Packaging <packaging@bongbetic.com>` |
|
||||||
| Expiry | 2 years from creation |
|
| Expiry | 2 years from creation |
|
||||||
| Hierarchy | Single key — no master/subkey split (single maintainer, manual builds) |
|
| Hierarchy | Single key — no master/subkey split (single maintainer, manual builds) |
|
||||||
| Private key storage | Password manager only |
|
| Private key storage | Gitea repository Actions secret `GPG_PRIVATE_KEY` |
|
||||||
| Public key storage | `packaging/keys/fenris-packaging.asc` in-repo, release notes, docs |
|
| Public key storage | `packaging/keys/fenris-packaging.asc` in-repo, release notes, docs |
|
||||||
| Keyservers | Never — TOFU-over-TLS via raw URL |
|
| Keyservers | Never — TOFU-over-TLS via raw URL |
|
||||||
|
|
||||||
@@ -37,71 +37,72 @@ gpg --armor --export packaging@bongbetic.com > packaging/keys/fenris-packaging.a
|
|||||||
gpg --fingerprint packaging@bongbetic.com
|
gpg --fingerprint packaging@bongbetic.com
|
||||||
```
|
```
|
||||||
|
|
||||||
Save the **private key** to the password manager immediately:
|
Provision the **private key** as the repository Actions secret `GPG_PRIVATE_KEY`.
|
||||||
|
Run the export on the trusted key-generation machine, then enter its output in
|
||||||
|
the Gitea repository's Actions secret settings. Do not save it in the checkout,
|
||||||
|
logs, or a runner directory. The release workflow checks its fingerprint
|
||||||
|
against the committed public key before signing.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
gpg --armor --export-secret-keys packaging@bongbetic.com
|
gpg --armor --export-secret-keys packaging@bongbetic.com
|
||||||
```
|
```
|
||||||
|
|
||||||
Then **delete the private key from the local keyring** — it must never persist
|
After provisioning the secret, delete the private key from the key-generation
|
||||||
on any build host:
|
keyring:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
gpg --delete-secret-keys packaging@bongbetic.com
|
gpg --batch --yes --delete-secret-keys packaging@bongbetic.com
|
||||||
gpg --delete-keys packaging@bongbetic.com
|
gpg --batch --yes --delete-keys packaging@bongbetic.com
|
||||||
```
|
```
|
||||||
|
|
||||||
The committed `fenris-packaging.asc` must contain the real public key (replace
|
The committed `fenris-packaging.asc` must contain the real public key (replace
|
||||||
the placeholder comments).
|
the placeholder comments).
|
||||||
|
|
||||||
|
## XBPS signing key
|
||||||
|
|
||||||
|
XBPS uses a separate RSA 3072 key. Its private half is stored as the Gitea
|
||||||
|
repository Actions secret `XBPS_SIGNING_KEY`. The corresponding public key is
|
||||||
|
published at
|
||||||
|
`https://git.bongbetic.com/xavierk/Fenris-xbps/raw/branch/stable/keys/fenris-xbps-signing.pub`,
|
||||||
|
with fingerprint `SHA256:AvPMRlKMikPg75u0iKr8AUkxlfU/Ad4k/S4o2M9W4/w`.
|
||||||
|
The secret must match that public key.
|
||||||
|
|
||||||
|
The release workflow writes the key to `~/.ssh/id_xbps` to sign the XBPS
|
||||||
|
package. A requested XBPS publication also uses the key to sign repository
|
||||||
|
metadata. A final `always()` cleanup removes the runner copy after publication
|
||||||
|
and release asset upload, including when an earlier step fails.
|
||||||
|
|
||||||
## Per-release signing flow
|
## Per-release signing flow
|
||||||
|
|
||||||
Each release performs: **import → sign → delete**. The private key is never
|
Each tagged release performs: **import → verify → sign → delete** on the
|
||||||
stored on disk longer than the release takes.
|
repository-scoped Gitea Actions runner. The Gitea secret remains configured;
|
||||||
|
the runner's keyring copy is removed after the job.
|
||||||
|
|
||||||
### Step 1: Import the private key
|
### Step 1: Push the release tag
|
||||||
|
|
||||||
Retrieve the private key from the password manager and import it:
|
After updating the version and dated changelog section, push the matching tag:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
gpg --import /tmp/packaging-key-private.asc
|
git push origin v<version>
|
||||||
rm /f /tmp/packaging-key-private.asc # Shred if possible
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Step 2: Build and sign packages
|
### Step 2: Build, verify, and sign packages
|
||||||
|
|
||||||
The Makefile target `make release` handles signing automatically when the
|
The release workflow imports `GPG_PRIVATE_KEY`, checks it against
|
||||||
key is in the keyring:
|
`packaging/keys/fenris-packaging.asc`, builds packages, signs the RPM and
|
||||||
|
clearsigned checksum manifest, validates both, and publishes the release. The
|
||||||
|
workflow imports `XBPS_SIGNING_KEY` separately and signs the XBPS package.
|
||||||
|
XBPS publication is optional and also signs repository metadata; it requires
|
||||||
|
host acceptance and explicit selection during workflow dispatch.
|
||||||
|
|
||||||
```bash
|
### Step 3: Verify runner cleanup
|
||||||
make release # builds, signs RPM, clearsigns SHA256SUMS, prints upload steps
|
|
||||||
```
|
|
||||||
|
|
||||||
Under the hood:
|
The workflow's `always()` cleanup removes the GPG key from the runner's keyring
|
||||||
|
and deletes `~/.ssh/id_xbps`, including after a failed job. Confirm no signing
|
||||||
|
key remains on the runner after the release job.
|
||||||
|
|
||||||
1. `rpmsign --addsign` signs the RPM payload with the packaging key
|
The Gitea Actions secret remains the approved signing source. Do not copy it to
|
||||||
(invoked by `make sign-rpm`).
|
the runner or repository outside the workflow.
|
||||||
2. `sha256sum` generates the checksum manifest.
|
|
||||||
3. `gpg --clearsign` produces `SHA256SUMS.asc` with the packaging key.
|
|
||||||
|
|
||||||
### Step 3: Delete the private key
|
|
||||||
|
|
||||||
Immediately after signing:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
gpg --delete-secret-keys packaging@bongbetic.com
|
|
||||||
gpg --delete-keys packaging@bongbetic.com
|
|
||||||
```
|
|
||||||
|
|
||||||
Verify the key is gone:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
gpg --list-keys packaging@bongbetic.com
|
|
||||||
# Should produce: gpg: keyblock resource ...: No such file or directory
|
|
||||||
```
|
|
||||||
|
|
||||||
The entire import → sign → delete cycle should take minutes. The private key
|
|
||||||
must never be left in any keyring between releases.
|
|
||||||
|
|
||||||
## Key rotation (outline)
|
## Key rotation (outline)
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,8 @@
|
|||||||
- **RPM payload: signed.** rpmsign with the dedicated packaging key, invoked by `make sign-rpm` after the package is built. This is required, not optional: it is the only working dnf-native verification path.
|
- **RPM payload: signed.** rpmsign with the dedicated packaging key, invoked by `make sign-rpm` after the package is built. This is required, not optional: it is the only working dnf-native verification path.
|
||||||
- **deb: unsigned.** apt never verifies payload signatures; trust = instance-signed `InRelease` (signed-by keyring) + TLS + Acquire-By-Hash. Manual-download integrity is covered by SHA256SUMS.
|
- **deb: unsigned.** apt never verifies payload signatures; trust = instance-signed `InRelease` (signed-by keyring) + TLS + Acquire-By-Hash. Manual-download integrity is covered by SHA256SUMS.
|
||||||
- **SHA256SUMS: clearsigned** with the packaging key — the trust anchor for manually downloaded release assets, independent of TLS.
|
- **SHA256SUMS: clearsigned** with the packaging key — the trust anchor for manually downloaded release assets, independent of TLS.
|
||||||
- **Packaging key:** single dedicated key, RSA 3072, UID `Fenris Packaging <packaging@bongbetic.com>`, 2-year expiry, no master/subkey hierarchy (single maintainer, manual builds). Private key lives in the password manager only; each release does import → sign → delete — nothing permanent on any build host. The full ceremony is documented in `docs/install/signing-key-ceremony.md`.
|
- **Packaging key:** single dedicated key, RSA 3072, UID `Fenris Packaging <packaging@bongbetic.com>`, 2-year expiry, no master/subkey hierarchy. The private key is stored as the repository Actions secret `GPG_PRIVATE_KEY`. The release workflow imports it on the self-hosted runner, verifies it against the in-repo public key, signs the RPM and SHA256SUMS, then deletes the runner's keyring copy in an `always()` cleanup step. The full ceremony is documented in `docs/install/signing-key-ceremony.md`.
|
||||||
|
- **XBPS key:** separate RSA 3072 key stored as the repository Actions secret `XBPS_SIGNING_KEY`; its public key and fingerprint are published in `Fenris-xbps`. The release workflow uses it for the XBPS package and, when publication is explicitly requested, the repository index. A final `always()` cleanup deletes its runner copy after publication and release asset upload.
|
||||||
- **Public key publication:** in-repo `packaging/keys/fenris-packaging.asc` (raw URL doubles as the `.repo` gpgkey target), release notes, docs page. No keyservers — TOFU-over-TLS.
|
- **Public key publication:** in-repo `packaging/keys/fenris-packaging.asc` (raw URL doubles as the `.repo` gpgkey target), release notes, docs page. No keyservers — TOFU-over-TLS.
|
||||||
- **Rotation (outline):** new key published alongside old; rpm signed with the new key; `fenris.repo` gpgkey lists both URLs (dnf accepts multiple); old key dropped after one release cycle. Procedure details stay in map fog.
|
- **Rotation (outline):** new key published alongside old; rpm signed with the new key; `fenris.repo` gpgkey lists both URLs (dnf accepts multiple); old key dropped after one release cycle. Procedure details stay in map fog.
|
||||||
|
|
||||||
@@ -53,7 +54,7 @@
|
|||||||
- **A Release is:** a version tag, its packages in the channel, a Gitea release entry with notes, and a clearsigned SHA256SUMS — all together. **Bare tags are forbidden** (tag without packages + release entry is not a Release).
|
- **A Release is:** a version tag, its packages in the channel, a Gitea release entry with notes, and a clearsigned SHA256SUMS — all together. **Bare tags are forbidden** (tag without packages + release entry is not a Release).
|
||||||
- **Cadence: on-demand.** Tag when user-visible changes or fixes accumulate; no calendar, no empty releases, no frequency SLA, no RC ceremony — fixes ship as a revision bump of the current version.
|
- **Cadence: on-demand.** Tag when user-visible changes or fixes accumulate; no calendar, no empty releases, no frequency SLA, no RC ceremony — fixes ship as a revision bump of the current version.
|
||||||
- **Versioning: plain semver.** Major = breaking CLI/config/unit change; store schema changes ride the natural bump (the forward-only refusal handles old-reader/new-store).
|
- **Versioning: plain semver.** Major = breaking CLI/config/unit change; store schema changes ride the natural bump (the forward-only refusal handles old-reader/new-store).
|
||||||
- **Promotion flow:** bump `pyproject.toml` and the matching dated `CHANGELOG.md` section, then push tag `v<version>`. The repository-scoped Gitea Actions workflow builds and validates the deb, rpm, checksums, and release entry. XBPS publication remains a manual dispatch option after host acceptance. `make release` remains the local package/sign/checksum fallback. The ceremony is documented in `docs/install/signing-key-ceremony.md`.
|
- **Promotion flow:** bump `pyproject.toml` and the matching dated `CHANGELOG.md` section, then push tag `v<version>`. The repository-scoped Gitea Actions workflow builds and validates the deb, rpm, checksums, and release entry. XBPS publication remains a manual dispatch option after host acceptance. `make release` is for local artifact preparation and does not replace the tag workflow as the supported publication path. The ceremony is documented in `docs/install/signing-key-ceremony.md`.
|
||||||
- **Rollback:** installing an older package over a newer store is **unsupported** — the store's forward-only version refusal fails it by design. Documented rollback = restore the observation-store snapshot, then install the old Release. No automatic downgrade machinery exists or will be built.
|
- **Rollback:** installing an older package over a newer store is **unsupported** — the store's forward-only version refusal fails it by design. Documented rollback = restore the observation-store snapshot, then install the old Release. No automatic downgrade machinery exists or will be built.
|
||||||
- **CI:** a repository-scoped self-hosted runner is registered and online (checked 2026-09-29). `.gitea/workflows/release.yml` is the tag-triggered release path; maintainers must confirm runner availability and required Gitea secrets before tagging. The workflow publishes deb/rpm packages and release assets; Void publication is withheld unless the signed XBPS host-release step is explicitly requested.
|
- **CI:** a repository-scoped self-hosted runner is registered and online (checked 2026-09-29). `.gitea/workflows/release.yml` is the tag-triggered release path; maintainers must confirm runner availability and required Gitea secrets before tagging. The workflow publishes deb/rpm packages and release assets; Void publication is withheld unless the signed XBPS host-release step is explicitly requested.
|
||||||
|
|
||||||
|
|||||||
@@ -259,8 +259,8 @@ class TestKeyCeremonyDoc:
|
|||||||
|
|
||||||
def test_documents_private_key_storage(self):
|
def test_documents_private_key_storage(self):
|
||||||
content = self._doc_content()
|
content = self._doc_content()
|
||||||
assert "password manager" in content.lower(), \
|
assert "gitea repository actions secret `gpg_private_key`" in content.lower(), \
|
||||||
"Must document that private key lives in password manager"
|
"Must document that Gitea Actions stores the private signing key"
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user