| FUGUWEB(1) | General Commands Manual | FUGUWEB(1) |
NAME
fuguweb — build a
documentation website for a Perl project
SYNOPSIS
fuguweb |
[-hq] [--project
dir] command
[argument ...] |
fuguweb |
init [dir] |
fuguweb |
build [--out
dir] |
fuguweb |
check [-v]
[--out dir] |
fuguweb |
clean [--out
dir] |
fuguweb |
mint-key --purpose
word [--type
type] --secret
path [--signer
path] [--bind
stem=path]
[--email address]
[--expires date]
[--dir name]
[--bootstrap --org
word [--url
prefix]] |
fuguweb |
import-key --purpose
word [--type
type] --file
path --secret
path [--signer
path] [--bind
stem=path]
[--dir name]
[--bootstrap --org
word [--url
prefix]] |
fuguweb |
promote-key --purpose
word --retiring
path [--secret
path] [--signer
path] [--bind
stem=path]
[--dir name] |
fuguweb |
help [command] |
DESCRIPTION
fuguweb renders one static website from
the documentation that a Perl project already keeps:
mdoc(7) manuals, POD
sidecars, and Markdown. It runs
mandoc(1),
lowdown(1) and
pod2man(1), and
wraps each result in one shared page layout. The output is plain HTML and
one stylesheet. There is no templating language and no JavaScript.
A project is a directory that holds a
.fuguwebrc file, the site description. Without
--project, fuguweb searches
up from the current directory for that file. The directory that holds it is
the project root. Every relative path in the description, and a relative
--out, resolves against the project root.
The build reads nothing from the network, writes nothing outside the output directory, and gives the same bytes for the same checkout.
The options are as follows:
-h,--help- Print the usage message and exit.
--projectdir- Use dir as the project root, and do not search for one.
-q,--quiet- Do not print informational messages.
The commands are as follows:
init[dir]- Write a starter .fuguwebrc into dir, which defaults to the current directory. The starter names one page and one navigation entry, and it builds once web/index.body.html exists. The command refuses a directory that already holds a description.
build[--outdir]- Render the whole site into the output directory.
--outoverrides theout_dirsetting.The command first checks that every renderer is installed, and names the first one that is not. It runs mandoc(1) in lint mode over every mdoc source, and a warning fails the build. It then writes the stylesheet, the assets and each key directory. It writes each page into each language tree, and then the pages of the top level: the copy of 404.html when the description holds that page, and the forward index.html. Next it writes one page for each manual into each tree. Last, it removes every file of the site that the description no longer names, so a second build over one directory gives the same tree as the first. See THE PAGE LAYOUT and THE OUTPUT DIRECTORY.
check[-v] [--outdir]- Check a built site. The command prints one line for each problem and exits
with status 5. It prints nothing and exits with status 0 when the site is
good.
The checks are these:
- Every page and asset of the site exists and is not empty.
- On every page but the forward index.html, the check finds a title, the banner link and the whole navigation.
- Each of those links takes the form of the kind of its page: <page> on a tree page, ../en/<page> on the key page, and en/<page> on the copy of 404.html at the top level. An absolute URL and a fragment stay as they stand.
- Every tree page carries the code of its tree as
‘
lang’. - The forward index.html holds a meta refresh to the entry page of en/. Its one reference is a link to that page, and it holds no navigation.
- Every local link, manual cross-reference, and fragment resolves.
- No reference uses a ‘
file:’ URL, or reads as a URL scheme. - No reference starts with a solidus. The one exception is the
‘
<base href="/">’ of the copy of 404.html at the top level, and that page must carry it. - Every page of each language tree is reachable from the entry page of
that tree, except a page marked
unlinked. The pages of the top level and the key page take no such walk. - The output holds the site and nothing else, at any depth.
A description with a
keysblock adds the checks of THE KEY DIRECTORY.-v,--verbosealso prints every external link. The command never fetches one. clean[--outdir]- Remove the output directory. The command deletes a tree without asking, so
it removes a built site and refuses anything else. See
THE OUTPUT DIRECTORY.
With
--outthe command runs without a loadable description. It then knows the files of the top level and the language trees, by their shape. It refuses a target that holds no stylesheet, because every build writes one. mint-key--purposeword [--typetype]--secretpath [--signerpath] [--bindstem=path] [--emailaddress] [--expiresdate] [--dirname] [--bootstrap--orgword [--urlprefix]]- Mint the next key of a purpose. The type defaults to
signify, andopenpgpis the other type that this release mints. An OpenPGP mint needs--email, and it takes an optional--expires. A mint of another type takes neither option. See KEY ROTATION. import-key--purposeword [--typetype]--filepath--secretpath [--signerpath] [--bindstem=path] [--dirname] [--bootstrap--orgword [--urlprefix]]- Publish a key that another tool made, under the next name of the purpose.
The command takes
--purpose,--signerand--bindas a mint of that purpose does, with--filebeside them. The type defaults tosignify, andopenpgpandx509are the other two. An issuer makes anx509certificate, so this command is the one that publishes one. It takes no--emailand no--expires, because it generates no key. The block of an importedopenpgpkey therefore carries noemail, and the Web Key Directory serves no file for the address of its user id. The KEYS file of its own directory still holds that key. TheEncryptionfield of security.txt can still name it, when that directory is the one that names the contact. A directory with nokeysblock takes one from--bootstrap,--dir,--organd--url, as a mint does.--secretnames the private half of the key in--file, and that file must stand. The command writes no key file of its own, and that private half signs the binding of the new key. A mint refuses a--secretthat stands already, because a mint writes one. See KEY ROTATION. promote-key--purposeword--retiringpath [--secretpath] [--signerpath] [--bindstem=path] [--dirname]- Make the
nextkey of a purpose current, and retire the key that was current. See KEY ROTATION. help[command]- Print the usage of the tool, or of one command.
THE SITE DESCRIPTION
The description is .fuguwebrc at the
project root. A setting is one name
= value line. A block opens
with its type, a name, and a brace, and closes with a brace on a line of its
own. A name that holds whitespace takes double quotes. A block with a
language code in front of its name is a translation, and
LANGUAGES states its rules. A
‘#’ starts a comment, and no value can
hold one.
The settings are as follows:
site- The name of the site. It ends the title of every page, and it is the text of the banner link. This is the one required setting.
out_dir- The output directory. The default is web/build.
source_dir- The source directory. The default is web.
entry- The front page of each language tree, and the target of the banner link. The default is index.html.
module_root- The prefix that a module name drops. The default is lib.
mandoc_os- The operating system name in the footer of every rendered manual. The
default is ‘
OpenBSD’. man_url- The manual host for a cross-reference to a manual outside the site. The default is https://man.openbsd.org/.
stylesheet- The path of the base stylesheet. By default the build searches for the sheet that ships with the tool.
A path setting must not step out of the project.
The block types are as follows:
- One navigation entry, in file order. Every page carries it, except the
forward index.html at the top level. The block
must set a
label. page"file"- One page of each language tree, named by its file in the tree. The name
must be one path segment. The block sets a
title, which defaults to the file name, and exactly one source:body- An HTML body fragment in the source directory.
markdown- A Markdown file, relative to the project root. lowdown(1) renders it, and the HTML that it holds stays as written.
index=yes- The generated manual index. The page can also set a
body, which opens the index below its heading.
unlinked=yesmarks a page that no other page links to, such as 404.html, and the reachability check skips it. manualsname- One group of
mdoc(7) manuals.
The name is one word of lowercase letters, digits and hyphens, and it is
the fragment identifier of the group on the index page. Two groups must
not share a name. The block must set
heading, the text of the heading of the group, anddir, the directory of the sources. An optionalnamespaceprefixes each manual name, so man/fugu/Daemon.3p with the namespace ‘Fugu::’ becomes ‘Fugu::Daemon(3p)’. modulesname- One group of POD sidecars. The name follows the rules of a
manualsname. The block must setheadinganddir, anddirmust lie belowmodule_root. keys"name"- One published key directory. A description can hold several, and two blocks must not name one directory. See THE KEY DIRECTORY.
The load fails when a block names no source or two sources, when
two blocks name one page, or when a group directory does not exist. A
symlink at the top level of the source directory fails the load. A symlink
below the source directory on the path of a fragment that the build reads
fails it too. A group block that sets anchor, or
whose name is a heading text, fails the load. The message names the block to
write in its place, such as ‘write manuals commands
with heading = Commands, and no anchor’. Every message names
the file and the block at fault.
LANGUAGES
English is the default language, and its code is
‘en’. A translation is a block with a
language code in front of its name. The code is two lowercase letters, per
ISO 639-1. The block translates the block of one argument of the same type
and name:
page "index.html" {
title = Home
body = index.body.html
}
page de "index.html" {
title = Start
body = de/index.body.html
}
manuals commands {
heading = Commands
dir = man/fuguweb
}
manuals de commands {
heading = Befehle
dir = man/de/fuguweb
}
The languages of a site are
‘en’ and each code that a translation
names. The build writes one tree for each language, and each tree holds the
same pages. A page, a nav, a
manuals and a modules block
can take a translation:
A page of a language takes each value of its translation, and the
value of the default block where the translation holds none. A page whose
body comes from the default source carries
‘lang="en"’ on the element
that holds the body. The index page marks each part on its own element
instead: the title, the body, each group heading and
each manual description. On another tree, each part of the default language
carries ‘lang="en"’.
The dir of a group translation mirrors the
default dir. A manual of a language comes from the
file of the same relative path in that directory, and from the default
source when the directory holds no such file. A
modules translation mirrors the umbrella sidecar as
well. A manual keeps the page name of its default source in every
language.
The load fails when a code has another shape, or is
‘en’. It fails when a translation
names no default block of its type, when it comes twice for one name and one
language, or when it sets a setting outside its list. It fails when a manual
source of a translation dir has no source of the
same path in the default dir. A
keys block and a key block
take no translation.
THE PAGE LAYOUT
Every source becomes one HTML body fragment, and the build wraps each fragment in one fixed layout: the head, a banner, the navigation, the fragment, and a footer. A project decides the site name, the navigation, and the footer prose.
Every page of the description sits in the tree of its language,
below <code>/ of the output. English is the
default language, and its tree en/ holds every page
of the description. The ‘html’ element
of a page carries the code of its tree as
‘lang’. The banner and the navigation
of a tree page name each page of the tree by its file name. An absolute URL
stays as it stands. After the navigation entries, a tree page links the same
page in each other language of the site, as
../<code>/<page>. The code is the text
and the ‘hreflang’ of each link. A
site of one language carries no such link.
The top level holds two pages for the host. The forward
index.html leads to the entry page of
en/ with a meta refresh, and holds one link to it.
Its title is the site name alone. It carries no banner, no navigation and no
footer, and it loads no stylesheet. When the description holds the page
404.html, the top level holds one more copy of it,
with ‘<base
href="/">’. A host serves that copy at each path
that no file answers, and each relative link of the page resolves from the
root of the origin. Its banner and its navigation therefore link
en/<page>.
On every page but the forward, the title is the title of the page,
an em dash, and the site name. The banner links the site name to the entry
page. The navigation lists each nav block, in file
order.
A footer.body.html in the source directory becomes the footer prose of each tree page. A language takes its own footer from <code>/footer.body.html in the source directory, and the default one when it has none. The key page and the pages of the top level carry no footer. A project without one gets no footer element.
The base stylesheet ships with the tool as
share/fuguweb/style.css. The build copies it to
style.css at the top level of the output, outside
every tree. A tree page and the key page link it as
../style.css, and the copy of the 404 page links it
as style.css. The stylesheet
setting names another sheet. A stylesheet that is not found fails the
build.
MANUALS
A manuals group reads its directory for
the plain files *.1, *.3p,
*.5, *.7 and
*.8. It sorts them by section, in the order 1, 3p,
5, 7, 8, and then by file name. The build renders each source with
mandoc(1) and
names the page
‘<name>.<section>.html’. A
cross-reference to a manual of the site becomes a local link. Any other
cross-reference links to the man_url host.
A modules group finds every
.pod file below its directory, and the sidecar that
names the directory itself: lib/App/FuguWeb.pod is
the umbrella of lib/App/FuguWeb/. The manual name is
the path below module_root with
‘::’ between the segments, and the
section is ‘3p’. The build renders
each sidecar with
pod2man(1) and
then mandoc(1), so
a module page carries the same layout as a hand-written manual. The page
date is the date of the last commit, from
git(1). A
‘L<>’ reference renders as text,
not as a link.
The index page is the page block with
index =
yes. Its heading is the title of the page, and its
body, when it sets one, follows the heading. The
index writes no prose of its own. Each group then adds one heading, with the
group name as its fragment identifier and the
heading of the language as its text, and one list of
its manuals. Each entry links the page and shows the one-line description,
from the first Nd macro of an mdoc source or the
NAME section of a sidecar. A group with no manual adds
nothing.
THE SOURCE DIRECTORY
The source directory holds the body fragments, the footer, and the assets. Every plain file directly in it that the build does not render is an asset, and the build copies it as it stands. A *.body.html fragment, a *.md file, and a dot file are not assets. robots.txt and CNAME therefore need no entry in the description, and notes for the maintainers are not published.
The build copies each asset to the top level of the output, outside every language tree. A tree page sits one directory below the top level. A body fragment, a Markdown page and the footer of a tree page therefore link an asset as ../<asset>.
The body of 404.html is the one exception.
The build writes it into each tree, and into the copy at the top level. The
check resolves each link of that copy against the top level, so there it
refuses ../<asset>. Each relative link of an
asset fails in one of the two copies, and a link that starts with a solidus
fails in both. The body fragment or Markdown page of
404.html must therefore link an asset with an
absolute URL, such as
‘https://example.org/logo.png’.
The build rejects a symlink at the top level of the source directory. It also rejects a symlink on the path of each fragment that it reads. The rule holds for the file, and for each directory between the source directory and the file. These fragments are the body fragment of each page in each language, the footer, and the footer of each language. An asset or a fragment would publish the bytes of the target, from anywhere on the machine.
THE OUTPUT DIRECTORY
The top level of the output holds the two pages for the host, the stylesheet and the assets. Each language tree is one flat directory of pages below it. The key directory and the well-known tree are the other parts below the top level.
The build writes plain files only. It refuses a symlink on any
path that it writes, before it writes one file. It owns a plain file at the
top level, and a plain file directly in a directory of the top level whose
name has the shape of a language code. It also owns a file that the site
names, and a file that has a shape of the key directory. A page of an older
flat layout, and the tree of a language that the description dropped, are
therefore removed. The build keeps every other entry, reports it, and never
removes it. clean removes what the build owns, and a
directory of the site, of a language tree, or of the key tree. It refuses
every other entry, and names it, so it never removes a file that the build
keeps.
Both build and
clean refuse a target that no build may own: the
root of the filesystem, the home directory, the project root, and any
directory that holds the project. They refuse the source directory and every
directory below it. The output directory of the description is the one
exception, because the default output directory sits inside the default
source directory.
The build stages every manual source in
.man/<code> inside the output directory, one
directory for each language, so that
mandoc(1) resolves
a cross-reference against the manuals of the site. The build removes the
staging directory when it ends. It refuses a staging directory of another
shape, and so does clean.
THE KEY DIRECTORY
An organization publishes its public keys under one prefix, so a
consumer can fetch a key and verify a release with it. A
keys block describes that directory, and the build
publishes it. A description can hold several such blocks, and each one names
one directory. Two blocks must not name one directory.
Each directory holds its own root of trust, and a binding never crosses a directory. A publisher that keeps two trust domains therefore keeps two directories in one site. The site-level files stay single: one security.txt, and one Web Key Directory that gathers every address.
keys "keys" {
org = fugubsd
contact = mailto:security@fugubsd.org
expires = 2027-09-07T00:00:00Z
url = https://www.fugubsd.org/keys
}
key "fugubsd-1-release" {
status = current
since = 2026-09-07
}
The block name is the directory, one path segment below the source directory. A name of two lowercase letters is refused, because a language tree takes a name of that shape in the output. The settings are as follows:
org- The organization word, the first field of every key name. This setting is required. Two blocks must not name one word. A key name holds no directory name, and the serial of a purpose counts inside one directory, so a mint of the second block would write a name that the first block publishes.
contact- The ‘
Contact’ field of security.txt. expires- The ‘
Expires’ field of security.txt, an RFC 3339 timestamp.contactandexpiresstand or fall together. A site publishes one security.txt, so one block alone names the two. url- The published prefix of the directory, an absolute URL.
Each key block names the stem of one key
file in the directory. The file extension gives the type:
.pub is a
signify(1) key,
.asc is an armored OpenPGP key, and
.pem is one X.509 certificate. The block sets a
status of current,
next or retired, and
optional since and until
dates. An OpenPGP key can set an email and a
fingerprint of 40 hexadecimal characters. A
certificate can set a fingerprint of 64 hexadecimal
characters, which is the SHA-256 of its DER form.
One signify key of the directory is the root of trust, and its
purpose word is root. The current root key signs the
manifest, and every other key of the directory is a subordinate key. A
binding is a detached signature by one key over the public key file of
another key, and its name is <target file>.<signer
stem>.<ext>. The extension is sig
for a signify(1)
signer, asc for an OpenPGP signer, and
p7s for an X.509 signer. A binding needs no
key block: its name holds the target, the signer and
the type.
The directory must hold a SHA256 manifest and its SHA256.sig signature. The build copies each key file, each binding file and the manifest pair byte for byte. It signs nothing, and it verifies no SHA256.sig. For the example above it generates these files:
- keys/KEYS
- Every OpenPGP key, when the set holds one.
- keys/index.html
- The human page, with the serial, purpose, type, status, fingerprint, dates and bindings of each key. The row of a certificate names its subject and its validity dates beside the fingerprint. The page is a page of the default language: its banner and its navigation link ../en/<page>, and it carries no footer.
- .well-known/openpgpkey/hu/<hash>
- The Web Key Directory file of each email address. The file holds the keys of that address of every directory, in publication order.
- .well-known/openpgpkey/policy
- The Web Key Directory policy file.
- .well-known/security.txt
- The RFC 9116 file, when a block sets a
contact. TheEncryptionfield names the current OpenPGP key of that block.
check reads the source key directory, so
the answer does not depend on a build. It checks that every key file has a
block and every block has a file, that every name matches the key name
pattern or the binding name pattern, that every purpose holds exactly one
current key, that the directory holds one current signify key of the purpose
root, that each binding verifies against the public
key of its signer and holds the retention rule, that the manifest names
every key file and every binding file with the digest that it has and
nothing else, that every declared fingerprint equals the computed one, and
that the signature has the shape of a
signify(1)
signature.
One report reads the whole site. The block which names the contact
and a url must hold a
current OpenPGP key of its own, because the
Encryption field names no key of another directory.
The check reports that block when another key directory holds such a key. A
site that publishes no OpenPGP key at all writes no such field, and that one
is no problem.
The retention rule holds each binding of the directory. A binding
whose signer is current or
next must target the current root key. A binding
whose signer is retired must target a key of its own
purpose with a higher serial. A signify binding verifies with no command. A
binding of another type needs
gpg(1) or
openssl(1), and
an absent command is a problem.
The check reads the validity of each
current and next key that
carries one. It reads the expiry of an OpenPGP key with
gpg(1), and the
‘notBefore’ and
‘notAfter’ of a certificate from the
certificate itself. An expiry that passed is a problem, and so is a
certificate whose ‘notBefore’ has not
come. An expiry within 30 days of a current key is a
problem when its purpose holds no next key, because
one rotation runs in two steps. A signify key carries no date, and an
OpenPGP key with no expiry reaches no rule.
A key verb fails for none of these reports, so
mint-key and import-key each
run on a directory that check rejects for a date. An
expired current key is still
current when the verb reads its work back, so a step
of that purpose could never be made. The 30-day report reads the whole
directory, so it would fail a step of another purpose, which writes no
next key of the purpose that the report names.
promote-key fails when the key that
retires is an OpenPGP key whose expiry passed. That key signs the chain
binding, and gpg(1)
signs nothing with an expired key. Promote the successor before the current
key expires.
KEY ROTATION
Every key verb writes one key directory.
--dir names it. A description with one
keys block needs no name, and one with several
refuses a step without it.
A consumer verifies a release against the copy of the public key that it holds. A release that a new key signs therefore fails in each consumer that holds the old copy. One rotation runs in two steps.
One signify key of the directory is the root of trust, and its
purpose word is root. The current root key signs the
manifest, and every other key binds to it. mint-key
and import-key refuse a key of another type for the
root purpose, and the mint refuses before it
generates one. Every step takes the private half of the current root as
--signer, with two exceptions below.
mint-key generates the next key of the
purpose, writes its public half into the key directory, and writes its
private half to --secret with no group and no other
permissions. The new key takes the status next, or
current when the purpose holds no current key.
--secret must not name a file that exists, and must
not name the signer. A mint refuses a purpose that already holds a
next key. A mint of a subordinate purpose also
writes the binding of the new key over the public key file of the current
root.
promote-key makes the
next key of the purpose current, and retires the key
that was current with an until date.
--retiring names the private half of the key that
retires, and that key signs the chain binding over the key which takes its
place. A consumer that trusts the old key therefore reaches the new one. The
promote removes the root binding of the retired key, and the chain binding
stays published. A retired key keeps its file and its URL, so a release that
it signed still verifies.
The key types
signify(1)
generates a signify key. The public half is one
comment line and one key body.
gpg(1)
generates an openpgp key: one Ed25519 primary key
with one Curve25519 encryption subkey. --email names
the address, which is the one user id of the key, and a correspondent
encrypts to the subkey. Both halves are armored text. The
key block of the new key takes that
email and the fingerprint,
which the command reads from the key that it generated.
--expires is a date of the form
YYYY-MM-DD, and the key expires at the start of that
date, in UTC. The date must be after the day of the run. The day of the run
and each earlier date fail the mint, and the reason names the date that you
gave. A mint with no --expires makes a key with no
expiry. A key directory retires a key with an until
date, so a rotation needs no expiry on the key itself.
An issuer makes an x509 certificate, such
as a code signing certificate of Apple Developer ID, so no verb mints one.
import-key publishes it:
--file names one PEM certificate, and
--secret names its unencrypted PEM private key. The
verbs take no passphrase, so convert a PKCS#12 file with
openssl pkcs12 first. The private key signs the
binding of the new key, and
openssl(1) makes
that detached CMS signature. The key block of the
new key takes the fingerprint, which the command
reads from the certificate that it published. The renderer knows no issuer
by name, and the root manifest vouches for the certificate. A renewal is a
rotation: the new certificate enters as next, and a
promote retires the old one.
The first root mint
The first mint of the root purpose finds
no root. That key is current at once, it signs its own manifest, and it
takes no --signer. A directory that already holds
keys takes one --bind
stem=path for each current key
and each next key of a subordinate purpose, and the step writes the binding
of every one of them again. It refuses before it writes when one private
half is absent.
A directory with no keys block takes one
from the command line: --bootstrap states the intent
to make that directory, --dir names it, which
defaults to keys, --org
names the organization word, and --url names the
published prefix. A second key directory of a site arrives the same way.
A step without --bootstrap refuses a
--dir that no keys block
names. Each key directory holds a root of trust of its own, so a mistyped
name would otherwise publish a second root in silence.
mint-key and import-key take
the option, and promote-key makes no directory.
The root promote
A root promote signs the manifest with the key that it makes
current, so it takes --secret and no
--signer. It takes one
--bind for each key in force, as the first root mint
does, and it removes each binding of the retired root but the chain binding
that names it.
Each step verifies the new signature and each new binding against the published public key of the signer before one byte reaches the key directory, and a step that fails puts every file back. On success the command prints one name=value line for each fact: the file name, the stem, the serial, the status, the retired name of a promote, and the digest and the URL of a new key file.
The command reaches no network and holds no token. The caller stores the private key and commits the public one.
FILES
- .fuguwebrc
- The site description, at the project root.
- share/fuguweb/style.css
- The base stylesheet, which ships with the tool.
- web/footer.body.html
- The footer prose, in the default source directory.
- web/<code>/footer.body.html
- The footer prose of one language.
- web/build/.man
- The staging directory of the build, in the default output directory.
EXIT STATUS
fuguweb exits with one of the following
values:
- 0
- Success.
- 1
- General error.
- 2
- Invalid arguments.
- 3
- Configuration error, or no .fuguwebrc found.
- 4
- The build failed: a renderer failed, or a manual source is malformed.
- 5
- The check found a problem.
- 6
- A renderer, or a command of a key step such as signify(1), gpg(1) or openssl(1), is not installed.
EXAMPLES
Start a site, write its front page, build it, and check it:
$ fuguweb init $ mkdir web $ echo "<h1>Home</h1>" >web/index.body.html $ fuguweb build $ fuguweb check
Build into another directory, from anywhere in the project:
$ fuguweb build --out /tmp/site $ fuguweb check --out /tmp/site
Publish the root key and the first release key of an organization:
$ fuguweb mint-key --purpose root --bootstrap \ --secret ~/keys/root-1.sec --org fugubsd \ --url https://www.fugubsd.org/keys $ fuguweb mint-key --purpose release \ --secret ~/keys/release-1.sec --signer ~/keys/root-1.sec
Publish an OpenPGP contact key that expires at the start of 2028:
$ fuguweb mint-key --purpose contact --type openpgp \ --email security@fugubsd.org --expires 2028-01-01 \ --secret ~/keys/contact-1.sec --signer ~/keys/root-1.sec
Publish a code signing certificate, whose private key stays with the publisher:
$ fuguweb import-key --purpose sign --type x509 \ --file ~/keys/sign-1.pem --secret ~/keys/sign-1.key \ --signer ~/keys/root-1.sec
Rotate the release key:
$ fuguweb mint-key --purpose release \ --secret ~/keys/release-2.sec --signer ~/keys/root-1.sec $ fuguweb promote-key --purpose release \ --signer ~/keys/root-1.sec --retiring ~/keys/release-1.sec
Rotate the root key. The promote writes the binding of each key in
force again, so it takes one --bind for each of
them:
$ fuguweb mint-key --purpose root \ --secret ~/keys/root-2.sec --signer ~/keys/root-1.sec $ fuguweb promote-key --purpose root \ --secret ~/keys/root-2.sec --retiring ~/keys/root-1.sec \ --bind fugubsd-2-release=~/keys/release-2.sec \ --bind fugubsd-1-contact=~/keys/contact-1.sec \ --bind fugubsd-1-sign=~/keys/sign-1.key
Publish a second key directory, for the keys of one project:
$ fuguweb mint-key --purpose root --dir project --bootstrap \ --secret ~/keys/project-root-1.sec --org fuguproj \ --url https://www.fugubsd.org/project $ fuguweb mint-key --purpose release --dir project \ --secret ~/keys/project-release-1.sec \ --signer ~/keys/project-root-1.sec
SEE ALSO
gpg(1), lowdown(1), mandoc(1), openssl(1), pod2man(1), signify(1), mdoc(7)
The module documentation is in the .pod sidecars beside lib/App/FuguWeb/.
STANDARDS
E. Foudil and Y. Shafranovich, A File Format to Aid in Security Vulnerability Disclosure, RFC 9116, April 2022.
AUTHORS
Dick Olsson <hi@senzilla.io>
| October 21, 2026 | OpenBSD |