owfeed

module
v0.1.4 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 28, 2026 License: Apache-2.0

README

owfeed

Русская версия

One binary. Turns a directory into a signed apk feed for OpenWrt 25.12+, so your users run three lines and apk add works.

A noarch package across all 35 architectures — built, signed, indexed, laid out — takes ~25 seconds. The status quo is 35 SDK builds.


How it works

A feed is a directory of files served over HTTPS. No server software, no database. You produce the directory; anything that can serve static files will do.

feed/
  podkop.pem                            your PUBLIC key
  releases/25.12/x86_64/
    packages.adb                        the signed index (binary)
    index.json                          the same thing as JSON
    sha256sums
    podkop-0.28-r1.apk                  the packages, flat, beside the index
    luci-app-podkop-0.28-r1.apk
  releases/25.12/aarch64_cortex-a53/    the same files again
  …                                     35 directories, one per architecture

On the router, four things happen:

  1. The user drops your .pem into /etc/apk/keys/. The router now trusts your signature.
  2. They write one line into /etc/apk/repositories.d/<feed>.list — the direct URL of packages.adb.
  3. apk update fetches that index and verifies it against your key.
  4. apk add podkop finds podkop 0.28-r1 in the index, derives the filename from the name and version, fetches podkop-0.28-r1.apk from the same directory, checks it against the hash the index recorded, and installs it.

Where does it install to? A package's payload is a piece of the root filesystem. What you put in staging/podkop/etc/config/podkop arrives at /etc/config/podkop.

Why 35 copies of the same file? In apk's ndx mode a router reads exactly one index — the one for its own architecture. A noarch package has to be in all of them. The copies are byte-identical.

The stages map onto that one to one:

build your staged directory → .apk files
sign a signature into each .apk
index fan out into the 35 directories, write a signed packages.adb in each
publish check it all, hand the directory to whatever uploads it

Two release lines, two package managers. 25.12 and later is apk; 24.10 and earlier is opkg, and they agree on almost nothing — a different index, a different signature scheme, a different word for "architecture-independent". owfeed publishes either or both from one config, and holds both to the same checks.

25.12+ (apk) 24.10 and earlier (opkg)
index binary packages.adb text Packages + Packages.gz
signed over the index itself the uncompressed Packages
signature EC prime256v1 usign / ed25519
repository line URL of the index file URL of the directory
key installed as /etc/apk/keys/<any name>.pem /etc/opkg/keys/<key id>
per-package signature yes none — trust rests on the index
"any architecture" noarch all

A feed serving both signs with two keys. That is not a choice: each manager verifies only its own scheme.

I want to publish a feed

owfeed init --url https://feed.example.org
owfeed keygen -o ~/keys/myfeed.pem      # outside the repo. A published key cannot be revoked.
export OWFEED_SIGN_KEY="$(cat ~/keys/myfeed.pem)"

# edit owfeed.yml: point `files:` at a directory laid out the way it should install

owfeed lock --update                    # derives the architecture list; commit it
owfeed build && owfeed sign && owfeed index
owfeed doctor

out/ is now the feed. Upload it.

I want to ship a new version

Bump version: in owfeed.yml, then:

owfeed build && owfeed sign && owfeed index && owfeed doctor

I want to add a package

Add another entry under packages:. Same four commands.

I want a package on one line only

releases:
  - line: "25.12"
    default: true
    format: apk
  - line: "24.10"
    format: ipk

packages:
  - name: luci-app-mine        # no `releases:` — goes to both
    ...
  - name: luci-app-mine-new
    releases: ["25.12"]        # 25.12 only
    ...

One owfeed build produces every line, in each line's format. One owfeed index publishes them all.

I want to tell users how to install it

owfeed install-snippet             # markdown, paste into your README
owfeed install-snippet --format sh # just the commands

Do not write your own. doctor compares your README against this output, because a feed whose documented URL 404s is a live bug in a major feed right now.

I want this in CI

The whole thing, correctly shaped, is one job:

jobs:
  publish:
    permissions:                 # a called workflow can only narrow these, never widen them,
      contents: read             # so the caller has to grant what the publish job needs
      pages: write
      id-token: write
      actions: read
    uses: VizzleTF/owfeed/.github/workflows/feed.yml@v0.1.4
    with:
      owfeed-version: v0.1.4
      smoke-releases: "25.12 24.10"
    secrets:
      sign-key: ${{ secrets.OWFEED_SIGN_KEY }}
      usign-key: ${{ secrets.OWFEED_USIGN_KEY }}   # only if you serve 24.10

That splits build from publish so the signing key is never in the job that runs your build scripts, gates the upload on owfeed publish, and installs the packages on a real OpenWrt image before any of it goes out. pre-build: and post-index: take shell if your feed fetches or checks anything of its own.

The signing secrets have to be repository or organization secrets, not environment ones: a calling job has no environment, so it cannot read an environment secret in order to pass it on. The environment: on the publish job is still what gates the run behind a reviewer.

If you want the steps yourself, take the tool and leave the shape:

- uses: VizzleTF/owfeed/setup@v0.1.4
  with: { version: v0.1.4 }
- run: owfeed --frozen-lock build && owfeed sign && owfeed index
  env:
    OWFEED_SIGN_KEY: ${{ secrets.OWFEED_SIGN_KEY }}
- run: owfeed publish            # refuses to publish a broken tree. No override flag.
- uses: actions/upload-pages-artifact@v3
  with: { path: out }
- uses: actions/deploy-pages@v4

Put publish in a separate job with environment: so a fork PR cannot reach the key.

setup downloads one binary and checks it against GitHub's build attestation before running it — not against a checksum file from the same release, which whoever replaced the binary could replace too.

Upstream added an architecture

owfeed lock --update    # prints the diff; commit it

--frozen-lock fails the build until you do. What your feed covers should not change without you seeing it.

I want to be sure it installs

owfeed smoke           # installs the built feed on a real OpenWrt image

doctor proves the tree is coherent. This proves a router accepts it — following your own published snippet, and failing if apk asks for --allow-untrusted. They are different claims.

I want to check what is already live

owfeed verify out      # fetches the published feed over its documented URL

Catches a redirect apk will not follow, a package the live index names that is missing or the wrong size, and — given the tree about to replace it — a version being republished with different contents.

Something broke

owfeed doctor          # numbered findings, each says why it matters and what to do

Things that will bite you

owfeed refuses each of these. Every one has burned a real maintainer.

arch: all apk rejects it as uninstallable. Use noarch.
PKCS#8 key openssl genpkey -algorithm EC writes the wrong PEM. Only SEC1 works.
1.0~beta after ~ apk reads a commit hash, so only hex. Use _beta1.
A feed URL that redirects apk does not follow 30x (openwrt#17180).
No sysupgrade.conf line /etc/apk/keys/*.pem do not survive sysupgrade. Top cause of post-upgrade UNTRUSTED signature.
Indexing before signing signing appends bytes, so the index no longer matches the file.
/etc/config/foo not in conffiles: sysupgrade replaces the user's settings with your defaults, silently, every upgrade.
.po files in the payload LuCI reads compiled .lmo. Point i18n.from: at them and owfeed compiles them.
A README that drifted already live in a major feed today.
A package the build dropped absence is invisible to every check that reads the tree, so owfeed checks the tree against your config.

Things owfeed will not pretend

  • apk has no revocation. No CRL, no expiry, no kill signal. owfeed makes rotation cheap; it does not claim revocation exists.
  • Your key is a trust anchor for every package name, not just yours. If you ship one package people install occasionally, signed release artifacts are a smaller ask.
  • Attended Sysupgrade will not carry your packages across. owut forwards no custom repositories and the ASU server's repository_allow_list is empty by default, which denies everything.
  • build packages a directory; it does not build one. Your CSS must already be built. owfeed does compile your .po catalogues — LuCI reads .lmo and ignores .po, so that gap is silent and worth closing — but it will not run your build for you, and it refuses to package sources.

Not there yet

SDK builds · GitHub Action and reusable workflow · Cloudflare R2 and rsync · SBOM · key rotation commands · reusing an already-published package instead of re-signing it unchanged.


A feed built with it

VizzleTF/owfeed-packages — a live feed carrying a LuCI theme and a static Go binary across 20 architectures. Pull requests build, sign, index and check the whole thing with a throwaway key, so a fork never comes near the feed's own.

Someone else's CI, my feed

A feed that carries other people's packages cannot hand them its signing key, and does not have to. Authors build and sign in their own CI and publish a signed release; the feed pulls, verifies the author's signature against a pinned key, and adds its own.

apk signature blocks are additive, so the package a router installs carries both — the author's signature travels all the way to the device rather than being checked at ingest and discarded.

owfeed sign                       # in the author's CI, with the author's key
owfeed doctor --require-origin    # every package says which repository it is from
If you are the author

Your repository has an ipk line and never builds an index: the feed at the far end signs that with its own key. What you publish is a release plus a signed inventory of it.

owfeed --frozen-lock build
owfeed release --repo "$GITHUB_REPOSITORY" --tag "$GITHUB_REF_NAME"

release writes manifest.txt — what belongs to this release, each file's size and hash — and signs it with a usign key, the scheme OpenWrt already ships so a router can verify it with nothing installed. The manifest records which repository it belongs to and readers check that: a signature proves who wrote something, never what it is about, so without that line a manifest lifted from another of your releases verifies perfectly as this one.

Release assets are flat, and an apk's filename carries no architecture — in a feed the architecture is the directory. A package built for twenty architectures would therefore produce twenty files with one name, so release appends the architecture where names collide, and only where they collide: a noarch package keeps the filename an installer already on a router looks it up by.

podkop_autoupdater is a worked example, including what its signing key does and does not vouch for.

The feed's CONTRIBUTING walks through both sides.

Docs

License

Apache-2.0.

Directories

Path Synopsis
cmd
owfeed command
Command owfeed builds, signs and publishes apk feeds for OpenWrt 25.12 and later.
Command owfeed builds, signs and publishes apk feeds for OpenWrt 25.12 and later.
internal
apk
arch
Package arch derives the set of architectures a release line publishes.
Package arch derives the set of architectures a release line publishes.
build
Package build produces .apk packages with `apk mkpkg`.
Package build produces .apk packages with `apk mkpkg`.
config
Package config parses owfeed.yml.
Package config parses owfeed.yml.
doctor
Package doctor checks a feed before its subscribers do.
Package doctor checks a feed before its subscribers do.
feedindex
Package feedindex reads a published index, whichever package manager wrote it.
Package feedindex reads a published index, whichever package manager wrote it.
index
Package index signs packages and builds the repository index.
Package index signs packages and builds the repository index.
ipk
Package ipk builds packages for OpenWrt 24.10 and earlier.
Package ipk builds packages for OpenWrt 24.10 and earlier.
ipkindex
Package ipkindex builds the repository index opkg reads on 24.10 and earlier.
Package ipkindex builds the repository index opkg reads on 24.10 and earlier.
keys
Package keys generates and loads the EC keys that sign an OpenWrt apk feed.
Package keys generates and loads the EC keys that sign an OpenWrt apk feed.
lmo
Package lmo compiles gettext catalogues into the LMO format LuCI reads.
Package lmo compiles gettext catalogues into the LMO format LuCI reads.
lock
Package lock reads and writes owfeed.lock.
Package lock reads and writes owfeed.lock.
meta
Package meta holds the metadata rules apk enforces on a package, expressed as checks that run before apk does.
Package meta holds the metadata rules apk enforces on a package, expressed as checks that run before apk does.
release
Package release assembles what an author publishes so a feed can consume it.
Package release assembles what an author publishes so a feed can consume it.
smoke
Package smoke installs a built feed on a real OpenWrt image.
Package smoke installs a built feed on a real OpenWrt image.
snippet
Package snippet renders the instructions a feed's subscribers follow.
Package snippet renders the instructions a feed's subscribers follow.
testapk
Package testapk hands tests a real apk-tools 3.x, or skips them.
Package testapk hands tests a real apk-tools 3.x, or skips them.
usign
Package usign verifies OpenWrt's usign (signify-style ed25519) signatures.
Package usign verifies OpenWrt's usign (signify-style ed25519) signatures.
verify
Package verify checks a published feed from outside, over its documented URL.
Package verify checks a published feed from outside, over its documented URL.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL