diff options
author | Jaime Martinez <jmartinez@gitlab.com> | 2021-10-08 02:29:35 +0300 |
---|---|---|
committer | Jaime Martinez <jmartinez@gitlab.com> | 2021-10-08 02:29:35 +0300 |
commit | 247bd7ba2fd9139711218c6a42ed03c551f958d9 (patch) | |
tree | 5f34b0cf1bf51aed35fe35d5396916e89eef5485 | |
parent | c48bb316739e39ba1b225f12147fad45337aa711 (diff) | |
parent | bf039d89071e527e2e5c1bc2e64c7513d87cba1a (diff) |
Merge branch 'remove/root' into 'master'
Remove gitlab-pages daemon
Closes #114 and #133
See merge request gitlab-org/gitlab-pages!542
-rw-r--r-- | .gitlab/ci/test.yml | 14 | ||||
-rw-r--r-- | README.md | 102 | ||||
-rw-r--r-- | daemon.go | 189 | ||||
-rw-r--r-- | doc/development.md | 2 | ||||
-rw-r--r-- | internal/config/config.go | 15 | ||||
-rw-r--r-- | internal/config/flags.go | 4 | ||||
-rw-r--r-- | main.go | 18 | ||||
-rw-r--r-- | test/acceptance/acceptance_test.go | 1 | ||||
-rw-r--r-- | test/acceptance/helpers_test.go | 23 |
9 files changed, 13 insertions, 355 deletions
diff --git a/.gitlab/ci/test.yml b/.gitlab/ci/test.yml index a29acb7d..ea7b6932 100644 --- a/.gitlab/ci/test.yml +++ b/.gitlab/ci/test.yml @@ -20,12 +20,6 @@ - echo "Running just the acceptance tests...." - make acceptance -.tests-acceptance-daemonize: - extends: .tests-common - script: - - echo "Running just the acceptance tests as daemon...." - - make acceptance ARGS='-daemonize' - test:1.16: extends: .tests-unit image: golang:1.16 @@ -34,10 +28,6 @@ test-acceptance:1.16: extends: .tests-acceptance image: golang:1.16 -test-acceptance-daemonize:1.16: - extends: .tests-acceptance-daemonize - image: golang:1.16 - test:1.17: extends: .tests-unit image: golang:1.17 @@ -46,10 +36,6 @@ test-acceptance:1.17: extends: .tests-acceptance image: golang:1.17 -test-acceptance-daemonize:1.17: - extends: .tests-acceptance-daemonize - image: golang:1.17 - race: extends: .tests-common script: @@ -1,4 +1,4 @@ -## GitLab Pages Daemon +## GitLab Pages [![build status](https://gitlab.com/gitlab-org/gitlab-pages/badges/master/pipeline.svg)](https://gitlab.com/gitlab-org/gitlab-pages/commits/master) [![coverage report](https://gitlab.com/gitlab-org/gitlab-pages/badges/master/coverage.svg)](https://gitlab.com/gitlab-org/gitlab-pages/commits/master) @@ -21,16 +21,16 @@ current requests. ### How it serves content -1. When client initiates the TLS connection, the GitLab-Pages daemon looks in +1. When a client initiates the TLS connection, GitLab Pages looks in the generated configuration for virtual hosts. If present, it uses the TLS key and certificate in `config.json`, otherwise it falls back to the global configuration. -2. When client connects to an HTTP port, the GitLab-Pages daemon looks in the +1. When a client connects to an HTTP port, GitLab Pages looks in the generated configuration for a matching virtual host. -3. The URL.Path is split into `/<project>/<subpath>` and the daemon tries to +1. The URL.Path is split into `/<project>/<subpath>` and Pages tries to load: `pages-root/group/project/public/subpath`. -4. If the file is not found, it will try to load `pages-root/group/<host>/public/<URL.Path>`. -5. If requested path is a directory, the `index.html` will be served. +1. If the file is not found, it will try to load `pages-root/group/<host>/public/<URL.Path>`. +1. If requested path is a directory, the `index.html` file is served. 6. If `.../path.gz` exists, it will be served instead of the main file, with a `Content-Encoding: gzip` header. This allows compressed versions of the files to be precalculated, saving CPU time and network bandwidth. @@ -80,89 +80,6 @@ $ ./gitlab-pages -listen-https ":9090" -root-cert=path/to/example.com.crt -root- See [doc/development.md](doc/development.md) - -### Run daemon **in secure mode** - -**Update**: - -Starting from GitLab 14.1 the -[jailing/chroot mechanism is disabled by default](https://docs.gitlab.com/ee/administration/pages/#jailing-mechanism-disabled-by-default-for-api-based-configuration). - -When compiled with `CGO_ENABLED=0` (which is the default), `gitlab-pages` is a -static binary and so can be run in chroot with dropped privileges. - -To enter this mode, run `gitlab-pages` as the root user and pass it the -`-daemon-uid` and `-daemon-gid` arguments to specify the user you want it to run -as. - -The daemon starts listening on ports and reads certificates as root, then -re-executes itself as the specified user. When re-executing it creates a chroot jail -containing a copy of its own binary, `/etc/hosts`, `/etc/nsswitch.conf`, `/etc/resolv.conf`, and a bind mount of `pages-root`. - -When `-artifacts-server` points to an HTTPS URL we also need a list of certificates for -the trusted Certification Authorities to copy inside the jail. -A file containing such list can be specified using `SSL_CERT_FILE` environment variable. -(`SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt` on Debian) - -This makes it possible to listen on privileged ports and makes it harder for the -process to read files outside of `pages-root`. - -Example: -``` -$ make -$ sudo ./gitlab-pages -listen-http ":80" -pages-root path/to/gitlab/shared/pages -pages-domain example.com -daemon-uid 1000 -daemon-gid 1000 -``` - -#### Caveats - -The `/etc/hosts`, `/etc/resolv.conf` and `/etc/nsswitch.conf` files, and any file pointed to by the `SSL_CERT_FILE` -environment variable, will be copied into the jail. As a result, changes to -these files will not be reflected in Pages until it's restarted. - -Bind mounts are unavailable on a range of non-Linux systems. Some of these -systems (e.g., BSD) offer native "jail" functionality. It is recommended to set -up an externally-managed jail and run the Pages daemon within it as an ordinary -user if available. - -A less-functional (but just as secure) operation mode is provided via the -`-daemon-inplace-chroot` command-line option. If passed, Pages will daemonize -as usual, but chroot directly to the `-pages-root` directory instead of building -a complete jail in the system temporary directory. There are some known issues -with this mode, such as: - -* Pages service will not be able to resolve the domain name of the auth server and the artifacts server due to missing `/etc/resolv.conf` at the chroot directory. As a workaround, you can manually copy the file to the pages root directory, however, it might cause a conflict with an existing pages data. As a result of DNS not working: - * [GitLab access control](#gitlab-access-control) might not work - * [Online view of HTML artifacts](https://about.gitlab.com/2017/10/22/gitlab-10-1-released/#online-view-of-html-artifacts) may not work. You can disable it and fall back to downloading artifacts by setting `artifacts_server` to `false` in `gitlab.yml` for your GitLab instance: - ```yml - ## GitLab Pages - pages: - enabled: true - artifacts_server: false - ``` -* TLS operation (on some systems) will not work - -The default secure mode will also fail for certain Linux-based configurations. -Known cases include: - -* The Pages daemon is running inside an unprivileged container - * Bind mount functionality requires the `CAP_SYS_ADMIN` privilege - * This is only available to containers run in privileged mode -* The system temporary directory is mounted `noexec` or `nodev` - * The jail is created in `$TMPDIR`. - * Character device files are created within the jail - * A copy of the gitlab-pages executable is run from within the bind mount -* AppArmor/SELinux is enabled - * These systems disallow bind-mounting in certain configurations - -In these cases, workarounds are similar to those documented for non-Linux -systems - use an external jailing technology, or fall back to the pre-v0.8.0 -behaviour using `-daemon-inplace-chroot`. - -On Linux, Docker and other containerization systems can be used to build a jail -within which the Pages daemon can safely run with secure mode disabled. However, -this configuration **is not secure** if simply using the default -`gitlab/gitlab-ce` and `gitlab-gitlab-ee` Docker containers! - ### Listen on multiple ports Each of the `listen-http`, `listen-https` and `listen-proxy` arguments can be @@ -224,8 +141,6 @@ $ make $ ./gitlab-pages -listen-http "10.0.0.1:8080" -listen-https "[fd00::1]:8080" -pages-root path/to/gitlab/shared/pages -pages-domain example.com -auth-client-id <id> -auth-client-secret <secret> -auth-redirect-uri https://projects.example.com/auth -auth-secret something-very-secret -auth-server https://gitlab.com ``` -> NOTE: GitLab access control might not work with `-daemon-inplace-chroot` option. Please take a look at [the caveat section](#caveats) above. - #### How it works 1. GitLab pages looks for `access_control` and `id` fields in `config.json` files @@ -260,8 +175,7 @@ with tools such as [ELK](https://www.elastic.co/elk-stack). ### Cross-origin requests GitLab Pages defaults to allowing cross-origin requests for any resource it -serves. This can be disabled globally by passing `-disable-cross-origin-requests` -when starting the daemon. +serves. This can be disabled globally by passing `-disable-cross-origin-requests`. Having cross-origin requests enabled allows third-party websites to make use of files stored on the Pages server, which allows various third-party integrations @@ -289,7 +203,7 @@ Example: ### Configuration -The daemon can be configured with any combination of these methods: +Gitlab Pages can be configured with any combination of these methods: 1. Command-line options 1. Environment variables 1. Configuration file diff --git a/daemon.go b/daemon.go deleted file mode 100644 index 86ede6be..00000000 --- a/daemon.go +++ /dev/null @@ -1,189 +0,0 @@ -package main - -import ( - "encoding/json" - "os" - "os/exec" - "os/signal" - "path/filepath" - "strings" - "syscall" - - "github.com/sirupsen/logrus" - - "gitlab.com/gitlab-org/gitlab-pages/internal/config" -) - -const ( - daemonRunProgram = "gitlab-pages-unprivileged" -) - -func daemonMain() { - if os.Args[0] != daemonRunProgram { - return - } - - // Validate that a working directory is valid - // https://man7.org/linux/man-pages/man2/getcwd.2.html - wd, err := os.Getwd() - if err != nil { - fatal(err, "could not get current working directory") - } else if strings.HasPrefix(wd, "(unreachable)") { - fatal(os.ErrPermission, "could not get current working directory") - } - - logrus.WithFields(logrus.Fields{ - "uid": syscall.Getuid(), - "gid": syscall.Getgid(), - "wd": wd, - }).Info("starting the daemon as unprivileged user") - - // read the configuration from the pipe "ExtraFiles" - var config config.Config - if err := json.NewDecoder(os.NewFile(3, "options")).Decode(&config); err != nil { - fatal(err, "could not decode app config") - } - runApp(&config) - os.Exit(0) -} - -func daemonReexec(uid, gid uint, args ...string) (cmd *exec.Cmd, err error) { - path, err := os.Executable() - if err != nil { - return - } - - cmd = &exec.Cmd{ - Path: path, - Args: args, - Env: os.Environ(), - Stdin: os.Stdin, - Stdout: os.Stdout, - Stderr: os.Stderr, - SysProcAttr: &syscall.SysProcAttr{ - Credential: &syscall.Credential{ - Uid: uint32(uid), - Gid: uint32(gid), - }, - Setsid: true, - }, - } - return -} - -func daemonUpdateFd(cmd *exec.Cmd, fd uintptr) (childFd uintptr) { - file := os.NewFile(fd, "[socket]") - - // we add 3 since, we have a 3 predefined FDs - childFd = uintptr(3 + len(cmd.ExtraFiles)) - cmd.ExtraFiles = append(cmd.ExtraFiles, file) - - return -} - -func daemonUpdateFds(cmd *exec.Cmd, fds []uintptr) { - for idx, fd := range fds { - fds[idx] = daemonUpdateFd(cmd, fd) - } -} - -func killProcess(cmd *exec.Cmd) { - if cmd.Process != nil { - cmd.Process.Kill() - } - cmd.Wait() - for _, file := range cmd.ExtraFiles { - file.Close() - } -} - -func passSignals(cmd *exec.Cmd) { - if cmd.Process == nil { - return - } - - s := make(chan os.Signal, 1) - signal.Notify(s, syscall.SIGTERM, os.Interrupt, os.Kill) - - go func() { - for cmd.Process != nil { - cmd.Process.Signal(<-s) - } - }() -} - -func daemonize(config *config.Config) error { - uid := config.Daemon.UID - gid := config.Daemon.GID - pagesRoot := config.General.RootDir - - // Ensure pagesRoot is an absolute path. This will produce a different path - // if any component of pagesRoot is a symlink (not likely). For example, - // -pages-root=/some-path where ln -s /other-path /some-path - // pagesPath will become: /other-path and we will fail to serve files from /some-path. - // GitLab Rails also resolves the absolute path for `pages_path` - // https://gitlab.com/gitlab-org/gitlab/blob/981ad651d8bd3690e28583eec2363a79f775af89/config/initializers/1_settings.rb#L296 - pagesRoot, err := filepath.Abs(pagesRoot) - if err != nil { - return err - } - - logrus.WithFields(logrus.Fields{ - "uid": uid, - "gid": gid, - "pages-root": pagesRoot, - }).Info("running the daemon as unprivileged user") - - cmd, err := daemonReexec(uid, gid, daemonRunProgram) - if err != nil { - return err - } - defer killProcess(cmd) - - // Create a pipe to pass the configuration - configReader, configWriter, err := os.Pipe() - if err != nil { - return err - } - defer configWriter.Close() - cmd.ExtraFiles = append(cmd.ExtraFiles, configReader) - - updateFds(config, cmd) - - // Start the process - if err := cmd.Start(); err != nil { - logrus.WithError(err).Error("start failed") - return err - } - - // Write the configuration - if err := json.NewEncoder(configWriter).Encode(config); err != nil { - return err - } - configWriter.Close() - - // Pass through signals - passSignals(cmd) - - // Wait for process to exit - return cmd.Wait() -} - -func updateFds(config *config.Config, cmd *exec.Cmd) { - for _, fds := range [][]uintptr{ - config.Listeners.HTTP, - config.Listeners.HTTPS, - config.Listeners.Proxy, - config.Listeners.HTTPSProxyv2, - } { - daemonUpdateFds(cmd, fds) - } - - for _, fdPtr := range []*uintptr{ - &config.ListenMetrics, - } { - if *fdPtr != 0 { - *fdPtr = daemonUpdateFd(cmd, *fdPtr) - } - } -} diff --git a/doc/development.md b/doc/development.md index d3de429f..91be0e33 100644 --- a/doc/development.md +++ b/doc/development.md @@ -171,7 +171,7 @@ This is an example of developing GitLab Pages inside the [GitLab Development Kit # Inside $GDK_ROOT/gitlab-pages $ gdk stop gitlab-pages $ make # calls go build in this project and creates a `gitlab-pages` binary under bin/ - # start daemon manually with a config + # start Pages manually with a config $ ./bin/gitlab-pages -config gitlab-pages.conf ``` diff --git a/internal/config/config.go b/internal/config/config.go index 61767a57..94c22328 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -18,7 +18,6 @@ type Config struct { General General ArtifactsServer ArtifactsServer Authentication Auth - Daemon Daemon GitLab GitLab Listeners Listeners Log Log @@ -80,12 +79,6 @@ type Auth struct { Scope string } -// Daemon groups settings related to configuring GitLab Pages daemon -type Daemon struct { - UID uint - GID uint -} - // Cache configuration for GitLab API type Cache struct { CacheExpiry time.Duration @@ -215,10 +208,6 @@ func loadConfig() (*Config, error) { RedirectURI: *redirectURI, Scope: *authScope, }, - Daemon: Daemon{ - UID: *daemonUID, - GID: *daemonGID, - }, Log: Log{ Format: *logFormat, Verbose: *logVerbose, @@ -281,8 +270,6 @@ func LogConfig(config *Config) { log.WithFields(log.Fields{ "artifacts-server": *artifactsServer, "artifacts-server-timeout": *artifactsServerTimeout, - "daemon-gid": *daemonGID, - "daemon-uid": *daemonUID, "default-config-filename": flag.DefaultConfigFlagname, "disable-cross-origin-requests": *disableCrossOriginRequests, "domain": config.General.Domain, @@ -313,7 +300,7 @@ func LogConfig(config *Config) { "zip-cache-cleanup": config.Zip.CleanupInterval, "zip-cache-refresh": config.Zip.RefreshInterval, "zip-open-timeout": config.Zip.OpenTimeout, - }).Debug("Start daemon with configuration") + }).Debug("Start Pages with configuration") } // LoadConfig parses configuration settings passed as command line arguments or diff --git a/internal/config/flags.go b/internal/config/flags.go index aa5bf1c5..52b7be18 100644 --- a/internal/config/flags.go +++ b/internal/config/flags.go @@ -21,8 +21,8 @@ var ( metricsAddress = flag.String("metrics-address", "", "The address to listen on for metrics requests") sentryDSN = flag.String("sentry-dsn", "", "The address for sending sentry crash reporting to") sentryEnvironment = flag.String("sentry-environment", "", "The environment for sentry crash reporting") - daemonUID = flag.Uint("daemon-uid", 0, "Drop privileges to this user") - daemonGID = flag.Uint("daemon-gid", 0, "Drop privileges to this group") + _ = flag.Uint("daemon-uid", 0, "DEPRECATED and ignored, will be removed in 15.0") + _ = flag.Uint("daemon-gid", 0, "DEPRECATED and ignored, will be removed in 15.0") _ = flag.Bool("daemon-enable-jail", false, "DEPRECATED and ignored, will be removed in 15.0") _ = flag.Bool("daemon-inplace-chroot", false, "DEPRECATED and ignored, will be removed in 15.0") // TODO: https://gitlab.com/gitlab-org/gitlab-pages/-/issues/599 propagateCorrelationID = flag.Bool("propagate-correlation-id", false, "Reuse existing Correlation-ID from the incoming request header `X-Request-ID` if present") @@ -5,7 +5,6 @@ import ( "io" "math/rand" "os" - "strings" "time" "github.com/sirupsen/logrus" @@ -71,7 +70,7 @@ func appMain() { log.WithFields(log.Fields{ "version": VERSION, "revision": REVISION, - }).Info("GitLab Pages Daemon") + }).Info("GitLab Pages") log.Info("URL: https://gitlab.com/gitlab-org/gitlab-pages") if err := os.Chdir(config.General.RootDir); err != nil { @@ -85,20 +84,6 @@ func appMain() { defer closeAll(cs) } - if config.Daemon.UID != 0 || config.Daemon.GID != 0 { - if err := daemonize(config); err != nil { - if strings.Contains(err.Error(), "signal:") { - log.WithField("signal", err.Error()).Info("daemon received signal") - return - } - - errortracking.Capture(err) - fatal(err, "could not create pages daemon") - } - - return - } - runApp(config) } @@ -205,6 +190,5 @@ func main() { metrics.MustRegister() - daemonMain() appMain() } diff --git a/test/acceptance/acceptance_test.go b/test/acceptance/acceptance_test.go index b36bd63d..3b41eaee 100644 --- a/test/acceptance/acceptance_test.go +++ b/test/acceptance/acceptance_test.go @@ -18,7 +18,6 @@ const ( var ( pagesBinary = flag.String("gitlab-pages-binary", "../../gitlab-pages", "Path to the gitlab-pages binary") - daemonize = flag.Bool("daemonize", false, "run tests as daemon") httpPort = "36000" httpsPort = "37000" diff --git a/test/acceptance/helpers_test.go b/test/acceptance/helpers_test.go index d9f2f14c..e2e1c1d0 100644 --- a/test/acceptance/helpers_test.go +++ b/test/acceptance/helpers_test.go @@ -352,7 +352,6 @@ func getPagesArgs(t *testing.T, listeners []ListenSpec, promPort string, extraAr args = append(args, "-metrics-address", promPort) } - args = append(args, getPagesDaemonArgs(t)...) args = append(args, extraArgs...) return @@ -367,28 +366,6 @@ func contains(slice []string, s string) bool { return false } -func getPagesDaemonArgs(t *testing.T) []string { - if !(*daemonize) { - return nil - } - - if os.Geteuid() != 0 { - t.Log("Privilege-dropping requested but not running as root!") - t.FailNow() - return nil - } - - out := []string{} - - t.Log("Running pages as a daemon") - - // This triggers the drop-privileges-and-chroot code in the pages daemon - out = append(out, "-daemon-uid", "0") - out = append(out, "-daemon-gid", "65534") - - return out -} - // Does a HTTP(S) GET against the listener specified, setting a fake // Host: and constructing the URL from the listener and the URL suffix. func GetPageFromListener(t *testing.T, spec ListenSpec, host, urlsuffix string) (*http.Response, error) { |