Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,17 @@

* text=auto eol=lf
*.png binary

# Left out of the release archive (scripts/release/build.sh): tests, CI and tool settings. The Ansible collection is a
# release file of its own.
/.github export-ignore
/tests export-ignore
/scripts/dev export-ignore
/scripts/release export-ignore
/ansible export-ignore
/.editorconfig export-ignore
/.gitattributes export-ignore
/.gitignore export-ignore
/.gitleaksignore export-ignore
/.shellcheckrc export-ignore
/.sonarcloud.properties export-ignore
12 changes: 7 additions & 5 deletions .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,15 +99,17 @@ and checks it against this list before approving:
4. **Security.** No weaker default than before; no path a local user controls is written, chowned or followed as
root; settings are validated in `mod_check` before anything changes; nothing is downloaded without a check.
5. **Lockout.** Changes to SSH, the firewall, PAM or sudo keep a way in and are covered by the lockout checks.
6. **Compatibility.** A changed setting name or default is labelled `breaking-change` with migration notes, and
`lss update` migrates the state of existing installations.
6. **Compatibility.** A changed setting name or default is labelled `breaking-change` with migration notes, and a
module of the new version takes over the state an older version left (installed after `lss update`, applied with
`lss configure`).
7. **Documentation.** README, module guide, hardening guide and `CHANGELOG.md` match the change.

## Releases

From 2.0.0 on, releases are built by the release workflow on `main`: it tags the version, attaches the archive with a
signed build attestation and publishes the GitHub release. The version step comes from the pull request labels:
`breaking-change` makes a major release, `enhancement` a minor one, anything else a patch.
From 2.0.0 on, releases are built by the release workflow on `main`: it tags the version, attaches the archive,
`bootstrap.sh`, the Ansible collection, the SBOM and `SHA256SUMS` with a signed build attestation and publishes the
GitHub release. The version step comes from the pull request labels: `breaking-change` makes a major release,
`enhancement` a minor one, anything else a patch. Details: [releasing](../docs/releasing.md).

## License

Expand Down
14 changes: 8 additions & 6 deletions .github/SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,19 +57,21 @@ documented as an [OpenVEX](https://openvex.dev/) statement in `.vex/`, with the

From 2.0.0 on, releases are built only by the release workflow of this repository, never from a laptop. Each release
carries a signed build attestation (Sigstore, SLSA provenance) that names this repository, the workflow and the
commit. Verify a downloaded archive with the GitHub CLI:
commit. Verify a downloaded file with the GitHub CLI, naming the release workflow on `main` as the signer:

```bash
gh attestation verify lss-<version>.tar.gz --repo VectoDE/Linux-Server-Setup-Script
gh attestation verify lss-<version>.tar.gz --repo VectoDE/Linux-Server-Setup-Script \
--cert-identity https://github.com/VectoDE/Linux-Server-Setup-Script/.github/workflows/release.yml@refs/heads/main
```

`lss update` checks the attestation of every version before it installs it and refuses an archive that does not
verify.
`bootstrap.sh` and `lss update` check the checksum and the attestation of every version before they install it and
refuse a release that does not verify ([installing and upgrading](../docs/upgrading.md#what-gets-checked)).

## Secrets and credentials

- The project keeps no stored secrets: workflows use the short-lived `GITHUB_TOKEN`, attestations use the workflow's
OIDC identity.
- Workflows use the short-lived `GITHUB_TOKEN`, attestations the workflow's OIDC identity. The only stored secrets
are the deploy key and the SSH signing key of the release commit and tag, in the environment `release`, which only
`main` can use and only the job that commits and tags reads ([releasing](../docs/releasing.md)).
- On a server, lss keeps generated secrets (passwords, 2FA enrolment) in `/etc/lss/secrets`, mode 0600 in a 0700 directory, and
shows them only with `lss secrets show NAME`. They never go into logs.
- Secrets are never committed. gitleaks in CI and GitHub secret scanning with push protection block leaks.
Expand Down
13 changes: 13 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,16 @@ updates:
groups:
images:
patterns: ['*']

# Python tools of the Ansible collection checks (tests/ansible/requirements.txt, pinned with hashes).
- package-ecosystem: pip
directory: /tests/ansible
schedule:
interval: weekly
day: monday
cooldown:
default-days: 7
labels: [dependencies, ci]
groups:
ansible:
patterns: ['*']
175 changes: 175 additions & 0 deletions .github/workflows/distribution.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
# SPDX-FileCopyrightText: 2025-2026 VectoDE
#
# SPDX-License-Identifier: MIT

# How lss reaches servers, tested with real signatures: two test releases are built with scripts/release/build.sh,
# exactly as the release workflow builds a release, and get a real build attestation (Sigstore) from this workflow.
# bootstrap.sh and `lss update` then install, update, roll back and downgrade them on every supported system and
# refuse tampered copies (tests/integration/update.sh); the Ansible collection is linted and installs and updates lss
# over SSH (tests/integration/ansible.sh). Only the signer differs from a release: bootstrap.sh accepts these test
# builds because the tests name this workflow and ref in LSS_RELEASE_WORKFLOW and LSS_RELEASE_REF.
name: Release and update

on:
pull_request:
push:
branches: [main]
schedule:
- cron: "47 3 * * *"
workflow_dispatch:

permissions:
contents: read

concurrency:
group: distribution-${{ github.ref }}
cancel-in-progress: true

env:
VERSION_A: 2.0.0-ci.1
VERSION_B: 2.0.0-ci.2

jobs:
build:
name: Build and sign test releases
# Pull requests from forks and Dependabot get no OIDC token to sign with; the release workflow is unaffected.
if: github.event_name != 'pull_request' || (github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]')
runs-on: ubuntu-24.04
timeout-minutes: 15
permissions:
contents: read
id-token: write # Sigstore signature for the test attestations
attestations: write # stores them with the repository, as a release does
steps:
- uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
with:
egress-policy: audit
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Install ansible-core (hash-pinned)
run: |
python3 -m venv "$RUNNER_TEMP/venv"
"$RUNNER_TEMP/venv/bin/pip" install --quiet --require-hashes -r tests/ansible/requirements.txt
echo "$RUNNER_TEMP/venv/bin" >> "$GITHUB_PATH"
- name: Build two releases
run: |
for version in "$VERSION_A" "$VERSION_B"; do
scripts/release/build.sh --version "$version" "$RUNNER_TEMP/release/download/v$version"
done
- uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
id: attest-a
with:
subject-path: ${{ runner.temp }}/release/download/v${{ env.VERSION_A }}/*
- uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
id: attest-b
with:
subject-path: ${{ runner.temp }}/release/download/v${{ env.VERSION_B }}/*
- name: Lay the releases out like GitHub releases
env:
BUNDLE_A: ${{ steps.attest-a.outputs.bundle-path }}
BUNDLE_B: ${{ steps.attest-b.outputs.bundle-path }}
run: |
cp "$BUNDLE_A" "$RUNNER_TEMP/release/download/v$VERSION_A/lss-$VERSION_A.intoto.jsonl"
cp "$BUNDLE_B" "$RUNNER_TEMP/release/download/v$VERSION_B/lss-$VERSION_B.intoto.jsonl"
mkdir -p "$RUNNER_TEMP/release/latest/download"
cp "$RUNNER_TEMP/release/download/v$VERSION_B/SHA256SUMS" "$RUNNER_TEMP/release/latest/download/SHA256SUMS"
find "$RUNNER_TEMP/release" -type f | sort
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: test-release
path: ${{ runner.temp }}/release/
retention-days: 3

lint:
name: Ansible collection lint
runs-on: ubuntu-24.04
timeout-minutes: 15
steps:
- uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
with:
egress-policy: audit
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Install ansible-core and ansible-lint (hash-pinned)
run: |
python3 -m venv "$RUNNER_TEMP/venv"
"$RUNNER_TEMP/venv/bin/pip" install --quiet --require-hashes -r tests/ansible/requirements.txt
echo "$RUNNER_TEMP/venv/bin" >> "$GITHUB_PATH"
- name: Build and install the collection, check the module documentation
run: |
scripts/release/build.sh "$RUNNER_TEMP/dist"
version="$(cat VERSION)"
ansible-galaxy collection install "$RUNNER_TEMP/dist/vectode-lss-$version.tar.gz" -p "$RUNNER_TEMP/collections"
echo "ANSIBLE_COLLECTIONS_PATH=$RUNNER_TEMP/collections" >> "$GITHUB_ENV"
ANSIBLE_COLLECTIONS_PATH="$RUNNER_TEMP/collections" ansible-doc vectode.lss.lss > /dev/null
ANSIBLE_COLLECTIONS_PATH="$RUNNER_TEMP/collections" ansible-doc -t role vectode.lss.lss > /dev/null
jq -e '.bomFormat == "CycloneDX" and (.components | length > 0)' "$RUNNER_TEMP/dist/lss-$version.cdx.json" > /dev/null
- name: ansible-lint
working-directory: ansible
run: ansible-lint --offline

update:
name: ${{ matrix.system }}${{ matrix.arch == 'arm64' && ' arm64' || '' }} / bootstrap and update
needs: build
runs-on: ${{ matrix.arch == 'arm64' && 'ubuntu-24.04-arm' || 'ubuntu-24.04' }}
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
system: [debian-12, debian-13, ubuntu-22.04, ubuntu-24.04, ubuntu-26.04, rocky-9, almalinux-10]
arch: [x64, arm64]
steps:
- uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
with:
egress-policy: audit
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: test-release
path: ${{ runner.temp }}/release
- name: Install, update, roll back, downgrade, refuse
env:
SYSTEM: ${{ matrix.system }}
LSS_RELEASE_WORKFLOW: distribution.yml
LSS_RELEASE_REF: ${{ github.ref }}
run: tests/integration/update.sh "$SYSTEM" "$RUNNER_TEMP/release" "$VERSION_A" "$VERSION_B"

ansible:
name: ${{ matrix.system }}${{ matrix.arch == 'arm64' && ' arm64' || '' }} / Ansible collection
needs: build
runs-on: ${{ matrix.arch == 'arm64' && 'ubuntu-24.04-arm' || 'ubuntu-24.04' }}
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
system: [debian-12, debian-13, ubuntu-22.04, ubuntu-24.04, ubuntu-26.04, rocky-9, almalinux-10]
arch: [x64, arm64]
steps:
- uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
with:
egress-policy: audit
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: test-release
path: ${{ runner.temp }}/release
- name: Install ansible-core (hash-pinned) and the collection
run: |
python3 -m venv "$RUNNER_TEMP/venv"
"$RUNNER_TEMP/venv/bin/pip" install --quiet --require-hashes -r tests/ansible/requirements.txt
echo "$RUNNER_TEMP/venv/bin" >> "$GITHUB_PATH"
"$RUNNER_TEMP/venv/bin/ansible-galaxy" collection install \
"$RUNNER_TEMP/release/download/v$VERSION_A/vectode-lss-$VERSION_A.tar.gz" -p "$RUNNER_TEMP/collections"
- name: Install lss and modules with the role, update, remove
env:
SYSTEM: ${{ matrix.system }}
LSS_RELEASE_WORKFLOW: distribution.yml
LSS_RELEASE_REF: ${{ github.ref }}
ANSIBLE_COLLECTIONS_PATH: ${{ runner.temp }}/collections
run: tests/integration/ansible.sh "$SYSTEM" "$RUNNER_TEMP/release" "$VERSION_A" "$VERSION_B"
Loading