package main import ( "context" "errors" "flag" "fmt" "io" "log" "net" "net/http" "os" "os/signal" "strconv" "strings" "syscall" "time" ) func envBoolValue(raw string, fallback bool) bool { if strings.TrimSpace(raw) == "" { return fallback } value, err := strconv.ParseBool(strings.TrimSpace(raw)) if err != nil { return fallback } return value } func validToken(token string) bool { return len(token) >= 24 && !strings.HasPrefix(token, "replace-with-") } func main() { if err := run(os.Args[1:], os.Stdout, os.Stderr); err != nil { log.Fatal(err) } } func run(args []string, stdout, stderr io.Writer) error { command := "serve" // Preserve the legacy invocation used by existing systemd units. if len(args) > 0 && !strings.HasPrefix(args[0], "-") { command, args = args[0], args[1:] } if command == "help" { printUsage(stdout) return nil } if command == "version" { fmt.Fprintln(stdout, version) return nil } envFile, err := requestedEnvFile(args) if err != nil { return err } fileValues, err := readEnvironmentFile(envFile) if err != nil { // PID 1 already loaded EnvironmentFile for the unprivileged daemon; // access to the on-disk secret is deliberately restricted to root. if !errors.Is(err, os.ErrPermission) || os.Getenv("RUSTPROXY_DOCKER_AGENT_TOKEN") == "" { return err } fileValues = map[string]string{} } env := func(name, fallback string) string { if v, ok := os.LookupEnv(name); ok { return strings.TrimSpace(v) } if v, ok := fileValues[name]; ok { return strings.TrimSpace(v) } return fallback } hostname, _ := os.Hostname() cfg := config{} fs := flag.NewFlagSet("rustproxy-docker-agent "+command, flag.ContinueOnError) fs.SetOutput(stderr) fs.StringVar(&envFile, "env-file", envFile, "systemd EnvironmentFile path") fs.StringVar(&cfg.Listen, "listen", env("RUSTPROXY_DOCKER_AGENT_LISTEN", "127.0.0.1:9876"), "listen address") // NEVER supply a token as a flag default: `--help` would leak it into output. tokenOverride := fs.String("token", "", "Bearer token override (prefer the protected environment file)") fs.StringVar(&cfg.DockerSock, "docker-socket", env("RUSTPROXY_DOCKER_SOCKET", "/var/run/docker.sock"), "Docker Engine unix socket") fs.StringVar(&cfg.AdvertiseIP, "advertise-ip", env("RUSTPROXY_DOCKER_ADVERTISE_IP", ""), "preferred host IP") fs.StringVar(&cfg.PublicIPURL, "public-ip-url", env("RUSTPROXY_DOCKER_PUBLIC_IP_URL", "https://api.ipify.org"), "public IP lookup URL") fs.StringVar(&cfg.Hostname, "hostname", env("RUSTPROXY_DOCKER_HOSTNAME", hostname), "reported host name") fs.BoolVar(&cfg.TLS, "tls", envBoolValue(env("RUSTPROXY_DOCKER_AGENT_TLS", "true"), true), "serve HTTPS") fs.StringVar(&cfg.CertFile, "cert-file", env("RUSTPROXY_DOCKER_AGENT_CERT_FILE", "/var/lib/rustproxy-docker-agent/agent.crt"), "TLS certificate file") fs.StringVar(&cfg.KeyFile, "key-file", env("RUSTPROXY_DOCKER_AGENT_KEY_FILE", "/var/lib/rustproxy-docker-agent/agent.key"), "TLS private key file") showToken := fs.Bool("show-token", false, "include the SECRET Bearer token in 'info' output") asJSON := fs.Bool("json", false, "print 'info' as JSON") initCert := fs.Bool("init-cert", false, "legacy alias for 'cert-init'") printFingerprint := fs.Bool("print-fingerprint", false, "legacy alias for 'print-fingerprint'") showVersion := fs.Bool("version", false, "print version") fs.Usage = func() { printUsage(stderr); fs.PrintDefaults() } if err := fs.Parse(args); err != nil { if errors.Is(err, flag.ErrHelp) { return nil } return err } if fs.NArg() != 0 { return fmt.Errorf("unexpected arguments: %s", strings.Join(fs.Args(), " ")) } if *showVersion { fmt.Fprintln(stdout, version) return nil } if *initCert { command = "cert-init" } if *printFingerprint { command = "print-fingerprint" } cfg.Token = env("RUSTPROXY_DOCKER_AGENT_TOKEN", "") if *tokenOverride != "" { cfg.Token = *tokenOverride } switch command { case "init": _, created, err := provisionToken(envFile, false) if err != nil { return err } if created { fmt.Fprintf(stdout, "Created a random Bearer token in %s\n", envFile) } // Reload because a new token was written after the initial environment read. fresh, err := readEnvironmentFile(envFile) if err != nil { return err } cfg.Token = fresh["RUSTPROXY_DOCKER_AGENT_TOKEN"] if cfg.TLS { fingerprint, created, err := ensureSelfSignedCertificate(cfg.CertFile, cfg.KeyFile, cfg.Hostname) if err != nil { return err } if created { if err := setServiceCertificateOwnership(cfg.CertFile, cfg.KeyFile); err != nil { return err } } fmt.Fprintf(stdout, "Certificate SHA-256: %s\n", fingerprint) } fmt.Fprintf(stdout, "Provisioning complete. Run 'sudo rustproxy-docker-agent info --show-token' to configure RustProxy.\n") return nil case "rotate-token": if _, _, err := provisionToken(envFile, true); err != nil { return err } fmt.Fprintf(stdout, "Rotated token in %s. Restart rustproxy-docker-agent.service to activate it and update RustProxy's agent configuration.\n", envFile) return nil case "show-token": if !validToken(cfg.Token) { return errors.New("no valid token configured; run 'sudo rustproxy-docker-agent init'") } fmt.Fprintln(stdout, cfg.Token) // Deliberate: an explicit administrative command. return nil case "info": return printAgentInfo(stdout, cfg, envFile, *showToken, *asJSON) case "cert-init": fingerprint, created, err := ensureSelfSignedCertificate(cfg.CertFile, cfg.KeyFile, cfg.Hostname) if err != nil { return err } if created { if err := setServiceCertificateOwnership(cfg.CertFile, cfg.KeyFile); err != nil { return err } } fmt.Fprintln(stdout, fingerprint) return nil case "print-fingerprint": fingerprint, err := certificateFingerprint(cfg.CertFile) if err != nil { return err } fmt.Fprintln(stdout, fingerprint) return nil case "serve": return serve(cfg) default: return fmt.Errorf("unknown command %q (use 'help')", command) } } func printUsage(w io.Writer) { fmt.Fprintln(w, "RustProxy Docker Agent commands:") fmt.Fprintln(w, " serve (default) Run the HTTPS Docker inventory server") fmt.Fprintln(w, " init Generate missing token and certificate, preserve existing secrets") fmt.Fprintln(w, " info [--show-token] Show endpoint, TLS certificate details and token status") fmt.Fprintln(w, " show-token Print the full Bearer token (sensitive)") fmt.Fprintln(w, " rotate-token Replace the Bearer token (service restart required)") fmt.Fprintln(w, " cert-init Create a missing TLS certificate and key") fmt.Fprintln(w, " print-fingerprint Print current TLS certificate SHA-256 fingerprint") fmt.Fprintln(w, " version Print version") fmt.Fprintln(w, " --env-file PATH Override /etc/rustproxy/docker-agent.env") fmt.Fprintln(w, " --help Show command flags") } func serve(cfg config) error { if !validToken(cfg.Token) { return errors.New("RUSTPROXY_DOCKER_AGENT_TOKEN must be a non-placeholder value with at least 24 characters; run 'sudo rustproxy-docker-agent init'") } if _, err := os.Stat(cfg.DockerSock); err != nil { return fmt.Errorf("Docker socket unavailable: %w", err) } if _, _, err := net.SplitHostPort(cfg.Listen); err != nil { return fmt.Errorf("invalid listen address: %w", err) } if cfg.TLS { fingerprint, created, err := ensureSelfSignedCertificate(cfg.CertFile, cfg.KeyFile, cfg.Hostname) if err != nil { return fmt.Errorf("TLS certificate: %w", err) } cfg.Fingerprint = fingerprint if created { log.Printf("generated self-signed TLS certificate; SHA-256 %s", fingerprint) } if details, err := describeCertificate(cfg.CertFile); err == nil { log.Printf("TLS certificate: subject=%s valid_until=%s SHA-256=%s", details.Subject, details.NotAfter, details.SHA256) } } server := &http.Server{Addr: cfg.Listen, Handler: handler(cfg), ReadHeaderTimeout: 3 * time.Second, IdleTimeout: 30 * time.Second, MaxHeaderBytes: 16 << 10} ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() go func() { <-ctx.Done() shutdown, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() _ = server.Shutdown(shutdown) }() scheme := "http" if cfg.TLS { scheme = "https" } log.Printf("RustProxy Docker Agent %s listening on %s://%s; Bearer token configured (%d chars, redacted)", version, scheme, cfg.Listen, len(cfg.Token)) var err error if cfg.TLS { err = server.ListenAndServeTLS(cfg.CertFile, cfg.KeyFile) } else { err = server.ListenAndServe() } if err != nil && !errors.Is(err, http.ErrServerClosed) { return err } return nil }