Loading INSTALL.md 0 → 100644 +182 −0 Original line number Diff line number Diff line # Installing BULLETIN ## Prerequisites * Go 1.25 or later * A Unix user account dedicated to running BULLETIN * SSH server with `AuthorizedKeysCommand` support (OpenSSH 6.2+) * Root access (for sshd configuration) ## Building ``` make ``` This produces a `bulletin` binary in the current directory. Install it somewhere permanent: ``` sudo cp bulletin /usr/local/bin/bulletin ``` ## Initial Setup Run the install command as the dedicated BULLETIN user: ``` bulletin -u SYSTEM -b install ``` This will interactively prompt for: * **System name** -- identifies this BULLETIN instance. * **Default expiration** -- message lifetime in days, or -1 for permanent (recommended). * **Expiration limit** -- maximum allowed expiration, or -1 for no limit. * **Initial user** -- login, name, and SSH public key for the first user (who will be an admin). * **Open registration** -- whether unknown SSH keys can self-onboard. If enabled, you can optionally add shibboleth challenge questions that new users must answer before creating an account. The install creates the following: | Path | Purpose | |---------------------------------------|-----------------------------| | `~/.local/share/BULLETIN/bulletin.db` | SQLite database | | `~/.config/BULLETIN/` | Command history files | | `~/.bulletin-installed` | Prevents re-running install | It also installs two crontab entries: ``` @reboot /usr/local/bin/bulletin -u SYSTEM -b reboot @daily /usr/local/bin/bulletin -u SYSTEM -b expire ``` ## Configuring SSHD If you enabled open registration, the install command will print an sshd configuration block. If you skipped it, or want to set it up manually, add the following to `/etc/ssh/sshd_config.d/50-bulletin.conf`: ``` Match User bulletin PasswordAuthentication no PubkeyAuthentication yes PermitTTY yes X11Forwarding no AllowTcpForwarding no PermitTunnel no AuthorizedKeysFile none ForceCommand /usr/sbin/nologin AuthorizedKeysCommand /usr/local/bin/bulletin authorized-keys %u %t %k AuthorizedKeysCommandUser bulletin ``` Replace `bulletin` with whatever Unix user runs BULLETIN, and adjust the path to the binary as needed. Then reload sshd: ``` sudo systemctl reload sshd ``` With this configuration, sshd delegates key lookup to BULLETIN. When a user connects, BULLETIN checks the presented key against its database and emits the appropriate forced command. Unknown keys are either rejected or directed to the onboarding flow, depending on the open registration setting. **Without open registration**, you can skip the sshd configuration entirely. BULLETIN will use `~/.ssh/authorized_keys` directly, managed via the `SSH ADD`, `SSH FETCH`, and `SSH DELETE` commands within BULLETIN. ## Adding Users There are several ways to add users: ### From the BULLETIN prompt (admin) ``` BULLETIN> USER ADD LOGIN "Full Name" BULLETIN> SSH FETCH LOGIN github github-username ``` ### From the command line ``` bulletin -u SYSTEM -b new-user LOGIN github github-username ``` This creates the user (if they don't exist) and fetches their SSH keys from the specified code forge. Supported sites: `github`, `gitlab`, `codeberg`. ### Self-service (open registration) If open registration is enabled and sshd is configured with `AuthorizedKeysCommand`, users with unknown SSH keys are presented with an onboarding flow where they can create their own account or link their key to an existing account using a link code (generated via `SSH LINK`). ## Migrating from Older Installs If you have an existing BULLETIN install that stores keys only in `~/.ssh/authorized_keys`, run: ``` bulletin -u SYSTEM -b migrate-keys ``` This reads the authorized_keys file, imports each BULLETIN key entry into the database, creates any missing users, and rewrites the file with the current secure format (`restrict,pty`). ## Connecting Users connect via SSH: ``` ssh bulletin@your-host ``` They are dropped into the BULLETIN REPL. Type `HELP` for available commands. ## Maintenance The crontab handles routine maintenance automatically: * **`@reboot`** -- deletes shutdown messages after a system restart. * **`@daily`** -- deletes expired messages and vacuums the database. ### Manual administration From within BULLETIN, admins can: * `USER LIST` -- list all users * `USER DISABLE login` / `USER ENABLE login` -- disable or re-enable users * `USER DELETE login` -- permanently remove a user * `SET DEFAULT_EXPIRE days` -- change default message expiration (-1 = permanent) * `SET REGISTRATION OPEN` / `SET REGISTRATION CLOSED` -- toggle self-service onboarding * `SET SHIBBOLETH` -- manage challenge questions for onboarding * `SHOW SYSTEM` -- display system information and uptime ## File Layout ``` ~/.local/share/BULLETIN/ bulletin.db SQLite database (all users, folders, messages, keys) ~/.config/BULLETIN/ LOGIN.history Command history per user .mail.history Mail sub-app history ~/.ssh/ authorized_keys Used when AuthorizedKeysCommand is not configured ~/.bulletin-installed Touch file from initial install ``` README.md +1 −2 Original line number Diff line number Diff line Loading @@ -49,5 +49,4 @@ some of the contributors. The place to get it seems to be the [DECUS archives](http://decuslib.com/). I tracked it down with help from Kent Brodie who I discovered via [an old USENET post](https://groups.google.com/forum/#!search/bulletin$20vms/comp.os.vms/rzM2LQMl6Jo/y1BKhO7dv80J) where he too was trying to track the software down. In 1994. Where he too was trying to track the software down. In 1994. batch/batch.go +5 −5 Original line number Diff line number Diff line Loading @@ -107,13 +107,13 @@ the first user.`) system.Name, err = ask.GetLine("Enter system name: ") system.Name = strings.ToUpper(system.Name) ask.CheckErr(err) system.DefaultExpire, err = ask.GetInt64("Enter default expiry in days (180): ") system.DefaultExpire, err = ask.GetInt64("Enter default expiry in days (-1 = permanent): ") if err != nil { system.DefaultExpire = 180 system.DefaultExpire = -1 } system.ExpireLimit, err = ask.GetInt64("Enter expiration limit in days (365): ") system.ExpireLimit, err = ask.GetInt64("Enter expiration limit in days (-1 = no limit): ") if err != nil { system.ExpireLimit = 365 system.ExpireLimit = -1 } err = q.SetSystem(ctx, storage.SetSystemParams{ Name: system.Name, Loading Loading @@ -161,7 +161,7 @@ the first user.`) Subject: seedMsgs[i].Subject, Message: seedMsgs[i].Message, CreateAt: seedMsgs[i].Date, Expiration: time.Now().UTC(), Expiration: time.Date(9999, 12, 31, 23, 59, 59, 0, time.UTC), })) } Loading folders/messages.go +9 −3 Original line number Diff line number Diff line Loading @@ -22,12 +22,18 @@ func CreateMessage(author, subject, message, folder string, permanent, system, s } if days <= 0 { days = sysdef.DefaultExpire } else { } else if sysdef.ExpireLimit > 0 { days = min(days, sysdef.ExpireLimit) } if days < 0 { permanent = 1 exp := time.Date(9999, 12, 31, 23, 59, 59, 0, time.UTC) expiration = &exp } else { exp := time.Now().AddDate(0, 0, int(days)).UTC() expiration = &exp } } err := this.Q.CreateMessage(ctx, storage.CreateMessageParams{ Folder: folder, Author: author, Loading repl/command.go +4 −7 Original line number Diff line number Diff line Loading @@ -265,9 +265,9 @@ BULLCOM.CLD.`, Sets the default number of days for messages to expire. Default value is 14.`, Default value is -1 (permanent).`, OptArg: true, Default: "14", Default: "-1", }, "/OWNER": { Description: `/OWNER=username Loading Loading @@ -1315,8 +1315,7 @@ privileged qualifier.`, }, "DEFAULT_EXPIRE": { Description: `Specifies the number of days the message is to be retained. The default is 14 days. The highest limit that can be specified is 30 days. This can be overridden by a user with privileges. is permanent (no expiration). This also specifies the default expiration date when adding a message. If no expiration date is entered when prompted for a date, or if Loading @@ -1326,9 +1325,7 @@ used. Format: SET DEFAULT_EXPIRE days If -1 is specified, messages will become permanent. If 0 is specified, no default expiration date will be present. The latter should never be specified for a folder or else the messages will disappear.`, If -1 is specified, messages will become permanent.`, MinArgs: 1, MaxArgs: 1, Action: ActionSetDefaultExpire, Loading Loading
INSTALL.md 0 → 100644 +182 −0 Original line number Diff line number Diff line # Installing BULLETIN ## Prerequisites * Go 1.25 or later * A Unix user account dedicated to running BULLETIN * SSH server with `AuthorizedKeysCommand` support (OpenSSH 6.2+) * Root access (for sshd configuration) ## Building ``` make ``` This produces a `bulletin` binary in the current directory. Install it somewhere permanent: ``` sudo cp bulletin /usr/local/bin/bulletin ``` ## Initial Setup Run the install command as the dedicated BULLETIN user: ``` bulletin -u SYSTEM -b install ``` This will interactively prompt for: * **System name** -- identifies this BULLETIN instance. * **Default expiration** -- message lifetime in days, or -1 for permanent (recommended). * **Expiration limit** -- maximum allowed expiration, or -1 for no limit. * **Initial user** -- login, name, and SSH public key for the first user (who will be an admin). * **Open registration** -- whether unknown SSH keys can self-onboard. If enabled, you can optionally add shibboleth challenge questions that new users must answer before creating an account. The install creates the following: | Path | Purpose | |---------------------------------------|-----------------------------| | `~/.local/share/BULLETIN/bulletin.db` | SQLite database | | `~/.config/BULLETIN/` | Command history files | | `~/.bulletin-installed` | Prevents re-running install | It also installs two crontab entries: ``` @reboot /usr/local/bin/bulletin -u SYSTEM -b reboot @daily /usr/local/bin/bulletin -u SYSTEM -b expire ``` ## Configuring SSHD If you enabled open registration, the install command will print an sshd configuration block. If you skipped it, or want to set it up manually, add the following to `/etc/ssh/sshd_config.d/50-bulletin.conf`: ``` Match User bulletin PasswordAuthentication no PubkeyAuthentication yes PermitTTY yes X11Forwarding no AllowTcpForwarding no PermitTunnel no AuthorizedKeysFile none ForceCommand /usr/sbin/nologin AuthorizedKeysCommand /usr/local/bin/bulletin authorized-keys %u %t %k AuthorizedKeysCommandUser bulletin ``` Replace `bulletin` with whatever Unix user runs BULLETIN, and adjust the path to the binary as needed. Then reload sshd: ``` sudo systemctl reload sshd ``` With this configuration, sshd delegates key lookup to BULLETIN. When a user connects, BULLETIN checks the presented key against its database and emits the appropriate forced command. Unknown keys are either rejected or directed to the onboarding flow, depending on the open registration setting. **Without open registration**, you can skip the sshd configuration entirely. BULLETIN will use `~/.ssh/authorized_keys` directly, managed via the `SSH ADD`, `SSH FETCH`, and `SSH DELETE` commands within BULLETIN. ## Adding Users There are several ways to add users: ### From the BULLETIN prompt (admin) ``` BULLETIN> USER ADD LOGIN "Full Name" BULLETIN> SSH FETCH LOGIN github github-username ``` ### From the command line ``` bulletin -u SYSTEM -b new-user LOGIN github github-username ``` This creates the user (if they don't exist) and fetches their SSH keys from the specified code forge. Supported sites: `github`, `gitlab`, `codeberg`. ### Self-service (open registration) If open registration is enabled and sshd is configured with `AuthorizedKeysCommand`, users with unknown SSH keys are presented with an onboarding flow where they can create their own account or link their key to an existing account using a link code (generated via `SSH LINK`). ## Migrating from Older Installs If you have an existing BULLETIN install that stores keys only in `~/.ssh/authorized_keys`, run: ``` bulletin -u SYSTEM -b migrate-keys ``` This reads the authorized_keys file, imports each BULLETIN key entry into the database, creates any missing users, and rewrites the file with the current secure format (`restrict,pty`). ## Connecting Users connect via SSH: ``` ssh bulletin@your-host ``` They are dropped into the BULLETIN REPL. Type `HELP` for available commands. ## Maintenance The crontab handles routine maintenance automatically: * **`@reboot`** -- deletes shutdown messages after a system restart. * **`@daily`** -- deletes expired messages and vacuums the database. ### Manual administration From within BULLETIN, admins can: * `USER LIST` -- list all users * `USER DISABLE login` / `USER ENABLE login` -- disable or re-enable users * `USER DELETE login` -- permanently remove a user * `SET DEFAULT_EXPIRE days` -- change default message expiration (-1 = permanent) * `SET REGISTRATION OPEN` / `SET REGISTRATION CLOSED` -- toggle self-service onboarding * `SET SHIBBOLETH` -- manage challenge questions for onboarding * `SHOW SYSTEM` -- display system information and uptime ## File Layout ``` ~/.local/share/BULLETIN/ bulletin.db SQLite database (all users, folders, messages, keys) ~/.config/BULLETIN/ LOGIN.history Command history per user .mail.history Mail sub-app history ~/.ssh/ authorized_keys Used when AuthorizedKeysCommand is not configured ~/.bulletin-installed Touch file from initial install ```
README.md +1 −2 Original line number Diff line number Diff line Loading @@ -49,5 +49,4 @@ some of the contributors. The place to get it seems to be the [DECUS archives](http://decuslib.com/). I tracked it down with help from Kent Brodie who I discovered via [an old USENET post](https://groups.google.com/forum/#!search/bulletin$20vms/comp.os.vms/rzM2LQMl6Jo/y1BKhO7dv80J) where he too was trying to track the software down. In 1994. Where he too was trying to track the software down. In 1994.
batch/batch.go +5 −5 Original line number Diff line number Diff line Loading @@ -107,13 +107,13 @@ the first user.`) system.Name, err = ask.GetLine("Enter system name: ") system.Name = strings.ToUpper(system.Name) ask.CheckErr(err) system.DefaultExpire, err = ask.GetInt64("Enter default expiry in days (180): ") system.DefaultExpire, err = ask.GetInt64("Enter default expiry in days (-1 = permanent): ") if err != nil { system.DefaultExpire = 180 system.DefaultExpire = -1 } system.ExpireLimit, err = ask.GetInt64("Enter expiration limit in days (365): ") system.ExpireLimit, err = ask.GetInt64("Enter expiration limit in days (-1 = no limit): ") if err != nil { system.ExpireLimit = 365 system.ExpireLimit = -1 } err = q.SetSystem(ctx, storage.SetSystemParams{ Name: system.Name, Loading Loading @@ -161,7 +161,7 @@ the first user.`) Subject: seedMsgs[i].Subject, Message: seedMsgs[i].Message, CreateAt: seedMsgs[i].Date, Expiration: time.Now().UTC(), Expiration: time.Date(9999, 12, 31, 23, 59, 59, 0, time.UTC), })) } Loading
folders/messages.go +9 −3 Original line number Diff line number Diff line Loading @@ -22,12 +22,18 @@ func CreateMessage(author, subject, message, folder string, permanent, system, s } if days <= 0 { days = sysdef.DefaultExpire } else { } else if sysdef.ExpireLimit > 0 { days = min(days, sysdef.ExpireLimit) } if days < 0 { permanent = 1 exp := time.Date(9999, 12, 31, 23, 59, 59, 0, time.UTC) expiration = &exp } else { exp := time.Now().AddDate(0, 0, int(days)).UTC() expiration = &exp } } err := this.Q.CreateMessage(ctx, storage.CreateMessageParams{ Folder: folder, Author: author, Loading
repl/command.go +4 −7 Original line number Diff line number Diff line Loading @@ -265,9 +265,9 @@ BULLCOM.CLD.`, Sets the default number of days for messages to expire. Default value is 14.`, Default value is -1 (permanent).`, OptArg: true, Default: "14", Default: "-1", }, "/OWNER": { Description: `/OWNER=username Loading Loading @@ -1315,8 +1315,7 @@ privileged qualifier.`, }, "DEFAULT_EXPIRE": { Description: `Specifies the number of days the message is to be retained. The default is 14 days. The highest limit that can be specified is 30 days. This can be overridden by a user with privileges. is permanent (no expiration). This also specifies the default expiration date when adding a message. If no expiration date is entered when prompted for a date, or if Loading @@ -1326,9 +1325,7 @@ used. Format: SET DEFAULT_EXPIRE days If -1 is specified, messages will become permanent. If 0 is specified, no default expiration date will be present. The latter should never be specified for a folder or else the messages will disappear.`, If -1 is specified, messages will become permanent.`, MinArgs: 1, MaxArgs: 1, Action: ActionSetDefaultExpire, Loading