mirror of
https://github.com/safedep/pmg.git
synced 2026-08-03 07:24:09 +02:00
feat(ebpf-poc): trust the CA machine wide instead of configuring npm
v1 owns one trust mechanism: the system trust store. Every user and every program that reads it gets the proxy CA with no configuration. Programs that carry their own certificate list are documented rather than managed, because each keeps that list in a different place and format. ca install now calls truststore.Install with system scope and verifies the result. ca remove untrusts before deleting, since a CA left trusted after its files are gone is worse than one never installed: nothing points at it, but whoever holds the old key can still intercept every user. Only the public certificate reaches the trust store. The private key stays 0600 under the proxy user, which is the entire security boundary here. Deletes npm_ca.go and the npm half of ca.go: the per user cafile handling, the previous value save and restore, the read back verification, and the --npm-user and --npm-bin flags. Verified on Ubuntu with no per tool configuration anywhere: curl without --cacert, npm without cafile, and a malicious package still blocked with 403. Removal confirmed to leave zero matches in ca-certificates.crt.
This commit is contained in:
+72
-10
@@ -239,7 +239,7 @@ Run these commands:
|
||||
|
||||
```bash
|
||||
cd ~/pmg/ebpf-poc
|
||||
sudo ./pmgwatch ca install --npm-user testuser
|
||||
sudo ./pmgwatch ca install
|
||||
sudo ./pmgwatch ca status
|
||||
```
|
||||
|
||||
@@ -250,8 +250,47 @@ The proxy reads the certificate when it starts.
|
||||
|
||||
This command does three things.
|
||||
It creates a certificate that does not change.
|
||||
It gives the private key to the proxy user.
|
||||
It tells npm to trust the certificate.
|
||||
It gives the private key to the proxy user only.
|
||||
It adds the certificate to the system trust store.
|
||||
|
||||
The system trust store makes the certificate valid for every user.
|
||||
Every program that reads the system trust store now trusts the proxy.
|
||||
|
||||
---
|
||||
|
||||
## Step 11a. Tools That Ignore the System Trust Store
|
||||
|
||||
Some programs do not read the system trust store.
|
||||
They carry their own list of certificates inside the program file.
|
||||
|
||||
| Program | Reads the system trust store |
|
||||
|---|---|
|
||||
| curl, wget, git, Go | Yes |
|
||||
| npm from `apt install nodejs` | Yes |
|
||||
| pip from `apt install python3-pip` | Yes |
|
||||
| **npm from nodejs.org, nvm, or Docker** | **No** |
|
||||
| **bun** | **No** |
|
||||
| **uv** | **No** |
|
||||
|
||||
If you use a program from the second group, configure it yourself.
|
||||
Point it at the file that Step 11 wrote:
|
||||
|
||||
```
|
||||
/var/lib/pmg-ebpf-poc/pmg-ca-bundle.pem
|
||||
```
|
||||
|
||||
Use one of these settings:
|
||||
|
||||
| Program | Setting |
|
||||
|---|---|
|
||||
| npm | `npm config set cafile /var/lib/pmg-ebpf-poc/pmg-ca-bundle.pem` |
|
||||
| Node, bun | `NODE_EXTRA_CA_CERTS=/var/lib/pmg-ebpf-poc/pmg-ca-bundle.pem` |
|
||||
| uv | `native-tls = true` in `/etc/uv/uv.toml` |
|
||||
| pip | `cert = /var/lib/pmg-ebpf-poc/pmg-ca-bundle.pem` in `/etc/pip.conf` |
|
||||
|
||||
On this guide's Ubuntu machine, npm comes from `apt`.
|
||||
It reads the system trust store.
|
||||
You do not need this step.
|
||||
|
||||
---
|
||||
|
||||
@@ -345,8 +384,7 @@ This is the purpose of the POC.
|
||||
Run this command as the test user:
|
||||
|
||||
```bash
|
||||
curl -s --cacert /var/lib/pmg-ebpf-poc/npm-ca-bundle.pem \
|
||||
https://registry.npmjs.org/safedep-test-pkg/-/safedep-test-pkg-0.1.3.tgz
|
||||
curl -s https://registry.npmjs.org/safedep-test-pkg/-/safedep-test-pkg-0.1.3.tgz
|
||||
```
|
||||
|
||||
You see the reason for the block:
|
||||
@@ -389,8 +427,25 @@ sudo -u pmg-proxy env HOME=/var/lib/pmg-proxy pmg proxy stop --state $STATE
|
||||
sudo ./pmgwatch ca remove
|
||||
```
|
||||
|
||||
`ca remove` restores the previous npm setting.
|
||||
It deletes the certificate files.
|
||||
Stop the proxy before you run `ca remove`.
|
||||
The command refuses to run while the proxy is running.
|
||||
|
||||
`ca remove` takes the certificate out of the system trust store.
|
||||
It then deletes the certificate files and the public bundle.
|
||||
|
||||
Check that the certificate is gone:
|
||||
|
||||
```bash
|
||||
grep -c "SafeDep" /etc/ssl/certs/ca-certificates.crt
|
||||
```
|
||||
|
||||
The result must be `0`.
|
||||
|
||||
Do not use `ls /etc/ssl/certs | grep pmg` for this check.
|
||||
That command can still show two links after a correct removal.
|
||||
The links point to a file that no longer exists.
|
||||
They do not grant trust.
|
||||
The `grep` command above is the reliable check.
|
||||
|
||||
---
|
||||
|
||||
@@ -404,7 +459,7 @@ It deletes the certificate files.
|
||||
| `fatal error: errno.h: No such file or directory` | The C library headers are missing | `sudo apt-get install -y build-essential` |
|
||||
| `Text file busy` | The proxy is running from that file | Stop the proxy, then copy again |
|
||||
| `map create: operation not permitted` | You are not root | Use `sudo` |
|
||||
| `UNABLE_TO_VERIFY_LEAF_SIGNATURE` | npm does not trust the certificate | Run Step 11 again |
|
||||
| `UNABLE_TO_VERIFY_LEAF_SIGNATURE` | The program does not trust the certificate | Run `sudo ./pmgwatch ca status`. If it passes, the program ignores the system trust store. See Step 11a |
|
||||
| `existing CA state was created with different options` | A previous setup used other options | Run `sudo ./pmgwatch ca remove` first |
|
||||
| `proxy pid N is still running` | The proxy is running | Stop the proxy, then install the certificate |
|
||||
| No events appear | npm used its local cache | `rm -rf ~/.npm/_cacache` |
|
||||
@@ -438,5 +493,12 @@ The POC does not handle these cases:
|
||||
- **Containers.** The hook sees container traffic. The container cannot reach
|
||||
the proxy, because `127.0.0.1` in a container is not the host.
|
||||
- **QUIC.** The hook does not block UDP port 443.
|
||||
- **Other package managers.** The certificate step configures npm only.
|
||||
pip, bun and uv are not configured.
|
||||
- **Programs with their own certificate list.** PMG adds the certificate to the
|
||||
system trust store only. It does not change the configuration of any program.
|
||||
Programs that ignore the system trust store need manual setup. See Step 11a.
|
||||
|
||||
The last item is a decision, not an oversight.
|
||||
PMG owns the one mechanism that works for every user and every program that
|
||||
reads it. Per program configuration is documented instead, because each
|
||||
program keeps its certificate list in a different place and in a different
|
||||
format.
|
||||
|
||||
+68
-105
@@ -17,14 +17,22 @@ import (
|
||||
"syscall"
|
||||
|
||||
"github.com/safedep/pmg/proxy/certmanager"
|
||||
"github.com/safedep/pmg/truststore"
|
||||
)
|
||||
|
||||
const defaultCAStateDir = "/var/lib/pmg-ebpf-poc"
|
||||
|
||||
// systemAccount is the identity the proxy runs as. The CA private key is owned
|
||||
// by it and readable by nobody else.
|
||||
type systemAccount struct {
|
||||
Name string
|
||||
UID uint32
|
||||
GID uint32
|
||||
HomeDir string
|
||||
}
|
||||
|
||||
type caCommandOptions struct {
|
||||
proxyUser string
|
||||
npmUser string
|
||||
npmBin string
|
||||
proxyConfigDir string
|
||||
bundlePath string
|
||||
statePath string
|
||||
@@ -33,23 +41,19 @@ type caCommandOptions struct {
|
||||
|
||||
type caState struct {
|
||||
ProxyUser string `json:"proxy_user"`
|
||||
NPMUser string `json:"npm_user"`
|
||||
NPMBin string `json:"npm_bin"`
|
||||
ProxyConfigDir string `json:"proxy_config_dir"`
|
||||
BundlePath string `json:"bundle_path"`
|
||||
PreviousNPMCA string `json:"previous_npm_ca,omitempty"`
|
||||
PreviousNPMSet bool `json:"previous_npm_ca_set"`
|
||||
CAFingerprint string `json:"ca_fingerprint"`
|
||||
CACreated bool `json:"ca_created"`
|
||||
ProxyStatePath string `json:"proxy_state_path"`
|
||||
}
|
||||
|
||||
type caStatus struct {
|
||||
CAValid bool
|
||||
KeyModeValid bool
|
||||
BundleValid bool
|
||||
NPMConfigured bool
|
||||
Fingerprint string
|
||||
CAValid bool
|
||||
KeyModeValid bool
|
||||
BundleValid bool
|
||||
SystemTrust bool
|
||||
Fingerprint string
|
||||
}
|
||||
|
||||
func runCACommand(args []string, out io.Writer) error {
|
||||
@@ -73,13 +77,11 @@ func runCACommand(args []string, out io.Writer) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := fmt.Fprintf(out,
|
||||
"Persistent CA: %s\nPrivate key owner: %s\nPublic npm bundle: %s\nnpm user: %s\nFingerprint: %s\n",
|
||||
certmanager.CACertPath(state.ProxyConfigDir), state.ProxyUser, state.BundlePath,
|
||||
state.NPMUser, state.CAFingerprint); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
_, err = fmt.Fprintf(out,
|
||||
"Persistent CA: %s\nPrivate key owner: %s\nSystem trust store: installed\nPublic bundle: %s\nFingerprint: %s\n",
|
||||
certmanager.CACertPath(state.ProxyConfigDir), state.ProxyUser,
|
||||
state.BundlePath, state.CAFingerprint)
|
||||
return err
|
||||
|
||||
case "status":
|
||||
opts, err := parseCAOptions("status", args[1:])
|
||||
@@ -96,7 +98,7 @@ func runCACommand(args []string, out io.Writer) error {
|
||||
if err := printCAStatus(out, status); err != nil {
|
||||
return err
|
||||
}
|
||||
if !status.CAValid || !status.KeyModeValid || !status.BundleValid || !status.NPMConfigured {
|
||||
if !status.CAValid || !status.KeyModeValid || !status.BundleValid || !status.SystemTrust {
|
||||
return errors.New("CA setup is unhealthy")
|
||||
}
|
||||
return nil
|
||||
@@ -112,10 +114,8 @@ func runCACommand(args []string, out io.Writer) error {
|
||||
if err := removeCA(opts); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := fmt.Fprintln(out, "CA setup removed"); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
_, err = fmt.Fprintln(out, "CA removed from the system trust store and deleted")
|
||||
return err
|
||||
|
||||
default:
|
||||
return fmt.Errorf("unknown CA command %q, expected install, status, or remove", args[0])
|
||||
@@ -124,13 +124,17 @@ func runCACommand(args []string, out io.Writer) error {
|
||||
|
||||
func printCAUsage(out io.Writer) error {
|
||||
_, err := fmt.Fprintln(out, `Usage:
|
||||
pmgwatch ca install --npm-user <user> [--proxy-user pmg-proxy]
|
||||
pmgwatch ca install [--proxy-user pmg-proxy]
|
||||
pmgwatch ca status
|
||||
pmgwatch ca remove
|
||||
|
||||
install generates or reuses the proxy CA and configures the selected user's npm cafile.
|
||||
status verifies the CA files, ownership, public bundle, and npm configuration.
|
||||
remove restores the previous npm cafile and deletes POC-owned CA files.`)
|
||||
install generates or reuses the proxy CA and trusts it machine wide.
|
||||
status verifies the CA files, key ownership, public bundle, and system trust.
|
||||
remove untrusts the CA and deletes the files this command created.
|
||||
|
||||
Tools that ignore the system trust store, such as Node from nodejs.org,
|
||||
bun and uv, need their own configuration. Point them at the public bundle
|
||||
this command writes. See SETUP.md.`)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -139,21 +143,17 @@ func parseCAOptions(name string, args []string) (caCommandOptions, error) {
|
||||
flags := flag.NewFlagSet("ca "+name, flag.ContinueOnError)
|
||||
flags.SetOutput(io.Discard)
|
||||
flags.StringVar(&opts.proxyUser, "proxy-user", "pmg-proxy", "user running the PMG proxy")
|
||||
flags.StringVar(&opts.npmUser, "npm-user", defaultNPMUser(), "user whose npm configuration PMG manages")
|
||||
flags.StringVar(&opts.npmBin, "npm-bin", "npm", "npm executable used to manage trust")
|
||||
flags.StringVar(&opts.proxyConfigDir, "proxy-config-dir", "", "PMG proxy config directory")
|
||||
flags.StringVar(&opts.bundlePath, "bundle", filepath.Join(defaultCAStateDir, "npm-ca-bundle.pem"), "public CA bundle for npm")
|
||||
flags.StringVar(&opts.bundlePath, "bundle", filepath.Join(defaultCAStateDir, "pmg-ca-bundle.pem"),
|
||||
"public CA bundle for tools that do not read the system trust store")
|
||||
flags.StringVar(&opts.statePath, "state", filepath.Join(defaultCAStateDir, "ca-state.json"), "CA setup state")
|
||||
flags.StringVar(&opts.proxyStatePath, "proxy-state", "", "PMG proxy state file used to guard removal")
|
||||
flags.StringVar(&opts.proxyStatePath, "proxy-state", "", "PMG proxy state file used to guard changes")
|
||||
if err := flags.Parse(args); err != nil {
|
||||
return caCommandOptions{}, err
|
||||
}
|
||||
if flags.NArg() != 0 {
|
||||
return caCommandOptions{}, fmt.Errorf("unexpected arguments: %s", strings.Join(flags.Args(), " "))
|
||||
}
|
||||
if name == "install" && opts.npmUser == "" {
|
||||
return caCommandOptions{}, errors.New("--npm-user is required when SUDO_USER is not set")
|
||||
}
|
||||
return opts, nil
|
||||
}
|
||||
|
||||
@@ -162,16 +162,11 @@ func installCA(opts caCommandOptions) (*caState, error) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("resolve proxy user: %w", err)
|
||||
}
|
||||
npmAccount, err := lookupSystemAccount(opts.npmUser)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("resolve npm user: %w", err)
|
||||
}
|
||||
resolveCAPaths(&opts, proxyAccount)
|
||||
if err := ensureProxyStopped(opts.proxyStatePath); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
npm := npmTrust{binary: opts.npmBin}
|
||||
state, err := loadCAState(opts.statePath)
|
||||
switch {
|
||||
case err == nil:
|
||||
@@ -179,18 +174,10 @@ func installCA(opts caCommandOptions) (*caState, error) {
|
||||
return nil, err
|
||||
}
|
||||
case errors.Is(err, os.ErrNotExist):
|
||||
previous, present, readErr := npm.Current(npmAccount)
|
||||
if readErr != nil {
|
||||
return nil, readErr
|
||||
}
|
||||
state = &caState{
|
||||
ProxyUser: opts.proxyUser,
|
||||
NPMUser: opts.npmUser,
|
||||
NPMBin: opts.npmBin,
|
||||
ProxyConfigDir: opts.proxyConfigDir,
|
||||
BundlePath: opts.bundlePath,
|
||||
PreviousNPMCA: previous,
|
||||
PreviousNPMSet: present,
|
||||
ProxyStatePath: opts.proxyStatePath,
|
||||
}
|
||||
default:
|
||||
@@ -202,24 +189,26 @@ func installCA(opts caCommandOptions) (*caState, error) {
|
||||
return nil, err
|
||||
}
|
||||
state.CACreated = state.CACreated || created
|
||||
|
||||
if err := writeClientBundle(opts.bundlePath, ca.Certificate); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := npm.Set(npmAccount, opts.bundlePath); err != nil {
|
||||
return nil, errors.Join(err, restoreNPM(npm, npmAccount, state))
|
||||
|
||||
// Only the public certificate reaches the trust store. The private key
|
||||
// never leaves the proxy's config directory.
|
||||
if err := truststore.Install(ca.Certificate, truststore.ScopeSystem); err != nil {
|
||||
return nil, fmt.Errorf("install CA into the system trust store: %w", err)
|
||||
}
|
||||
effective, present, err := npm.Current(npmAccount)
|
||||
if err != nil {
|
||||
return nil, errors.Join(err, restoreNPM(npm, npmAccount, state))
|
||||
}
|
||||
if !present || effective != opts.bundlePath {
|
||||
setupErr := fmt.Errorf("npm cafile is %q after setup, expected %q", effective, opts.bundlePath)
|
||||
return nil, errors.Join(setupErr, restoreNPM(npm, npmAccount, state))
|
||||
|
||||
if _, trusted, err := truststore.Status(certmanager.CACommonName); err != nil {
|
||||
return nil, fmt.Errorf("verify system trust: %w", err)
|
||||
} else if !trusted {
|
||||
return nil, errors.New("CA is not trusted after install")
|
||||
}
|
||||
|
||||
state.CAFingerprint = certificateFingerprint(ca)
|
||||
if err := saveCAState(opts.statePath, state); err != nil {
|
||||
return nil, errors.Join(err, restoreNPM(npm, npmAccount, state))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return state, nil
|
||||
@@ -235,10 +224,6 @@ func inspectCA(opts caCommandOptions) (caStatus, error) {
|
||||
if err != nil {
|
||||
return caStatus{}, err
|
||||
}
|
||||
npmAccount, err := lookupSystemAccount(opts.npmUser)
|
||||
if err != nil {
|
||||
return caStatus{}, err
|
||||
}
|
||||
|
||||
status := caStatus{}
|
||||
ca, err := certmanager.LoadCA(opts.proxyConfigDir)
|
||||
@@ -253,16 +238,17 @@ func inspectCA(opts caCommandOptions) (caStatus, error) {
|
||||
status.KeyModeValid = ok && keyInfo.Mode().Perm() == 0o600 &&
|
||||
keyStat.Uid == proxyAccount.UID && keyStat.Gid == proxyAccount.GID
|
||||
}
|
||||
|
||||
if status.CAValid {
|
||||
bundle, bundleErr := os.ReadFile(opts.bundlePath)
|
||||
status.BundleValid = bundleErr == nil && bytes.Contains(bundle, ca.Certificate)
|
||||
}
|
||||
|
||||
effective, present, npmErr := (npmTrust{binary: opts.npmBin}).Current(npmAccount)
|
||||
if npmErr != nil {
|
||||
return caStatus{}, npmErr
|
||||
_, trusted, err := truststore.Status(certmanager.CACommonName)
|
||||
if err != nil {
|
||||
return caStatus{}, fmt.Errorf("read system trust store: %w", err)
|
||||
}
|
||||
status.NPMConfigured = present && effective == opts.bundlePath
|
||||
status.SystemTrust = trusted
|
||||
|
||||
return status, nil
|
||||
}
|
||||
@@ -277,24 +263,11 @@ func removeCA(opts caCommandOptions) error {
|
||||
return err
|
||||
}
|
||||
|
||||
npmAccount, err := lookupSystemAccount(opts.npmUser)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
npm := npmTrust{binary: opts.npmBin}
|
||||
effective, present, err := npm.Current(npmAccount)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !present || effective != opts.bundlePath {
|
||||
return fmt.Errorf("npm cafile changed to %q after setup; refusing to overwrite it", effective)
|
||||
}
|
||||
if state.PreviousNPMSet {
|
||||
if err := npm.Set(npmAccount, state.PreviousNPMCA); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err := npm.Remove(npmAccount); err != nil {
|
||||
return err
|
||||
// Untrust first. A CA left in the machine trust store after its files are
|
||||
// gone is worse than one that was never installed: nothing points at it,
|
||||
// but anyone holding the old key can still intercept every user.
|
||||
if err := truststore.Uninstall(certmanager.CACommonName, truststore.ScopeSystem); err != nil {
|
||||
return fmt.Errorf("remove CA from the system trust store: %w", err)
|
||||
}
|
||||
|
||||
paths := []string{opts.bundlePath}
|
||||
@@ -337,6 +310,8 @@ func ensurePersistentCA(dir string, account systemAccount) (*certmanager.Certifi
|
||||
return nil, false, errors.New("persisted CA is expired")
|
||||
}
|
||||
|
||||
// The private key is the whole security boundary: whoever can read it can
|
||||
// mint a certificate for any domain that the machine now trusts.
|
||||
for _, item := range []struct {
|
||||
path string
|
||||
mode os.FileMode
|
||||
@@ -355,15 +330,18 @@ func ensurePersistentCA(dir string, account systemAccount) (*certmanager.Certifi
|
||||
return ca, created, nil
|
||||
}
|
||||
|
||||
// writeClientBundle writes the CA merged with the system roots. Settings such
|
||||
// as npm's cafile replace the trust set rather than adding to it, so a client
|
||||
// pointed at the bare CA would reject every other site.
|
||||
func writeClientBundle(path string, caPEM []byte) error {
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||
return fmt.Errorf("create bundle directory: %w", err)
|
||||
}
|
||||
if err := os.WriteFile(path, certmanager.MergeWithSystemCA(caPEM), 0o644); err != nil {
|
||||
return fmt.Errorf("write npm CA bundle: %w", err)
|
||||
return fmt.Errorf("write CA bundle: %w", err)
|
||||
}
|
||||
if err := os.Chmod(path, 0o644); err != nil {
|
||||
return fmt.Errorf("chmod npm CA bundle: %w", err)
|
||||
return fmt.Errorf("chmod CA bundle: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -398,8 +376,7 @@ func loadCAState(path string) (*caState, error) {
|
||||
}
|
||||
|
||||
func (s *caState) matches(opts caCommandOptions) error {
|
||||
if s.ProxyUser != opts.proxyUser || s.NPMUser != opts.npmUser ||
|
||||
s.NPMBin != opts.npmBin || s.ProxyConfigDir != opts.proxyConfigDir ||
|
||||
if s.ProxyUser != opts.proxyUser || s.ProxyConfigDir != opts.proxyConfigDir ||
|
||||
s.BundlePath != opts.bundlePath {
|
||||
return errors.New("existing CA state was created with different options; remove it before changing setup")
|
||||
}
|
||||
@@ -409,8 +386,6 @@ func (s *caState) matches(opts caCommandOptions) error {
|
||||
func (s *caState) options(statePath string) caCommandOptions {
|
||||
return caCommandOptions{
|
||||
proxyUser: s.ProxyUser,
|
||||
npmUser: s.NPMUser,
|
||||
npmBin: s.NPMBin,
|
||||
proxyConfigDir: s.ProxyConfigDir,
|
||||
bundlePath: s.BundlePath,
|
||||
statePath: statePath,
|
||||
@@ -427,6 +402,8 @@ func resolveCAPaths(opts *caCommandOptions, proxy systemAccount) {
|
||||
}
|
||||
}
|
||||
|
||||
// ensureProxyStopped guards changes to the CA. The proxy reads the certificate
|
||||
// once at startup, so a running daemon would keep serving the old one.
|
||||
func ensureProxyStopped(path string) error {
|
||||
data, err := os.ReadFile(path)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
@@ -458,8 +435,8 @@ func printCAStatus(out io.Writer, status caStatus) error {
|
||||
}{
|
||||
{"Persistent CA", status.CAValid},
|
||||
{"Private key access", status.KeyModeValid},
|
||||
{"Public npm bundle", status.BundleValid},
|
||||
{"npm cafile", status.NPMConfigured},
|
||||
{"Public bundle", status.BundleValid},
|
||||
{"System trust store", status.SystemTrust},
|
||||
}
|
||||
for _, row := range rows {
|
||||
result := "FAIL"
|
||||
@@ -504,13 +481,6 @@ func lookupSystemAccount(name string) (systemAccount, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func defaultNPMUser() string {
|
||||
if name := os.Getenv("SUDO_USER"); name != "" && name != "root" {
|
||||
return name
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func requireRoot() error {
|
||||
if os.Geteuid() != 0 {
|
||||
return errors.New("run this command as root")
|
||||
@@ -524,10 +494,3 @@ func removeFile(path string) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func restoreNPM(npm npmTrust, account systemAccount, state *caState) error {
|
||||
if state.PreviousNPMSet {
|
||||
return npm.Set(account, state.PreviousNPMCA)
|
||||
}
|
||||
return npm.Remove(account)
|
||||
}
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
type systemAccount struct {
|
||||
Name string
|
||||
UID uint32
|
||||
GID uint32
|
||||
HomeDir string
|
||||
}
|
||||
|
||||
type npmTrust struct {
|
||||
binary string
|
||||
}
|
||||
|
||||
func (n npmTrust) Current(account systemAccount) (string, bool, error) {
|
||||
output, err := n.run(account, "config", "get", "cafile", "--location=user")
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
value := strings.TrimSpace(output)
|
||||
switch value {
|
||||
case "", "null", "undefined":
|
||||
return "", false, nil
|
||||
default:
|
||||
return value, true, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (n npmTrust) Set(account systemAccount, path string) error {
|
||||
_, err := n.run(account, "config", "set", "cafile="+path, "--location=user")
|
||||
return err
|
||||
}
|
||||
|
||||
func (n npmTrust) Remove(account systemAccount) error {
|
||||
_, err := n.run(account, "config", "delete", "cafile", "--location=user")
|
||||
return err
|
||||
}
|
||||
|
||||
func (n npmTrust) run(account systemAccount, args ...string) (string, error) {
|
||||
binary, err := exec.LookPath(n.binary)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("find npm executable %q: %w", n.binary, err)
|
||||
}
|
||||
|
||||
cmd := exec.Command(binary, args...)
|
||||
cmd.Env = accountEnvironment(account)
|
||||
cmd.Dir = account.HomeDir
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||
Credential: &syscall.Credential{
|
||||
Uid: account.UID,
|
||||
Gid: account.GID,
|
||||
},
|
||||
}
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("run npm as %s: %w: %s", account.Name, err, strings.TrimSpace(string(output)))
|
||||
}
|
||||
return string(output), nil
|
||||
}
|
||||
|
||||
func accountEnvironment(account systemAccount) []string {
|
||||
blocked := map[string]struct{}{
|
||||
"HOME": {},
|
||||
"USER": {},
|
||||
"LOGNAME": {},
|
||||
"XDG_CONFIG_HOME": {},
|
||||
"NPM_CONFIG_CAFILE": {},
|
||||
"npm_config_cafile": {},
|
||||
"NPM_CONFIG_USERCONFIG": {},
|
||||
"npm_config_userconfig": {},
|
||||
"NPM_CONFIG_GLOBALCONFIG": {},
|
||||
"npm_config_globalconfig": {},
|
||||
}
|
||||
|
||||
env := make([]string, 0, len(os.Environ())+4)
|
||||
for _, item := range os.Environ() {
|
||||
key, _, ok := strings.Cut(item, "=")
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if _, skip := blocked[key]; !skip {
|
||||
env = append(env, item)
|
||||
}
|
||||
}
|
||||
return append(env,
|
||||
"HOME="+account.HomeDir,
|
||||
"USER="+account.Name,
|
||||
"LOGNAME="+account.Name,
|
||||
"XDG_CONFIG_HOME="+filepath.Join(account.HomeDir, ".config"),
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user