Skip to content

Commit 3b6b92b

Browse files
committed
feat: split out agent executable
This allows for a smaller agent executable that uses less (locked) memory. Refactor the package `pago` into a number of packages to enable separating the agent. Stop locking memory in the main pago executable when contacting the agent. It potentially confuses the user's security expectations because operations without an agent don't do it. v0.10.0
1 parent 5d046f4 commit 3b6b92b

File tree

15 files changed

+589
-398
lines changed

15 files changed

+589
-398
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/attic/
22
/cmd/pago/pago
3+
/cmd/pago-agent/pago-agent
34
/dist/
45
/help
56
/pago

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ Once Go is installed on your system, run the following command:
6868

6969
```
7070
go install dbohdan.com/pago/cmd/pago@latest
71+
go install dbohdan.com/pago/cmd/pago-agent@latest
7172
```
7273

7374
Shell completion files for Bash and fish are available in [`completions/`](completions/).
@@ -185,6 +186,11 @@ pago show foo/bar
185186
# Start manually.
186187
pago agent start
187188

189+
# By default, the agent locks its memory to prevent secrets from being written to swap.
190+
# You may need to run `ulimit -l 100000` to let it lock enough memory.
191+
# Alternatively, you can disable this feature with the flag `--no-mlock`.
192+
pago agent start --no-mlock
193+
188194
# Run without an agent.
189195
pago -s '' show foo/bar
190196

@@ -194,6 +200,8 @@ pago agent stop
194200

195201
### Environment variables
196202

203+
- `PAGO_AGENT`:
204+
The agent executable path
197205
- `PAGO_CLIP`:
198206
The command to use to copy the password to the clipboard.
199207
The default differs by platform.
@@ -208,6 +216,9 @@ pago agent stop
208216
Whether to use Git
209217
- `PAGO_LENGTH`:
210218
The default length of random passwords
219+
- `PAGO_MLOCK`:
220+
Whether the agent should lock its memory using [mlockall(2)](https://pubs.opengroup.org/onlinepubs/9799919799/functions/mlockall.html) to prevent secrets from being written to swap.
221+
`0` to disable.
211222
- `PAGO_PATTERN`:
212223
The default character pattern (regular expression) for random passwords
213224
- `PAGO_SOCK`:

Taskfile.yml

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
version: '3'
22

33
vars:
4-
cmd_dir: cmd/pago
54
ext: '{{if eq OS "windows"}}.exe{{end}}'
5+
pago_agent_dir: cmd/pago-agent
6+
pago_dir: cmd/pago
67

78
env:
89
CGO_ENABLED: 0
@@ -21,34 +22,46 @@ tasks:
2122
desc: 'Build all components'
2223
deps:
2324
- build_pago
25+
- build_pago_agent
2426

2527
build_pago:
26-
dir: '{{.cmd_dir}}'
28+
dir: '{{.pago_dir}}'
2729
desc: 'Build the pago binary'
2830
cmds:
2931
- cmd: go build -trimpath
3032
sources:
3133
- '*.go'
32-
- '../../*.go'
34+
- '../**/*.go'
3335
generates:
3436
- pago
3537

38+
build_pago_agent:
39+
dir: '{{.pago_agent_dir}}'
40+
desc: 'Build the pago agent binary'
41+
cmds:
42+
- cmd: go build -trimpath
43+
sources:
44+
- '*.go'
45+
- '../**/*.go'
46+
generates:
47+
- pago-agent
48+
3649
clean:
37-
dir: '{{.cmd_dir}}'
3850
desc: 'Clean up binaries'
3951
cmds:
40-
- rm -f pago pago.exe
52+
- rm -f {{.pago_dir}}/pago{{.ext}} {{.pago_agent_dir}}/pago-agent{{.ext}}
4153

4254
release:
4355
desc: 'Prepare a release'
4456
deps:
4557
- build_pago
4658
cmds:
47-
- VERSION=$({{.cmd_dir}}/pago{{.ext}} version) go run script/release.go
59+
- VERSION=$({{.pago_dir}}/pago{{.ext}} version) go run script/release.go
4860

4961
test:
5062
desc: 'Run tests'
5163
deps:
5264
- build_pago
65+
- build_pago_agent
5366
cmds:
5467
- go test ./...

cmd/pago/agent.go renamed to agent/agent.go

Lines changed: 29 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
1-
//go:build !windows
2-
31
// pago - a command-line password manager.
42
//
53
// License: MIT.
64
// See the file LICENSE.
75

8-
package main
6+
package agent
97

108
import (
119
"bytes"
@@ -19,136 +17,56 @@ import (
1917
"syscall"
2018

2119
"dbohdan.com/pago"
20+
"dbohdan.com/pago/crypto"
2221

2322
"filippo.io/age"
2423
"github.com/tidwall/redcon"
2524
"github.com/valkey-io/valkey-go"
2625
)
2726

28-
var defaultSocket = filepath.Join(defaultCacheDir, agentSocketPath)
29-
30-
func (cmd *RestartCmd) Run(config *Config) error {
31-
if config.Verbose {
32-
printRepr(cmd)
33-
}
34-
35-
if err := pago.LockMemory(); err != nil {
36-
return err
37-
}
38-
39-
_, _ = messageAgent(config.Socket, "SHUTDOWN")
40-
41-
identitiesText, err := decryptIdentities(config.Identities)
42-
if err != nil {
43-
return err
44-
}
45-
46-
return startAgentProcess(config.Socket, identitiesText)
47-
}
48-
49-
func (cmd *RunCmd) Run(config *Config) error {
50-
if config.Verbose {
51-
printRepr(cmd)
52-
}
53-
54-
if err := pago.LockMemory(); err != nil {
55-
return err
56-
}
57-
58-
return runAgent(config.Socket)
59-
}
60-
61-
func (cmd *StartCmd) Run(config *Config) error {
62-
if config.Verbose {
63-
printRepr(cmd)
64-
}
65-
66-
if err := pago.LockMemory(); err != nil {
67-
return err
68-
}
69-
70-
if err := pingAgent(config.Socket); err == nil {
71-
return fmt.Errorf("found agent responding on socket")
72-
}
73-
74-
identitiesText, err := decryptIdentities(config.Identities)
75-
if err != nil {
76-
return err
77-
}
78-
79-
return startAgentProcess(config.Socket, identitiesText)
80-
}
81-
82-
func (cmd *StatusCmd) Run(config *Config) error {
83-
if config.Verbose {
84-
printRepr(cmd)
85-
}
86-
87-
err := pingAgent(config.Socket)
88-
if err == nil {
89-
fmt.Println("Ping successful")
90-
os.Exit(0)
91-
} else {
92-
fmt.Println("Failed to ping agent")
93-
os.Exit(1)
94-
}
95-
96-
return nil
97-
}
98-
99-
func (cmd *StopCmd) Run(config *Config) error {
100-
if config.Verbose {
101-
printRepr(cmd)
102-
}
103-
104-
_, err := messageAgent(config.Socket, "SHUTDOWN")
105-
return err
106-
}
107-
108-
func startAgentProcess(agentSocket, identitiesText string) error {
109-
// The agent is the same executable.
110-
exe, err := os.Executable()
111-
if err != nil {
112-
return fmt.Errorf("failed to get executable path: %v", err)
27+
func StartProcess(executable string, mlock bool, socket, identitiesText string) error {
28+
mlockFlag := "--mlock"
29+
if !mlock {
30+
mlockFlag = "--no-mlock"
11331
}
11432

115-
cmd := exec.Command(exe, "agent", "run", "--socket", agentSocket)
33+
cmd := exec.Command(executable, "run", mlockFlag, "--socket", socket)
11634

11735
// Start the process in the background.
11836
if err := cmd.Start(); err != nil {
11937
return fmt.Errorf("failed to start agent: %v", err)
12038
}
12139

122-
_ = os.Remove(agentSocket)
40+
_ = os.Remove(socket)
12341
// Don't wait for the process to finish.
12442
go func() {
12543
_ = cmd.Wait()
12644
}()
12745

128-
if err := waitUntilAvailable(agentSocket, waitForSocket); err != nil {
46+
if err := pago.WaitUntilAvailable(socket, pago.WaitForSocket); err != nil {
12947
return fmt.Errorf("timed out waiting for agent socket")
13048
}
13149

132-
_, err = messageAgent(agentSocket, "IDENTITIES", identitiesText)
50+
_, err := Message(socket, "IDENTITIES", identitiesText)
13351
return err
13452
}
13553

136-
func runAgent(agentSocket string) error {
137-
if err := pingAgent(agentSocket); err == nil {
54+
func Run(socket string) error {
55+
if err := Ping(socket); err == nil {
13856
return fmt.Errorf("found agent responding on socket")
13957
}
14058

141-
socketDir := filepath.Dir(agentSocket)
142-
if err := os.MkdirAll(socketDir, dirPerms); err != nil {
59+
socketDir := filepath.Dir(socket)
60+
if err := os.MkdirAll(socketDir, pago.DirPerms); err != nil {
14361
return fmt.Errorf("failed to create socket directory: %v", err)
14462
}
14563

146-
os.Remove(agentSocket)
64+
os.Remove(socket)
14765

14866
identities := []age.Identity{}
14967
srv := redcon.NewServerNetwork(
15068
"unix",
151-
agentSocket,
69+
socket,
15270
func(conn redcon.Conn, cmd redcon.Command) {
15371
switch strings.ToUpper(string(cmd.Args[0])) {
15472

@@ -162,7 +80,7 @@ func runAgent(agentSocket string) error {
16280

16381
// Decrypt the data.
16482
reader := bytes.NewReader(encryptedData)
165-
decryptedReader, err := wrapDecrypt(reader, identities...)
83+
decryptedReader, err := crypto.WrapDecrypt(reader, identities...)
16684
if err != nil {
16785
conn.WriteError("ERR failed to decrypt: " + err.Error())
16886
return
@@ -218,22 +136,22 @@ func runAgent(agentSocket string) error {
218136
return
219137
}
220138

221-
if err := os.Chmod(agentSocket, filePerms); err != nil {
222-
exitWithError("failed to set permissions on agent socket: %v", err)
139+
if err := os.Chmod(socket, pago.FilePerms); err != nil {
140+
pago.ExitWithError("failed to set permissions on agent socket: %v", err)
223141
}
224142
}()
225143

226144
return srv.ListenServeAndSignal(errc)
227145
}
228146

229-
func messageAgent(agentSocket string, args ...string) (string, error) {
147+
func Message(socket string, args ...string) (string, error) {
230148
// Check socket security.
231-
if err := checkSocketSecurity(agentSocket); err != nil {
149+
if err := checkSocketSecurity(socket); err != nil {
232150
return "", fmt.Errorf("socket security check failed: %v", err)
233151
}
234152

235153
// Connect to the server.
236-
opts, err := valkey.ParseURL("unix://" + agentSocket)
154+
opts, err := valkey.ParseURL("unix://" + socket)
237155
if err != nil {
238156
return "", fmt.Errorf("failed to parse socket URL: %v", err)
239157
}
@@ -269,23 +187,23 @@ func messageAgent(agentSocket string, args ...string) (string, error) {
269187
return string(result), nil
270188
}
271189

272-
func pingAgent(agentSocket string) error {
273-
_, err := messageAgent(agentSocket)
190+
func Ping(socket string) error {
191+
_, err := Message(socket)
274192
return err
275193
}
276194

277-
func decryptWithAgent(agentSocket string, data []byte) (string, error) {
278-
return messageAgent(agentSocket, "DECRYPT", valkey.BinaryString(data))
195+
func Decrypt(socket string, data []byte) (string, error) {
196+
return Message(socket, "DECRYPT", valkey.BinaryString(data))
279197
}
280198

281-
func checkSocketSecurity(agentSocket string) error {
282-
info, err := os.Stat(agentSocket)
199+
func checkSocketSecurity(socket string) error {
200+
info, err := os.Stat(socket)
283201
if err != nil {
284202
return fmt.Errorf("failed to stat socket: %v", err)
285203
}
286204

287205
// Check socket permissions.
288-
if info.Mode().Perm() != filePerms {
206+
if info.Mode().Perm() != pago.FilePerms {
289207
return fmt.Errorf("incorrect socket permissions: %v", info.Mode().Perm())
290208
}
291209

0 commit comments

Comments
 (0)