Skip to content

Commit 8a9721e

Browse files
authored
fix: auto-generate and trust CA cert during install (#30)
## Summary - `greyproxy install` now generates the CA certificate if missing and attempts to trust it via sudo on all install paths (fresh, reinstall, brew) - Fixes brew install never generating or trusting certs - If sudo fails (non-interactive brew postflight, user cancels), install continues and tells the user to run `greyproxy cert install` - Simplifies README: merges duplicate install sections, removes redundant manual cert steps - UI settings page now shows `greyproxy cert install` instead of raw sudo commands when cert is not trusted ## Test plan - [ ] Fresh `greyproxy install` generates cert and prompts for sudo trust - [ ] `greyproxy install` with existing trusted cert skips cert steps - [ ] `greyproxy install -f` (brew postflight) generates and trusts cert without interactive prompt; continues gracefully if sudo fails - [ ] Dashboard Settings > TLS tab shows `greyproxy cert install` when cert is not trusted - [ ] `greyproxy uninstall` still removes trusted cert
1 parent 6c9386f commit 8a9721e

File tree

4 files changed

+116
-38
lines changed

4 files changed

+116
-38
lines changed

README.md

Lines changed: 23 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ This software is meant to be used with [**greywall**](https://github.com/Greyhav
3535
```bash
3636
brew tap greyhavenhq/tap
3737
brew install greyproxy
38+
greyproxy install
3839
```
3940

4041
### Build from Source
@@ -43,53 +44,50 @@ brew install greyproxy
4344
git clone https://github.com/greyhavenhq/greyproxy.git
4445
cd greyproxy
4546
go build ./cmd/greyproxy
47+
./greyproxy install
4648
```
4749

48-
**macOS only:** after building, codesign the binary to avoid Gatekeeper quarantine:
50+
**macOS only:** after building, codesign the binary before installing to avoid Gatekeeper quarantine:
4951

5052
```bash
5153
codesign --sign - --force ./greyproxy
5254
```
5355

54-
Install the binary and register it as a service:
55-
56-
```bash
57-
./greyproxy install
58-
```
56+
Alternatively, use [`greywall setup`](https://github.com/GreyhavenHQ/greywall) to handle the full build and install automatically.
5957

60-
This copies the binary to `~/.local/bin/`, registers a launchd user agent (macOS) or systemd user service (Linux), and starts it automatically. The dashboard will be available at `http://localhost:43080`.
58+
### What `install` Does
6159

62-
Generate and install the CA certificate for HTTPS inspection:
60+
`greyproxy install` handles the full setup in one step:
6361

64-
```bash
65-
greyproxy cert generate
66-
greyproxy cert install
67-
```
62+
1. Copies the binary to `~/.local/bin/` (skipped for Homebrew installs)
63+
2. Registers a launchd user agent (macOS) or systemd user service (Linux)
64+
3. Generates a CA certificate for HTTPS inspection (if not already present)
65+
4. Installs the CA certificate into the OS trust store (requires sudo)
66+
5. Starts the service
6867

69-
`greyproxy install` generates the certificate automatically on first install if one does not exist. If you regenerate the certificate later, greyproxy detects the change and reloads automatically — no restart needed. You can also trigger a reload manually:
68+
The dashboard will be available at `http://localhost:43080`.
7069

71-
```bash
72-
greyproxy cert reload
73-
```
70+
If you decline the sudo prompt for certificate trust, HTTPS inspection will not work until you run `greyproxy cert install` manually.
7471

75-
Alternatively, use [`greywall setup`](https://github.com/GreyhavenHQ/greywall) to handle the full build and install automatically.
76-
77-
### Install
78-
79-
Install the binary to `~/.local/bin/` and register it as a systemd user service:
72+
To remove everything:
8073

8174
```bash
82-
./greyproxy install
75+
greyproxy uninstall
8376
```
8477

85-
This copies the binary, registers a systemd user service, and starts it automatically. The dashboard will be available at `http://localhost:43080`.
78+
### Certificate Management
8679

87-
To remove everything:
80+
The CA certificate is generated and trusted automatically during `greyproxy install`. For manual control:
8881

8982
```bash
90-
greyproxy uninstall
83+
greyproxy cert generate # regenerate the CA certificate
84+
greyproxy cert install # trust it on the OS (requires sudo)
85+
greyproxy cert uninstall # remove from OS trust store
86+
greyproxy cert reload # reload cert in running server (no restart needed)
9187
```
9288

89+
If you regenerate the certificate, greyproxy detects the file change and reloads it automatically.
90+
9391
### Run in Foreground
9492

9593
To run the server directly without installing as a service:

cmd/greyproxy/install.go

Lines changed: 88 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -106,11 +106,18 @@ func handleInstall(args []string) {
106106
}
107107

108108
label := serviceLabel()
109+
step := 3
109110
fmt.Printf("Ready to install greyproxy. This will:\n")
110111
fmt.Printf(" 1. Copy %s -> %s\n", binSrc, binDst)
111112
fmt.Printf(" 2. Register greyproxy as a %s\n", label)
112-
fmt.Printf(" 3. Generate CA certificate (if not already present)\n")
113-
fmt.Printf(" 4. Start the service\n")
113+
if _, err := os.Stat(filepath.Join(greyproxyDataHome(), "ca-cert.pem")); os.IsNotExist(err) {
114+
fmt.Printf(" %d. Generate and trust CA certificate (requires sudo)\n", step)
115+
step++
116+
} else if !isCertInstalled() {
117+
fmt.Printf(" %d. Install CA certificate into OS trust store (requires sudo)\n", step)
118+
step++
119+
}
120+
fmt.Printf(" %d. Start the service\n", step)
114121

115122
if !force {
116123
fmt.Printf("\nProceed? [Y/n] ")
@@ -158,6 +165,9 @@ func handleBrewInstall(brewBin string, force bool) {
158165
}
159166
fmt.Printf("Registered %s\n", label)
160167

168+
// Generate and trust CA certificate
169+
ensureCert()
170+
161171
if err := service.Control(s, "start"); err != nil {
162172
fmt.Fprintf(os.Stderr, "error: starting service: %v\n", err)
163173
os.Exit(1)
@@ -227,11 +237,8 @@ func freshInstall(binSrc, binDst string) {
227237
}
228238
fmt.Printf("Registered %s\n", label)
229239

230-
// Generate CA certificate if not already present
231-
certFile := filepath.Join(greyproxyDataHome(), "ca-cert.pem")
232-
if _, err := os.Stat(certFile); os.IsNotExist(err) {
233-
handleCertGenerate(false)
234-
}
240+
// Generate and trust CA certificate
241+
ensureCert()
235242

236243
// Start service
237244
if err := service.Control(s, "start"); err != nil {
@@ -241,6 +248,80 @@ func freshInstall(binSrc, binDst string) {
241248
fmt.Println("Service started")
242249
}
243250

251+
// ensureCert generates the CA certificate if missing and attempts to install
252+
// it into the OS trust store. If the trust step fails (e.g. sudo unavailable
253+
// in a non-interactive context), installation continues but the user is told
254+
// to run 'greyproxy cert install' manually.
255+
func ensureCert() {
256+
certFile := filepath.Join(greyproxyDataHome(), "ca-cert.pem")
257+
if _, err := os.Stat(certFile); os.IsNotExist(err) {
258+
handleCertGenerate(false)
259+
}
260+
261+
if isCertInstalled() {
262+
return
263+
}
264+
265+
if !tryCertInstall() {
266+
fmt.Println("\n HTTPS inspection requires a trusted CA certificate.")
267+
fmt.Println(" Run 'greyproxy cert install' to complete the setup.")
268+
}
269+
}
270+
271+
// tryCertInstall attempts to install the CA certificate into the OS trust
272+
// store. Returns true on success, false if the install failed (e.g. sudo
273+
// not available or user denied the prompt).
274+
func tryCertInstall() bool {
275+
certFile := filepath.Join(greyproxyDataHome(), "ca-cert.pem")
276+
if _, err := os.Stat(certFile); os.IsNotExist(err) {
277+
return false
278+
}
279+
280+
switch runtime.GOOS {
281+
case "darwin":
282+
// Remove any existing Greyproxy CA cert to avoid errSecDuplicateItem
283+
exec.Command("security", "delete-certificate", "-c", "Greyproxy CA").Run()
284+
285+
fmt.Println("Installing CA certificate into system trust store (requires sudo)...")
286+
cmd := exec.Command("sudo", "security", "add-trusted-cert",
287+
"-d", "-p", "ssl", "-p", "basic",
288+
"-k", "/Library/Keychains/System.keychain",
289+
certFile,
290+
)
291+
cmd.Stdout = os.Stdout
292+
cmd.Stderr = os.Stderr
293+
cmd.Stdin = os.Stdin
294+
if err := cmd.Run(); err != nil {
295+
return false
296+
}
297+
fmt.Println("CA certificate installed and trusted")
298+
return true
299+
300+
case "linux":
301+
destPath, updateCmd := linuxCertInstallInfo()
302+
fmt.Println("Installing CA certificate into system trust store (requires sudo)...")
303+
cpCmd := exec.Command("sudo", "cp", certFile, destPath)
304+
cpCmd.Stdout = os.Stdout
305+
cpCmd.Stderr = os.Stderr
306+
cpCmd.Stdin = os.Stdin
307+
if err := cpCmd.Run(); err != nil {
308+
return false
309+
}
310+
updCmd := exec.Command("sudo", updateCmd)
311+
updCmd.Stdout = os.Stdout
312+
updCmd.Stderr = os.Stderr
313+
updCmd.Stdin = os.Stdin
314+
if err := updCmd.Run(); err != nil {
315+
return false
316+
}
317+
fmt.Printf("CA certificate installed and trusted at %s\n", destPath)
318+
return true
319+
320+
default:
321+
return false
322+
}
323+
}
324+
244325
func askConfirm() bool {
245326
var answer string
246327
fmt.Scanln(&answer)

internal/greyproxy/api/cert.go

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,14 +66,13 @@ func buildCertStatus(dataHome string) certStatusResponse {
6666
return resp
6767
}
6868

69-
func buildInstallCommands(certPath string) map[string]string {
69+
func buildInstallCommands(_ string) map[string]string {
7070
cmds := make(map[string]string)
7171
switch runtime.GOOS {
7272
case "darwin":
73-
cmds["macos"] = fmt.Sprintf("sudo security add-trusted-cert -d -p ssl -p basic -k /Library/Keychains/System.keychain \"%s\"", certPath)
73+
cmds["macos"] = "greyproxy cert install"
7474
case "linux":
75-
destPath, updateCmd := linuxCertInstallInfo()
76-
cmds["linux"] = fmt.Sprintf("sudo cp \"%s\" %s && sudo %s", certPath, destPath, updateCmd)
75+
cmds["linux"] = "greyproxy cert install"
7776
}
7877
return cmds
7978
}

internal/greyproxy/ui/templates/settings.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ <h2 class="text-lg font-semibold mb-4">TLS Interception</h2>
217217
<div id="cert-install-section" class="hidden mb-4">
218218
<div class="p-4 bg-muted rounded-lg border border-border">
219219
<p class="font-medium text-sm mb-2">Install in system trust store</p>
220-
<p class="text-xs text-muted-foreground mb-3">Run this command in your terminal to trust the CA certificate system-wide:</p>
220+
<p class="text-xs text-muted-foreground mb-3">Run this in your terminal to trust the CA certificate system-wide (requires sudo):</p>
221221
<div class="relative">
222222
<pre id="cert-install-cmd" class="text-xs bg-background rounded p-3 pr-10 overflow-x-auto border border-border font-mono"></pre>
223223
<button onclick="copyInstallCmd()" class="absolute top-2 right-2 p-1 rounded hover:bg-muted-foreground/10" title="Copy to clipboard">
@@ -663,7 +663,7 @@ <h2 class="text-lg font-semibold mb-4">Active Sessions</h2>
663663
document.getElementById('cert-install-section').classList.add('hidden');
664664
} else {
665665
icon.innerHTML = '<svg class="h-5 w-5 text-yellow-600 dark:text-yellow-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" /></svg>';
666-
text.innerHTML = '<span class="text-yellow-600 dark:text-yellow-500">Certificate generated but not installed in system trust store</span>';
666+
text.innerHTML = '<span class="text-yellow-600 dark:text-yellow-500">Certificate generated but not trusted by your OS</span>';
667667
if (data.expiresAt) {
668668
text.innerHTML += ' <span class="text-muted-foreground">&middot; expires ' + data.expiresAt.split('T')[0] + '</span>';
669669
}

0 commit comments

Comments
 (0)