0.34.2_agent
This commit is contained in:
1 parent
b2b5b45d8a
commit
8c0de520d7
13 files changed
+820
-123
No files matched your search
+180
-47
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -16,80 +17,214 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
func envOr(name, fallback string) string {
|
||||
if value := strings.TrimSpace(os.Getenv(name)); value != "" {
|
||||
return value
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
func envBool(name string, fallback bool) bool {
|
||||
raw := strings.TrimSpace(os.Getenv(name))
|
||||
if raw == "" {
|
||||
func envBoolValue(raw string, fallback bool) bool {
|
||||
if strings.TrimSpace(raw) == "" {
|
||||
return fallback
|
||||
}
|
||||
value, err := strconv.ParseBool(raw)
|
||||
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{}
|
||||
flag.StringVar(&cfg.Listen, "listen", envOr("RUSTPROXY_DOCKER_AGENT_LISTEN", "127.0.0.1:9876"), "listen address")
|
||||
flag.StringVar(&cfg.Token, "token", envOr("RUSTPROXY_DOCKER_AGENT_TOKEN", ""), "Bearer token (prefer environment variable)")
|
||||
flag.StringVar(&cfg.DockerSock, "docker-socket", envOr("RUSTPROXY_DOCKER_SOCKET", "/var/run/docker.sock"), "Docker Engine unix socket")
|
||||
flag.StringVar(&cfg.AdvertiseIP, "advertise-ip", envOr("RUSTPROXY_DOCKER_ADVERTISE_IP", ""), "optional preferred host IP; all usable host addresses are still reported")
|
||||
flag.StringVar(&cfg.PublicIPURL, "public-ip-url", envOr("RUSTPROXY_DOCKER_PUBLIC_IP_URL", "https://api.ipify.org"), "public IP lookup URL; empty disables")
|
||||
flag.StringVar(&cfg.Hostname, "hostname", envOr("RUSTPROXY_DOCKER_HOSTNAME", hostname), "reported host name")
|
||||
flag.BoolVar(&cfg.TLS, "tls", envBool("RUSTPROXY_DOCKER_AGENT_TLS", true), "serve HTTPS; enabled by default")
|
||||
flag.StringVar(&cfg.CertFile, "cert-file", envOr("RUSTPROXY_DOCKER_AGENT_CERT_FILE", "/var/lib/rustproxy-docker-agent/agent.crt"), "TLS certificate file")
|
||||
flag.StringVar(&cfg.KeyFile, "key-file", envOr("RUSTPROXY_DOCKER_AGENT_KEY_FILE", "/var/lib/rustproxy-docker-agent/agent.key"), "TLS private key file")
|
||||
initCert := flag.Bool("init-cert", false, "create the self-signed TLS certificate if missing and exit")
|
||||
printFingerprint := flag.Bool("print-fingerprint", false, "print SHA-256 certificate fingerprint and exit")
|
||||
flag.Parse()
|
||||
|
||||
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 {
|
||||
fingerprint, created, err := ensureSelfSignedCertificate(cfg.CertFile, cfg.KeyFile, cfg.Hostname)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Printf("%s\n", fingerprint)
|
||||
if created {
|
||||
log.Printf("created self-signed certificate %s", cfg.CertFile)
|
||||
}
|
||||
return
|
||||
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 {
|
||||
log.Fatal(err)
|
||||
return err
|
||||
}
|
||||
fmt.Printf("%s\n", fingerprint)
|
||||
return
|
||||
fmt.Fprintln(stdout, fingerprint)
|
||||
return nil
|
||||
case "serve":
|
||||
return serve(cfg)
|
||||
default:
|
||||
return fmt.Errorf("unknown command %q (use 'help')", command)
|
||||
}
|
||||
if len(cfg.Token) < 24 {
|
||||
log.Fatal("RUSTPROXY_DOCKER_AGENT_TOKEN must contain at least 24 characters")
|
||||
}
|
||||
|
||||
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 {
|
||||
log.Fatalf("Docker socket unavailable: %v", err)
|
||||
return fmt.Errorf("Docker socket unavailable: %w", err)
|
||||
}
|
||||
if _, _, err := net.SplitHostPort(cfg.Listen); err != nil {
|
||||
log.Fatalf("invalid listen address: %v", err)
|
||||
return fmt.Errorf("invalid listen address: %w", err)
|
||||
}
|
||||
if cfg.TLS {
|
||||
fingerprint, created, err := ensureSelfSignedCertificate(cfg.CertFile, cfg.KeyFile, cfg.Hostname)
|
||||
if err != nil {
|
||||
log.Fatalf("TLS certificate: %v", err)
|
||||
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()
|
||||
@@ -103,10 +238,7 @@ func main() {
|
||||
if cfg.TLS {
|
||||
scheme = "https"
|
||||
}
|
||||
log.Printf("RustProxy Docker Agent %s listening on %s://%s", version, scheme, cfg.Listen)
|
||||
if cfg.Fingerprint != "" {
|
||||
log.Printf("TLS certificate SHA-256: %s", cfg.Fingerprint)
|
||||
}
|
||||
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)
|
||||
@@ -114,6 +246,7 @@ func main() {
|
||||
err = server.ListenAndServe()
|
||||
}
|
||||
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
log.Fatal(err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in new issue
Block a user