orchard
mirrorEvery site I host, in one repo, along with the Cloudflare Tunnel and Caddy that front them. It's all Go, Vite, and SQLite, and it runs on a desktop at home with nothing listening on an inbound port.
blogbuncaddycloudflare-tunneldockergogolanghomelabhtml-templatemonorepoself-hostedseosqlitestatic-sitetypstuptime-monitoringviteweb-analytics
1package main
2
3import (
4 "database/sql"
5 "errors"
6 "fmt"
7)
8
9// runInit seeds the account and prints the first set of recovery codes, which
10// are what the first login uses. Nothing here needs ntfy, so a fresh machine can
11// get in before the phone is enrolled and mint everything else from inside.
12//
13// It is idempotent the same way `ntfy up` is: an account that already exists
14// keeps what it has and nothing is printed, because handing back a set of codes
15// that were never applied is worse than printing nothing.
16//
17// This runs as `docker exec orchard-auth /app -init`, which works against a
18// scratch image because exec wants a binary path rather than a shell. It must
19// never generate on first boot and print to stdout, since container stdout ships
20// to logging.bythewood.me and that would write recovery codes into a store with
21// a long retention on it.
22func runInit(db *sql.DB) error {
23 created, err := createUser(db, seedUsername)
24 if err != nil {
25 return err
26 }
27 if !created {
28 user, err := loadUser(db)
29 if err != nil {
30 return err
31 }
32 remaining, err := countRecoveryCodes(db)
33 if err != nil {
34 return err
35 }
36 fmt.Printf("already initialized as %q, with %d recovery codes left, and nothing was changed.\n",
37 user.Username, remaining)
38 fmt.Println("to replace the codes, sign in and use the security page.")
39 return nil
40 }
41
42 codes, err := regenerateRecoveryCodes(db)
43 if err != nil {
44 return err
45 }
46
47 fmt.Printf("\nthe account is %q, and these are its recovery codes:\n\n", seedUsername)
48 for _, code := range codes {
49 fmt.Printf(" %s\n", code)
50 }
51 fmt.Printf("\nthese are the only copies, so put them in 1Password now. each one\n")
52 fmt.Printf("works once. sign in with one at %s/recovery, then\n", baseURL)
53 fmt.Printf("subscribe the phone to the %q topic and regenerate them from the\n", ntfyTopic)
54 fmt.Printf("security page, so the set you keep was never on a terminal.\n")
55 return nil
56}
57
58// runCheck reports what exists without changing anything, which is what
59// `make doctor` calls. It has to be its own flag rather than a second run of
60// -init, since that would create the account on a machine that has none.
61func runCheck(db *sql.DB) error {
62 user, err := loadUser(db)
63 if errors.Is(err, errNoUser) {
64 fmt.Println("not initialized")
65 return nil
66 }
67 if err != nil {
68 return err
69 }
70 remaining, err := countRecoveryCodes(db)
71 if err != nil {
72 return err
73 }
74 fmt.Printf("initialized as %s, %d recovery codes left\n", user.Username, remaining)
75 return nil
76}
77
78// runRecovery replaces the recovery codes from the command line, for the case
79// that has no other way out: no codes left and ntfy or the tunnel down, so the
80// browser cannot reach a sign in and the security page is unreachable.
81//
82// It needs host access to the Docker socket, which is the point. That is the
83// bottom of the ladder and it is why nothing about this account has to be kept
84// anywhere: every credential here can be replaced from the machine.
85func runRecovery(db *sql.DB) error {
86 if _, err := loadUser(db); errors.Is(err, errNoUser) {
87 return fmt.Errorf("not initialized yet, run -init instead")
88 } else if err != nil {
89 return err
90 }
91
92 codes, err := regenerateRecoveryCodes(db)
93 if err != nil {
94 return err
95 }
96
97 fmt.Printf("\nevery previous recovery code stopped working just now. the new set:\n\n")
98 for _, code := range codes {
99 fmt.Printf(" %s\n", code)
100 }
101 fmt.Printf("\neach one works once, at %s/recovery.\n", baseURL)
102 return nil
103}