diff --git a/assets/install-scripts/install.sh b/assets/install-scripts/install.sh deleted file mode 100755 index 949bb1bcc4d8f..0000000000000 --- a/assets/install-scripts/install.sh +++ /dev/null @@ -1,430 +0,0 @@ -#!/bin/bash -# Copyright 2022 Gravitational, Inc - -# This script detects the current Linux distribution and installs Teleport -# through its package manager, if supported, or downloading a tarball otherwise. -# We'll download Teleport from the official website and checksum it to make sure it was properly -# downloaded before executing. - -# The script is wrapped inside a function to protect against the connection being interrupted -# in the middle of the stream. - -# For more download options, head to https://goteleport.com/download/ - -set -euo pipefail - -# download uses curl or wget to download a teleport binary -download() { - URL=$1 - TMP_PATH=$2 - - echo "Downloading $URL" - if type curl &>/dev/null; then - set -x - # shellcheck disable=SC2086 - $SUDO $CURL -o "$TMP_PATH" "$URL" - else - set -x - # shellcheck disable=SC2086 - $SUDO $CURL -O "$TMP_PATH" "$URL" - fi - set +x -} - -install_via_apt_get() { - echo "Installing Teleport v$TELEPORT_VERSION via apt-get" - add_apt_key - set -x - $SUDO apt-get install -y "teleport$TELEPORT_SUFFIX=$TELEPORT_VERSION" - set +x - if [ "$TELEPORT_EDITION" = "cloud" ]; then - set -x - $SUDO apt-get install -y teleport-ent-updater - set +x - fi -} - -add_apt_key() { - APT_REPO_ID=$ID - APT_REPO_VERSION_CODENAME=$VERSION_CODENAME - IS_LEGACY=0 - - # check if we must use legacy .asc key - case "$ID" in - ubuntu | pop | neon | zorin) - if ! expr "$VERSION_ID" : "2.*" >/dev/null; then - IS_LEGACY=1 - fi - ;; - debian | raspbian) - if [ "$VERSION_ID" -lt 11 ]; then - IS_LEGACY=1 - fi - ;; - linuxmint | parrot) - if [ "$VERSION_ID" -lt 5 ]; then - IS_LEGACY=1 - fi - ;; - elementary) - if [ "$VERSION_ID" -lt 6 ]; then - IS_LEGACY=1 - fi - ;; - kali) - YEAR="$(echo "$VERSION_ID" | cut -f1 -d.)" - if [ "$YEAR" -lt 2021 ]; then - IS_LEGACY=1 - fi - ;; - esac - - if [[ "$IS_LEGACY" == 0 ]]; then - # set APT_REPO_ID if necessary - case "$ID" in - linuxmint | kali | elementary | pop | raspbian | neon | zorin | parrot) - APT_REPO_ID=$ID_LIKE - ;; - esac - - # set APT_REPO_VERSION_CODENAME if necessary - case "$ID" in - linuxmint | elementary | pop | neon | zorin) - APT_REPO_VERSION_CODENAME=$UBUNTU_CODENAME - ;; - kali) - APT_REPO_VERSION_CODENAME="bullseye" - ;; - parrot) - APT_REPO_VERSION_CODENAME="buster" - ;; - esac - fi - - echo "Downloading Teleport's PGP public key..." - TEMP_DIR=$(mktemp -d -t teleport-XXXXXXXXXX) - MAJOR=$(echo "$TELEPORT_VERSION" | cut -f1 -d.) - TELEPORT_REPO="" - - CHANNEL="stable/v${MAJOR}" - if [ "$TELEPORT_EDITION" = "cloud" ]; then - CHANNEL="stable/cloud" - fi - - if [[ "$IS_LEGACY" == 1 ]]; then - if ! type gpg >/dev/null; then - echo "Installing gnupg" - set -x - $SUDO apt-get update - $SUDO apt-get install -y gnupg - set +x - fi - TMP_KEY="$TEMP_DIR/teleport-pubkey.asc" - download "https://deb.releases.teleport.dev/teleport-pubkey.asc" "$TMP_KEY" - set -x - $SUDO apt-key add "$TMP_KEY" - set +x - TELEPORT_REPO="deb https://apt.releases.teleport.dev/${APT_REPO_ID?} ${APT_REPO_VERSION_CODENAME?} ${CHANNEL}" - else - TMP_KEY="$TEMP_DIR/teleport-pubkey.gpg" - download "https://apt.releases.teleport.dev/gpg" "$TMP_KEY" - set -x - $SUDO mkdir -p /etc/apt/keyrings - $SUDO cp "$TMP_KEY" /etc/apt/keyrings/teleport-archive-keyring.asc - set +x - TELEPORT_REPO="deb [signed-by=/etc/apt/keyrings/teleport-archive-keyring.asc] https://apt.releases.teleport.dev/${APT_REPO_ID?} ${APT_REPO_VERSION_CODENAME?} ${CHANNEL}" - fi - - set -x - echo "$TELEPORT_REPO" | $SUDO tee /etc/apt/sources.list.d/teleport.list >/dev/null - set +x - - set -x - $SUDO apt-get update - set +x -} - -# $1 is the value of the $ID path segment in the YUM repo URL. In -# /etc/os-release, this is either the value of $ID or $ID_LIKE. -install_via_yum() { - # shellcheck source=/dev/null - source /etc/os-release - - # Get the major version from the version ID. - VERSION_ID=$(echo "$VERSION_ID" | grep -Eo "^[0-9]+") - TELEPORT_MAJOR_VERSION="v$(echo "$TELEPORT_VERSION" | grep -Eo "^[0-9]+")" - - CHANNEL="stable/${TELEPORT_MAJOR_VERSION}" - if [ "$TELEPORT_EDITION" = "cloud" ]; then - CHANNEL="stable/cloud" - fi - - if type dnf &>/dev/null; then - echo "Installing Teleport v$TELEPORT_VERSION through dnf" - $SUDO dnf install -y 'dnf-command(config-manager)' - $SUDO dnf config-manager --add-repo "$(rpm --eval "https://yum.releases.teleport.dev/$1/$VERSION_ID/Teleport/%{_arch}/$CHANNEL/teleport-yum.repo")" - $SUDO dnf install -y "teleport$TELEPORT_SUFFIX-$TELEPORT_VERSION" - - if [ "$TELEPORT_EDITION" = "cloud" ]; then - $SUDO dnf install -y teleport-ent-updater - fi - - else - echo "Installing Teleport v$TELEPORT_VERSION through yum" - $SUDO yum install -y yum-utils - $SUDO yum-config-manager --add-repo "$(rpm --eval "https://yum.releases.teleport.dev/$1/$VERSION_ID/Teleport/%{_arch}/$CHANNEL/teleport-yum.repo")" - $SUDO yum install -y "teleport$TELEPORT_SUFFIX-$TELEPORT_VERSION" - - if [ "$TELEPORT_EDITION" = "cloud" ]; then - $SUDO yum install -y teleport-ent-updater - fi - fi - set +x -} - -install_via_zypper() { - # shellcheck source=/dev/null - source /etc/os-release - - # Get the major version from the version ID. - VERSION_ID=$(echo "$VERSION_ID" | grep -Eo "^[0-9]+") - TELEPORT_MAJOR_VERSION="v$(echo "$TELEPORT_VERSION" | grep -Eo "^[0-9]+")" - - CHANNEL="stable/${TELEPORT_MAJOR_VERSION}" - if [ "$TELEPORT_EDITION" = "cloud" ]; then - CHANNEL="stable/cloud" - fi - - $SUDO rpm --import https://zypper.releases.teleport.dev/gpg - $SUDO zypper addrepo --refresh --repo "$(rpm --eval "https://zypper.releases.teleport.dev/$ID/$VERSION_ID/Teleport/%{_arch}/$CHANNEL/teleport-zypper.repo")" - $SUDO zypper --gpg-auto-import-keys refresh teleport - $SUDO zypper install -y "teleport$TELEPORT_SUFFIX" - - if [ "$TELEPORT_EDITION" = "cloud" ]; then - $SUDO zypper install -y teleport-ent-updater - fi - - set +x -} - - -# download .tar.gz file via curl/wget, unzip it and run the install script -install_via_curl() { - TEMP_DIR=$(mktemp -d -t teleport-XXXXXXXXXX) - - TELEPORT_FILENAME="teleport$TELEPORT_SUFFIX-v$TELEPORT_VERSION-linux-$ARCH-bin.tar.gz" - URL="https://cdn.teleport.dev/${TELEPORT_FILENAME}" - download "${URL}" "${TEMP_DIR}/${TELEPORT_FILENAME}" - - TMP_CHECKSUM="${TEMP_DIR}/${TELEPORT_FILENAME}.sha256" - download "${URL}.sha256" "$TMP_CHECKSUM" - - set -x - cd "$TEMP_DIR" - # shellcheck disable=SC2086 - $SUDO $SHA_COMMAND -c "$TMP_CHECKSUM" - cd - - - $SUDO tar -xzf "${TEMP_DIR}/${TELEPORT_FILENAME}" -C "$TEMP_DIR" - $SUDO "$TEMP_DIR/teleport/install" - set +x -} - -# wrap script in a function so a partially downloaded script -# doesn't execute -install_teleport() { - # exit if not on Linux - if [[ $(uname) != "Linux" ]]; then - echo "ERROR: This script works only for Linux. Please go to the downloads page to find the proper installation method for your operating system:" - echo "https://goteleport.com/download/" - exit 1 - fi - - KERNEL_VERSION=$(uname -r) - MIN_VERSION="2.6.23" - if [ $MIN_VERSION != "$(echo -e "$MIN_VERSION\n$KERNEL_VERSION" | sort -V | head -n1)" ]; then - echo "ERROR: Teleport requires Linux kernel version $MIN_VERSION+" - exit 1 - fi - - # check if can run as admin either by running as root or by - # having 'sudo' or 'doas' installed - IS_ROOT="" - SUDO="" - if [ "$(id -u)" = 0 ]; then - # running as root, no need for sudo/doas - IS_ROOT="YES" - SUDO="" - elif type sudo &>/dev/null; then - SUDO="sudo" - elif type doas &>/dev/null; then - SUDO="doas" - fi - - if [ -z "$SUDO" ] && [ -z "$IS_ROOT" ]; then - echo "ERROR: The installer requires a way to run commands as root." - echo "Either run this script as root or install sudo/doas." - exit 1 - fi - - # require curl/wget - CURL="" - if type curl &>/dev/null; then - CURL="curl -fL" - elif type wget &>/dev/null; then - CURL="wget" - fi - if [ -z "$CURL" ]; then - echo "ERROR: This script requires either curl or wget in order to download files. Please install one of them and try again." - exit 1 - fi - - # require shasum/sha256sum - SHA_COMMAND="" - if type shasum &>/dev/null; then - SHA_COMMAND="shasum -a 256" - elif type sha256sum &>/dev/null; then - SHA_COMMAND="sha256sum" - else - echo "ERROR: This script requires sha256sum or shasum to validate the download. Please install it and try again." - exit 1 - fi - - # detect distro - OS_RELEASE=/etc/os-release - ID="" - ID_LIKE="" - VERSION_CODENAME="" - UBUNTU_CODENAME="" - if [[ -f "$OS_RELEASE" ]]; then - # shellcheck source=/dev/null - . $OS_RELEASE - fi - # Some $ID_LIKE values include multiple distro names in an arbitrary order, so - # evaluate the first one. - ID_LIKE="${ID_LIKE%% *}" - - # detect architecture - ARCH="" - case $(uname -m) in - x86_64) - ARCH="amd64" - ;; - i386) - ARCH="386" - ;; - armv7l) - ARCH="arm" - ;; - aarch64) - ARCH="arm64" - ;; - **) - echo "ERROR: Your system's architecture isn't officially supported or couldn't be determined." - echo "Please refer to the installation guide for more information:" - echo "https://goteleport.com/docs/installation/" - exit 1 - ;; - esac - - # select install method based on distribution - # if ID is debian derivate, run apt-get - case "$ID" in - debian | ubuntu | kali | linuxmint | pop | raspbian | neon | zorin | parrot | elementary) - install_via_apt_get - ;; - # if ID is amazon Linux 2/RHEL/etc, run yum - centos | rhel | amzn) - install_via_yum "$ID" - ;; - sles) - install_via_zypper - ;; - *) - # before downloading manually, double check if we didn't miss any debian or - # rh/fedora derived distros using the ID_LIKE var. - case "${ID_LIKE}" in - ubuntu | debian) - install_via_apt_get - ;; - centos | fedora | rhel) - # There is no repository for "fedora", and there is no difference - # between the repositories for "centos" and "rhel", so pick an arbitrary - # one. - install_via_yum rhel - ;; - *) - if [ "$TELEPORT_EDITION" = "cloud" ]; then - echo "The system does not support a package manager, which is required for Teleport Enterprise Cloud." - exit 1 - fi - - # if ID and ID_LIKE didn't return a supported distro, download through curl - echo "There is no officially supported package for your package manager. Downloading and installing Teleport via curl." - install_via_curl - ;; - esac - ;; - esac - - GREEN='\033[0;32m' - COLOR_OFF='\033[0m' - - echo "" - echo -e "${GREEN}$(teleport version) installed successfully!${COLOR_OFF}" - echo "" - echo "The following commands are now available:" - if type teleport &>/dev/null; then - echo " teleport - The daemon that runs the Auth Service, Proxy Service, and other Teleport services." - fi - if type tsh &>/dev/null; then - echo " tsh - A tool that lets end users interact with Teleport." - fi - if type tctl &>/dev/null; then - echo " tctl - An administrative tool that can configure the Teleport Auth Service." - fi - if type tbot &>/dev/null; then - echo " tbot - Teleport Machine ID client." - fi - if type fdpass-teleport &>/dev/null; then - echo " fdpass-teleport - Teleport Machine ID client." - fi - if type teleport-update &>/dev/null; then - echo " teleport-update - Teleport auto-update agent." - fi -} - -# The suffix is "-ent" if we are installing a commercial edition of Teleport and -# empty for Teleport Community Edition. -TELEPORT_SUFFIX="" -TELEPORT_VERSION="" -TELEPORT_EDITION="" -if [ $# -ge 1 ] && [ -n "$1" ]; then - TELEPORT_VERSION=$1 -else - echo "ERROR: Please provide the version you want to install (e.g., 10.1.9)." - exit 1 -fi - -if ! echo "$1" | grep -qE "[0-9]+\.[0-9]+\.[0-9]+"; then - echo "ERROR: The first parameter must be a version number, e.g., 10.1.9." - exit 1 -fi - -if [ $# -ge 2 ] && [ -n "$2" ]; then - TELEPORT_EDITION=$2 - - case $TELEPORT_EDITION in - enterprise | cloud) - TELEPORT_SUFFIX="-ent" - ;; - # An empty edition defaults to OSS. - oss | "" ) - ;; - *) - echo 'ERROR: The second parameter must be "oss", "cloud", or "enterprise".' - exit 1 - ;; - esac -fi -install_teleport diff --git a/assets/install-scripts/install.sh b/assets/install-scripts/install.sh new file mode 120000 index 0000000000000..c41183ab5d100 --- /dev/null +++ b/assets/install-scripts/install.sh @@ -0,0 +1 @@ +../../lib/web/scripts/install/install.sh \ No newline at end of file diff --git a/lib/srv/server/installer/defaultinstallers.go b/lib/srv/server/installer/defaultinstallers.go index c5c2642903bb8..f9bb4dbf77b9f 100644 --- a/lib/srv/server/installer/defaultinstallers.go +++ b/lib/srv/server/installer/defaultinstallers.go @@ -40,7 +40,7 @@ func oneoffScriptToDefaultInstaller() *types.InstallerV1 { } script, err := oneoff.BuildScript(oneoff.OneOffScriptParams{ - TeleportArgs: strings.Join(argsList, " "), + EntrypointArgs: strings.Join(argsList, " "), SuccessMessage: "Teleport is installed and running.", TeleportCommandPrefix: oneoff.PrefixSUDO, }) diff --git a/lib/web/apiserver.go b/lib/web/apiserver.go index 95ca84e410619..009083aa9dede 100644 --- a/lib/web/apiserver.go +++ b/lib/web/apiserver.go @@ -894,6 +894,11 @@ func (h *Handler) bindDefaultEndpoints() { h.GET("/webapi/tokens", h.WithAuth(h.getTokens)) h.DELETE("/webapi/tokens", h.WithAuth(h.deleteToken)) + // install script, the ':token' wildcard is a hack to make the router happy and support + // the token-less route "/scripts/install.sh". + // h.installScriptHandle Will reject any unknown sub-route. + h.GET("/scripts/:token", h.WithHighLimiter(h.installScriptHandle)) + // join scripts h.GET("/scripts/:token/install-node.sh", h.WithLimiter(h.getNodeJoinScriptHandle)) h.GET("/scripts/:token/install-app.sh", h.WithLimiter(h.getAppJoinScriptHandle)) diff --git a/lib/web/apiserver_test.go b/lib/web/apiserver_test.go index ca6f1272e1763..a9d93db9559a0 100644 --- a/lib/web/apiserver_test.go +++ b/lib/web/apiserver_test.go @@ -8538,9 +8538,9 @@ func createProxy(ctx context.Context, t *testing.T, proxyID string, node *regula }, ) handler.handler.cfg.ProxyKubeAddr = utils.FromAddr(kubeProxyAddr) + handler.handler.cfg.PublicProxyAddr = webServer.Listener.Addr().String() url, err := url.Parse("https://" + webServer.Listener.Addr().String()) require.NoError(t, err) - handler.handler.cfg.PublicProxyAddr = url.String() return &testProxy{ clock: clock, diff --git a/lib/web/autoupdate_common.go b/lib/web/autoupdate_common.go index 8e2d817c78c46..0daaadaec02ce 100644 --- a/lib/web/autoupdate_common.go +++ b/lib/web/autoupdate_common.go @@ -42,7 +42,7 @@ func (h *Handler) autoUpdateAgentVersion(ctx context.Context, group, updaterUUID rollout, err := h.cfg.AccessPoint.GetAutoUpdateAgentRollout(ctx) if err != nil { // Fallback to channels if there is no autoupdate_agent_rollout. - if trace.IsNotFound(err) { + if trace.IsNotFound(err) || trace.IsNotImplemented(err) { return getVersionFromChannel(ctx, h.cfg.AutomaticUpgradesChannels, group) } // Something is broken, we don't want to fallback to channels, this would be harmful. @@ -77,7 +77,7 @@ func (h *Handler) autoUpdateAgentShouldUpdate(ctx context.Context, group, update rollout, err := h.cfg.AccessPoint.GetAutoUpdateAgentRollout(ctx) if err != nil { // Fallback to channels if there is no autoupdate_agent_rollout. - if trace.IsNotFound(err) { + if trace.IsNotFound(err) || trace.IsNotImplemented(err) { // Updaters using the RFD184 API are not aware of maintenance windows // like RFD109 updaters are. To have both updaters adopt the same behavior // we must do the CMC window lookup for them. diff --git a/lib/web/integrations_awsoidc.go b/lib/web/integrations_awsoidc.go index aa8019b330f49..ff98c9ed8f1dd 100644 --- a/lib/web/integrations_awsoidc.go +++ b/lib/web/integrations_awsoidc.go @@ -600,7 +600,7 @@ func (h *Handler) awsOIDCConfigureDeployServiceIAM(w http.ResponseWriter, r *htt fmt.Sprintf("--aws-account-id=%s", shsprintf.EscapeDefaultContext(awsAccountID)), } script, err := oneoff.BuildScript(oneoff.OneOffScriptParams{ - TeleportArgs: strings.Join(argsList, " "), + EntrypointArgs: strings.Join(argsList, " "), SuccessMessage: "Success! You can now go back to the Teleport Web UI to complete the database enrollment.", }) if err != nil { @@ -633,7 +633,7 @@ func (h *Handler) awsOIDCConfigureAWSAppAccessIAM(w http.ResponseWriter, r *http fmt.Sprintf("--role=%s", shsprintf.EscapeDefaultContext(role)), } script, err := oneoff.BuildScript(oneoff.OneOffScriptParams{ - TeleportArgs: strings.Join(argsList, " "), + EntrypointArgs: strings.Join(argsList, " "), SuccessMessage: "Success! You can now go back to the Teleport Web UI to use AWS App Access.", }) if err != nil { @@ -704,7 +704,7 @@ func (h *Handler) awsOIDCConfigureEC2SSMIAM(w http.ResponseWriter, r *http.Reque fmt.Sprintf("--aws-account-id=%s", shsprintf.EscapeDefaultContext(awsAccountID)), } script, err := oneoff.BuildScript(oneoff.OneOffScriptParams{ - TeleportArgs: strings.Join(argsList, " "), + EntrypointArgs: strings.Join(argsList, " "), SuccessMessage: "Success! You can now go back to the Teleport Web UI to finish the EC2 auto discover set up.", }) if err != nil { @@ -745,7 +745,7 @@ func (h *Handler) awsOIDCConfigureEKSIAM(w http.ResponseWriter, r *http.Request, fmt.Sprintf("--aws-account-id=%s", shsprintf.EscapeDefaultContext(awsAccountID)), } script, err := oneoff.BuildScript(oneoff.OneOffScriptParams{ - TeleportArgs: strings.Join(argsList, " "), + EntrypointArgs: strings.Join(argsList, " "), SuccessMessage: "Success! You can now go back to the Teleport Web UI to complete the EKS enrollment.", }) if err != nil { @@ -1252,7 +1252,7 @@ func (h *Handler) awsOIDCConfigureIdP(w http.ResponseWriter, r *http.Request, p } script, err := oneoff.BuildScript(oneoff.OneOffScriptParams{ - TeleportArgs: strings.Join(argsList, " "), + EntrypointArgs: strings.Join(argsList, " "), SuccessMessage: "Success! You can now go back to the Teleport Web UI to use the integration with AWS.", }) if err != nil { @@ -1293,7 +1293,7 @@ func (h *Handler) awsOIDCConfigureListDatabasesIAM(w http.ResponseWriter, r *htt fmt.Sprintf("--aws-account-id=%s", shsprintf.EscapeDefaultContext(awsAccountID)), } script, err := oneoff.BuildScript(oneoff.OneOffScriptParams{ - TeleportArgs: strings.Join(argsList, " "), + EntrypointArgs: strings.Join(argsList, " "), SuccessMessage: "Success! You can now go back to the Teleport Web UI to complete the Database enrollment.", }) if err != nil { @@ -1339,7 +1339,7 @@ func (h *Handler) awsAccessGraphOIDCSync(w http.ResponseWriter, r *http.Request, fmt.Sprintf("--aws-account-id=%s", shsprintf.EscapeDefaultContext(awsAccountID)), } script, err := oneoff.BuildScript(oneoff.OneOffScriptParams{ - TeleportArgs: strings.Join(argsList, " "), + EntrypointArgs: strings.Join(argsList, " "), SuccessMessage: "Success! You can now go back to the Teleport Web UI to complete the Access Graph AWS Sync enrollment.", }) if err != nil { diff --git a/lib/web/integrations_awsoidc_test.go b/lib/web/integrations_awsoidc_test.go index 8e1c099bf5575..babdf4d56ed3d 100644 --- a/lib/web/integrations_awsoidc_test.go +++ b/lib/web/integrations_awsoidc_test.go @@ -173,7 +173,7 @@ func TestBuildDeployServiceConfigureIAMScript(t *testing.T) { } require.Contains(t, string(resp.Bytes()), - fmt.Sprintf("teleportArgs='%s'\n", tc.expectedTeleportArgs), + fmt.Sprintf("entrypointArgs='%s'\n", tc.expectedTeleportArgs), ) }) } @@ -304,7 +304,7 @@ func TestBuildEC2SSMIAMScript(t *testing.T) { } require.Contains(t, string(resp.Bytes()), - fmt.Sprintf("teleportArgs='%s'\n", tc.expectedTeleportArgs), + fmt.Sprintf("entrypointArgs='%s'\n", tc.expectedTeleportArgs), ) }) } @@ -379,7 +379,7 @@ func TestBuildAWSAppAccessConfigureIAMScript(t *testing.T) { } require.Contains(t, string(resp.Bytes()), - fmt.Sprintf("teleportArgs='%s'\n", tc.expectedTeleportArgs), + fmt.Sprintf("entrypointArgs='%s'\n", tc.expectedTeleportArgs), ) }) } @@ -482,7 +482,7 @@ func TestBuildEKSConfigureIAMScript(t *testing.T) { } require.Contains(t, string(resp.Bytes()), - fmt.Sprintf("teleportArgs='%s'\n", tc.expectedTeleportArgs), + fmt.Sprintf("entrypointArgs='%s'\n", tc.expectedTeleportArgs), ) }) } @@ -614,7 +614,7 @@ func TestBuildAWSOIDCIdPConfigureScript(t *testing.T) { } require.Contains(t, string(resp.Bytes()), - fmt.Sprintf("teleportArgs='%s'\n", tc.expectedTeleportArgs), + fmt.Sprintf("entrypointArgs='%s'\n", tc.expectedTeleportArgs), ) }) } @@ -717,7 +717,7 @@ func TestBuildListDatabasesConfigureIAMScript(t *testing.T) { } require.Contains(t, string(resp.Bytes()), - fmt.Sprintf("teleportArgs='%s'\n", tc.expectedTeleportArgs), + fmt.Sprintf("entrypointArgs='%s'\n", tc.expectedTeleportArgs), ) }) } diff --git a/lib/web/integrations_azureoidc.go b/lib/web/integrations_azureoidc.go index 0ce8d624a79f1..3a1dd654550e5 100644 --- a/lib/web/integrations_azureoidc.go +++ b/lib/web/integrations_azureoidc.go @@ -66,7 +66,7 @@ func (h *Handler) azureOIDCConfigure(w http.ResponseWriter, r *http.Request, p h } script, err := oneoff.BuildScript(oneoff.OneOffScriptParams{ - TeleportArgs: strings.Join(argsList, " "), + EntrypointArgs: strings.Join(argsList, " "), SuccessMessage: "Success! You can now go back to the Teleport Web UI to use the integration with Azure.", }) if err != nil { diff --git a/lib/web/integrations_azureoidc_test.go b/lib/web/integrations_azureoidc_test.go index cbdadad4ef433..6e0f72c6d3136 100644 --- a/lib/web/integrations_azureoidc_test.go +++ b/lib/web/integrations_azureoidc_test.go @@ -97,7 +97,7 @@ func TestAzureOIDCConfigureScript(t *testing.T) { } require.Contains(t, string(resp.Bytes()), - fmt.Sprintf("teleportArgs='%s'\n", tc.expectedTeleportArgs), + fmt.Sprintf("entrypointArgs='%s'\n", tc.expectedTeleportArgs), ) }) } diff --git a/lib/web/integrations_samlidp.go b/lib/web/integrations_samlidp.go index 0ea1e0b1d67d3..eda5dac78a265 100644 --- a/lib/web/integrations_samlidp.go +++ b/lib/web/integrations_samlidp.go @@ -56,7 +56,7 @@ func (h *Handler) gcpWorkforceConfigScript(w http.ResponseWriter, r *http.Reques fmt.Sprintf("--idp-metadata-url=%s", shsprintf.EscapeDefaultContext(samlIdPMetadataURL)), } script, err := oneoff.BuildScript(oneoff.OneOffScriptParams{ - TeleportArgs: strings.Join(argsList, " "), + EntrypointArgs: strings.Join(argsList, " "), SuccessMessage: "Success! You can now go back to the Teleport Web UI to complete enrolling this workforce pool to Teleport SAML Identity Provider.", }) if err != nil { diff --git a/lib/web/scripts.go b/lib/web/scripts.go new file mode 100644 index 0000000000000..d5e89bec59e1d --- /dev/null +++ b/lib/web/scripts.go @@ -0,0 +1,154 @@ +/* + * Teleport + * Copyright (C) 2025 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package web + +import ( + "context" + "fmt" + "net/http" + "os" + + "github.com/gravitational/trace" + "github.com/julienschmidt/httprouter" + + "github.com/gravitational/teleport" + "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/lib/modules" + "github.com/gravitational/teleport/lib/utils/teleportassets" + "github.com/gravitational/teleport/lib/web/scripts" +) + +// installScriptHandle handles calls for "/scripts/install.sh" and responds with a bash script installing Teleport +// by downloading and running `teleport-update`. This installation script does not start the agent, join it, +// or configure its services. This is handled by the "/scripts/:token/install-*.sh" scripts. +func (h *Handler) installScriptHandle(w http.ResponseWriter, r *http.Request, params httprouter.Params) (any, error) { + // This is a hack because the router is not allowing us to register "/scripts/install.sh", so we use + // the parameter ":token" to match the script name. + // Currently, only "install.sh" is supported. + if params.ByName("token") != "install.sh" { + return nil, trace.NotFound(`Route not found, query "/scripts/install.sh" for the install-only script, or "/scripts/:token/install-node.sh" for the install + join script.`) + } + + // TODO(hugoShaka): cache function + opts, err := h.installScriptOptions(r.Context()) + if err != nil { + return nil, trace.Wrap(err, "Failed to build install script options") + } + + script, err := scripts.GetInstallScript(r.Context(), opts) + if err != nil { + h.logger.WarnContext(r.Context(), "Failed to get install script", "error", err) + return nil, trace.Wrap(err, "getting script") + } + + w.WriteHeader(http.StatusOK) + if _, err := fmt.Fprintln(w, script); err != nil { + h.logger.WarnContext(r.Context(), "Failed to write install script", "error", err) + } + + return nil, nil +} + +// installScriptOptions computes the agent installation options based on the proxy configuration and the cluster status. +// This includes: +// - the type of automatic updates +// - the desired version +// - the proxy address (used for updates). +// - the Teleport artifact name and CDN +func (h *Handler) installScriptOptions(ctx context.Context) (scripts.InstallScriptOptions, error) { + const defaultGroup, defaultUpdater = "", "" + + version, err := h.autoUpdateAgentVersion(ctx, defaultGroup, defaultUpdater) + if err != nil { + h.logger.WarnContext(ctx, "Failed to get intended agent version", "error", err) + version = teleport.Version + } + + // if there's a rollout, we do new autoupdates + _, rolloutErr := h.cfg.AccessPoint.GetAutoUpdateAgentRollout(ctx) + if rolloutErr != nil && !(trace.IsNotFound(rolloutErr) || trace.IsNotImplemented(rolloutErr)) { + h.logger.WarnContext(ctx, "Failed to get rollout", "error", rolloutErr) + return scripts.InstallScriptOptions{}, trace.Wrap(err, "failed to check the autoupdate agent rollout state") + } + + var autoupdateStyle scripts.AutoupdateStyle + switch { + case rolloutErr == nil: + autoupdateStyle = scripts.UpdaterBinaryAutoupdate + case automaticUpgrades(h.clusterFeatures): + autoupdateStyle = scripts.PackageManagerAutoupdate + default: + autoupdateStyle = scripts.NoAutoupdate + } + + var teleportFlavor string + switch modules.GetModules().BuildType() { + case modules.BuildEnterprise: + teleportFlavor = types.PackageNameEnt + case modules.BuildOSS, modules.BuildCommunity: + teleportFlavor = types.PackageNameOSS + default: + h.logger.WarnContext(ctx, "Unknown built type, defaulting to the 'teleport' package.", "type", modules.GetModules().BuildType()) + teleportFlavor = types.PackageNameOSS + } + + cdnBaseURL, err := getCDNBaseURL() + if err != nil { + h.logger.WarnContext(ctx, "Failed to get CDN base URL", "error", err) + return scripts.InstallScriptOptions{}, trace.Wrap(err) + } + + return scripts.InstallScriptOptions{ + AutoupdateStyle: autoupdateStyle, + TeleportVersion: version, + CDNBaseURL: cdnBaseURL, + ProxyAddr: h.PublicProxyAddr(), + TeleportFlavor: teleportFlavor, + FIPS: modules.IsBoringBinary(), + }, nil + +} + +// EnvVarCDNBaseURL is the environment variable that allows users to override the Teleport base CDN url used in the installation script. +// Setting this value is required for testing (make production builds install from the dev CDN, and vice versa). +// As we (the Teleport company) don't distribute AGPL binaries, this must be set when using a Teleport OSS build. +// Example values: +// - "https://cdn.teleport.dev" (prod) +// - "https://cdn.cloud.gravitational.io" (dev builds/staging) +const EnvVarCDNBaseURL = "TELEPORT_CDN_BASE_URL" + +func getCDNBaseURL() (string, error) { + // If the user explicitly overrides the CDN base URL, we use it. + if override := os.Getenv(EnvVarCDNBaseURL); override != "" { + return override, nil + } + + // If this is an AGPL build, we don't want to automatically install binaries distributed under a more restrictive + // license so we error and ask the user set the CDN URL, either to: + // - the official Teleport CDN if they agree with the community license and meet its requirements + // - a custom CDN where they can store their own AGPL binaries + if modules.GetModules().BuildType() == modules.BuildOSS { + return "", trace.BadParameter( + "This proxy is licensed under AGPL but CDN binaries are licensed under the more restrictive Community license. "+ + "You can set TELEPORT_CDN_BASE_URL to a custom CDN, or to %q if you are OK with using the Community Edition license.", + teleportassets.CDNBaseURL()) + } + + return teleportassets.CDNBaseURL(), nil +} diff --git a/lib/web/scripts/install.go b/lib/web/scripts/install.go new file mode 100644 index 0000000000000..25a8970e58563 --- /dev/null +++ b/lib/web/scripts/install.go @@ -0,0 +1,167 @@ +/* + * Teleport + * Copyright (C) 2025 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package scripts + +import ( + "context" + _ "embed" + "net/url" + "strings" + + "github.com/google/safetext/shsprintf" + "github.com/gravitational/trace" + + "github.com/gravitational/teleport/lib/web/scripts/oneoff" +) + +// AutoupdateStyle represents the kind of autoupdate mechanism the script should use. +type AutoupdateStyle int + +const ( + // NoAutoupdate means the installed Teleport should not autoupdate. + NoAutoupdate AutoupdateStyle = iota + // PackageManagerAutoupdate means the installed Teleport should update via a script triggering package manager + // updates. The script lives in the 'teleport-ent-update' package and was our original attempt at automatic updates. + // See RFD-109 for more details: https://github.com/gravitational/teleport/blob/master/rfd/0109-cloud-agent-upgrades.md + PackageManagerAutoupdate + // UpdaterBinaryAutoupdate means the installed Teleport should update via the teleport-update binary. + // This update style does not depend on any package manager (although it has a system dependency to wake up the + // updater). + // See RFD-184 for more details: https://github.com/gravitational/teleport/blob/master/rfd/0184-agent-auto-updates.md + UpdaterBinaryAutoupdate +) + +// InstallScriptOptions contains the Teleport installation options used to generate installation scripts. +type InstallScriptOptions struct { + AutoupdateStyle AutoupdateStyle + // TeleportVersion that should be installed. Without the leading "v". + TeleportVersion string + // CDNBaseURL is the URL of the CDN hosting teleport tarballs. + // If left empty, the 'teleport-update' installer will pick the one to use. + // For example: "https://cdn.example.com" + CDNBaseURL string + // ProxyAddr is the address of the Teleport Proxy service that will be used + // by the updater to fetch the desired version. Teleport Addrs are + // 'hostname:port' (no scheme nor path). + ProxyAddr string + // TeleportFlavor is the name of the Teleport artifact fetched from the CDN. + // Common values are "teleport" and "teleport-ent". + TeleportFlavor string + // FIPS represents if the installed Teleport version should use Teleport + // binaries built for FIPS compliance. + FIPS bool +} + +// Check validates that the minimal options are set. +func (o *InstallScriptOptions) Check() error { + switch o.AutoupdateStyle { + case NoAutoupdate, PackageManagerAutoupdate: + return nil + case UpdaterBinaryAutoupdate: + // We'll do the checks later. + default: + return trace.BadParameter("unsupported autoupdate style: %v", o.AutoupdateStyle) + } + if o.ProxyAddr == "" { + return trace.BadParameter("Proxy address is required") + } + + if o.TeleportVersion == "" { + return trace.BadParameter("Teleport version is required") + } + + if o.TeleportFlavor == "" { + return trace.BadParameter("Teleport flavor is required") + } + + if o.CDNBaseURL != "" { + url, err := url.Parse(o.CDNBaseURL) + if err != nil { + return trace.Wrap(err, "failed to parse CDN base URL") + } + if url.Scheme != "https" { + return trace.BadParameter("CDNBaseURL's scheme must be 'https://'") + } + } + return nil +} + +// oneOffParams returns the oneoff.OneOffScriptParams that will install Teleport +// using the oneoff.sh script to download and execute 'teleport-update'. +func (o *InstallScriptOptions) oneOffParams() (params oneoff.OneOffScriptParams) { + // We add the leading v if it's not here + version := o.TeleportVersion + if o.TeleportVersion[0] != 'v' { + version = "v" + o.TeleportVersion + } + + args := []string{"enable", "--proxy", shsprintf.EscapeDefaultContext(o.ProxyAddr)} + if o.CDNBaseURL != "" { + args = append(args, "--base-url", shsprintf.EscapeDefaultContext(o.CDNBaseURL)) + } + + return oneoff.OneOffScriptParams{ + Entrypoint: "teleport-update", + EntrypointArgs: strings.Join(args, " "), + CDNBaseURL: o.CDNBaseURL, + TeleportVersion: version, + TeleportFlavor: o.TeleportFlavor, + SuccessMessage: "Teleport successfully installed.", + TeleportFIPS: o.FIPS, + } +} + +// GetInstallScript returns a Teleport installation script. +// This script only installs Teleport, it does not start the agent, join it, nor configure its services. +// See the InstallNodeBashScript if you need a more complete setup. +func GetInstallScript(ctx context.Context, opts InstallScriptOptions) (string, error) { + switch opts.AutoupdateStyle { + case NoAutoupdate, PackageManagerAutoupdate: + return getLegacyInstallScript(ctx, opts) + case UpdaterBinaryAutoupdate: + return getUpdaterInstallScript(ctx, opts) + default: + return "", trace.BadParameter("unsupported autoupdate style: %v", opts.AutoupdateStyle) + } +} + +//go:embed install/install.sh +var legacyInstallScript string + +// getLegacyInstallScript returns the installation script that we have been serving at +// "https://cdn.teleport.dev/install.sh". This script installs teleport via package manager +// or by unpacking the tarball. Its usage should be phased out in favor of the updater-based +// installation script served by getUpdaterInstallScript. +func getLegacyInstallScript(ctx context.Context, opts InstallScriptOptions) (string, error) { + return legacyInstallScript, nil +} + +// getUpdaterInstallScript returns an installation script that downloads teleport-update +// and uses it to install a self-updating version of Teleport. +// This installation script is based on the oneoff.sh script and will become the standard +// way of installing Teleport. +func getUpdaterInstallScript(ctx context.Context, opts InstallScriptOptions) (string, error) { + if err := opts.Check(); err != nil { + return "", trace.Wrap(err, "invalid install script parameters") + } + + scriptParams := opts.oneOffParams() + + return oneoff.BuildScript(scriptParams) +} diff --git a/lib/web/scripts/install/install.sh b/lib/web/scripts/install/install.sh new file mode 100755 index 0000000000000..52d3da00e4f63 --- /dev/null +++ b/lib/web/scripts/install/install.sh @@ -0,0 +1,430 @@ +#!/bin/bash +# Copyright 2022 Gravitational, Inc + +# This script detects the current Linux distribution and installs Teleport +# through its package manager, if supported, or downloading a tarball otherwise. +# We'll download Teleport from the official website and checksum it to make sure it was properly +# downloaded before executing. + +# The script is wrapped inside a function to protect against the connection being interrupted +# in the middle of the stream. + +# For more download options, head to https://goteleport.com/download/ + +set -euo pipefail + +# download uses curl or wget to download a teleport binary +download() { + URL=$1 + TMP_PATH=$2 + + echo "Downloading $URL" + if type curl &>/dev/null; then + set -x + # shellcheck disable=SC2086 + $SUDO $CURL -o "$TMP_PATH" "$URL" + else + set -x + # shellcheck disable=SC2086 + $SUDO $CURL -O "$TMP_PATH" "$URL" + fi + set +x +} + +install_via_apt_get() { + echo "Installing Teleport v$TELEPORT_VERSION via apt-get" + add_apt_key + set -x + $SUDO apt-get install -y "teleport$TELEPORT_SUFFIX=$TELEPORT_VERSION" + set +x + if [ "$TELEPORT_EDITION" = "cloud" ]; then + set -x + $SUDO apt-get install -y teleport-ent-updater + set +x + fi +} + +add_apt_key() { + APT_REPO_ID=$ID + APT_REPO_VERSION_CODENAME=$VERSION_CODENAME + IS_LEGACY=0 + + # check if we must use legacy .asc key + case "$ID" in + ubuntu | pop | neon | zorin) + if ! expr "$VERSION_ID" : "2.*" >/dev/null; then + IS_LEGACY=1 + fi + ;; + debian | raspbian) + if [ "$VERSION_ID" -lt 11 ]; then + IS_LEGACY=1 + fi + ;; + linuxmint | parrot) + if [ "$VERSION_ID" -lt 5 ]; then + IS_LEGACY=1 + fi + ;; + elementary) + if [ "$VERSION_ID" -lt 6 ]; then + IS_LEGACY=1 + fi + ;; + kali) + YEAR="$(echo "$VERSION_ID" | cut -f1 -d.)" + if [ "$YEAR" -lt 2021 ]; then + IS_LEGACY=1 + fi + ;; + esac + + if [[ "$IS_LEGACY" == 0 ]]; then + # set APT_REPO_ID if necessary + case "$ID" in + linuxmint | kali | elementary | pop | raspbian | neon | zorin | parrot) + APT_REPO_ID=$ID_LIKE + ;; + esac + + # set APT_REPO_VERSION_CODENAME if necessary + case "$ID" in + linuxmint | elementary | pop | neon | zorin) + APT_REPO_VERSION_CODENAME=$UBUNTU_CODENAME + ;; + kali) + APT_REPO_VERSION_CODENAME="bullseye" + ;; + parrot) + APT_REPO_VERSION_CODENAME="buster" + ;; + esac + fi + + echo "Downloading Teleport's PGP public key..." + TEMP_DIR=$(mktemp -d -t teleport-XXXXXXXXXX) + MAJOR=$(echo "$TELEPORT_VERSION" | cut -f1 -d.) + TELEPORT_REPO="" + + CHANNEL="stable/v${MAJOR}" + if [ "$TELEPORT_EDITION" = "cloud" ]; then + CHANNEL="stable/cloud" + fi + + if [[ "$IS_LEGACY" == 1 ]]; then + if ! type gpg >/dev/null; then + echo "Installing gnupg" + set -x + $SUDO apt-get update + $SUDO apt-get install -y gnupg + set +x + fi + TMP_KEY="$TEMP_DIR/teleport-pubkey.asc" + download "https://deb.releases.teleport.dev/teleport-pubkey.asc" "$TMP_KEY" + set -x + $SUDO apt-key add "$TMP_KEY" + set +x + TELEPORT_REPO="deb https://apt.releases.teleport.dev/${APT_REPO_ID?} ${APT_REPO_VERSION_CODENAME?} ${CHANNEL}" + else + TMP_KEY="$TEMP_DIR/teleport-pubkey.gpg" + download "https://apt.releases.teleport.dev/gpg" "$TMP_KEY" + set -x + $SUDO mkdir -p /etc/apt/keyrings + $SUDO cp "$TMP_KEY" /etc/apt/keyrings/teleport-archive-keyring.asc + set +x + TELEPORT_REPO="deb [signed-by=/etc/apt/keyrings/teleport-archive-keyring.asc] https://apt.releases.teleport.dev/${APT_REPO_ID?} ${APT_REPO_VERSION_CODENAME?} ${CHANNEL}" + fi + + set -x + echo "$TELEPORT_REPO" | $SUDO tee /etc/apt/sources.list.d/teleport.list >/dev/null + set +x + + set -x + $SUDO apt-get update + set +x +} + +# $1 is the value of the $ID path segment in the YUM repo URL. In +# /etc/os-release, this is either the value of $ID or $ID_LIKE. +install_via_yum() { + # shellcheck source=/dev/null + source /etc/os-release + + # Get the major version from the version ID. + VERSION_ID=$(echo "$VERSION_ID" | grep -Eo "^[0-9]+") + TELEPORT_MAJOR_VERSION="v$(echo "$TELEPORT_VERSION" | grep -Eo "^[0-9]+")" + + CHANNEL="stable/${TELEPORT_MAJOR_VERSION}" + if [ "$TELEPORT_EDITION" = "cloud" ]; then + CHANNEL="stable/cloud" + fi + + if type dnf &>/dev/null; then + echo "Installing Teleport v$TELEPORT_VERSION through dnf" + $SUDO dnf install -y 'dnf-command(config-manager)' + $SUDO dnf config-manager --add-repo "$(rpm --eval "https://yum.releases.teleport.dev/$1/$VERSION_ID/Teleport/%{_arch}/$CHANNEL/teleport-yum.repo")" + $SUDO dnf install -y "teleport$TELEPORT_SUFFIX-$TELEPORT_VERSION" + + if [ "$TELEPORT_EDITION" = "cloud" ]; then + $SUDO dnf install -y teleport-ent-updater + fi + + else + echo "Installing Teleport v$TELEPORT_VERSION through yum" + $SUDO yum install -y yum-utils + $SUDO yum-config-manager --add-repo "$(rpm --eval "https://yum.releases.teleport.dev/$1/$VERSION_ID/Teleport/%{_arch}/$CHANNEL/teleport-yum.repo")" + $SUDO yum install -y "teleport$TELEPORT_SUFFIX-$TELEPORT_VERSION" + + if [ "$TELEPORT_EDITION" = "cloud" ]; then + $SUDO yum install -y teleport-ent-updater + fi + fi + set +x +} + +install_via_zypper() { + # shellcheck source=/dev/null + source /etc/os-release + + # Get the major version from the version ID. + VERSION_ID=$(echo "$VERSION_ID" | grep -Eo "^[0-9]+") + TELEPORT_MAJOR_VERSION="v$(echo "$TELEPORT_VERSION" | grep -Eo "^[0-9]+")" + + CHANNEL="stable/${TELEPORT_MAJOR_VERSION}" + if [ "$TELEPORT_EDITION" = "cloud" ]; then + CHANNEL="stable/cloud" + fi + + $SUDO rpm --import https://zypper.releases.teleport.dev/gpg + $SUDO zypper addrepo --refresh --repo "$(rpm --eval "https://zypper.releases.teleport.dev/$ID/$VERSION_ID/Teleport/%{_arch}/$CHANNEL/teleport-zypper.repo")" + $SUDO zypper --gpg-auto-import-keys refresh teleport + $SUDO zypper install -y "teleport$TELEPORT_SUFFIX" + + if [ "$TELEPORT_EDITION" = "cloud" ]; then + $SUDO zypper install -y teleport-ent-updater + fi + + set +x +} + + +# download .tar.gz file via curl/wget, unzip it and run the install script +install_via_curl() { + TEMP_DIR=$(mktemp -d -t teleport-XXXXXXXXXX) + + TELEPORT_FILENAME="teleport$TELEPORT_SUFFIX-v$TELEPORT_VERSION-linux-$ARCH-bin.tar.gz" + URL="https://cdn.teleport.dev/${TELEPORT_FILENAME}" + download "${URL}" "${TEMP_DIR}/${TELEPORT_FILENAME}" + + TMP_CHECKSUM="${TEMP_DIR}/${TELEPORT_FILENAME}.sha256" + download "${URL}.sha256" "$TMP_CHECKSUM" + + set -x + cd "$TEMP_DIR" + # shellcheck disable=SC2086 + $SUDO $SHA_COMMAND -c "$TMP_CHECKSUM" + cd - + + $SUDO tar -xzf "${TEMP_DIR}/${TELEPORT_FILENAME}" -C "$TEMP_DIR" + $SUDO "$TEMP_DIR/teleport/install" + set +x +} + +# wrap script in a function so a partially downloaded script +# doesn't execute +install_teleport() { + # exit if not on Linux + if [[ $(uname) != "Linux" ]]; then + echo "ERROR: This script works only for Linux. Please go to the downloads page to find the proper installation method for your operating system:" + echo "https://goteleport.com/download/" + exit 1 + fi + + KERNEL_VERSION=$(uname -r) + MIN_VERSION="2.6.23" + if [ $MIN_VERSION != "$(echo -e "$MIN_VERSION\n$KERNEL_VERSION" | sort -V | head -n1)" ]; then + echo "ERROR: Teleport requires Linux kernel version $MIN_VERSION+" + exit 1 + fi + + # check if can run as admin either by running as root or by + # having 'sudo' or 'doas' installed + IS_ROOT="" + SUDO="" + if [ "$(id -u)" = 0 ]; then + # running as root, no need for sudo/doas + IS_ROOT="YES" + SUDO="" + elif type sudo &>/dev/null; then + SUDO="sudo" + elif type doas &>/dev/null; then + SUDO="doas" + fi + + if [ -z "$SUDO" ] && [ -z "$IS_ROOT" ]; then + echo "ERROR: The installer requires a way to run commands as root." + echo "Either run this script as root or install sudo/doas." + exit 1 + fi + + # require curl/wget + CURL="" + if type curl &>/dev/null; then + CURL="curl -fL" + elif type wget &>/dev/null; then + CURL="wget" + fi + if [ -z "$CURL" ]; then + echo "ERROR: This script requires either curl or wget in order to download files. Please install one of them and try again." + exit 1 + fi + + # require shasum/sha256sum + SHA_COMMAND="" + if type shasum &>/dev/null; then + SHA_COMMAND="shasum -a 256" + elif type sha256sum &>/dev/null; then + SHA_COMMAND="sha256sum" + else + echo "ERROR: This script requires sha256sum or shasum to validate the download. Please install it and try again." + exit 1 + fi + + # detect distro + OS_RELEASE=/etc/os-release + ID="" + ID_LIKE="" + VERSION_CODENAME="" + UBUNTU_CODENAME="" + if [[ -f "$OS_RELEASE" ]]; then + # shellcheck source=/dev/null + . $OS_RELEASE + fi + # Some $ID_LIKE values include multiple distro names in an arbitrary order, so + # evaluate the first one. + ID_LIKE="${ID_LIKE%% *}" + + # detect architecture + ARCH="" + case $(uname -m) in + x86_64) + ARCH="amd64" + ;; + i386) + ARCH="386" + ;; + armv7l) + ARCH="arm" + ;; + aarch64) + ARCH="arm64" + ;; + **) + echo "ERROR: Your system's architecture isn't officially supported or couldn't be determined." + echo "Please refer to the installation guide for more information:" + echo "https://goteleport.com/docs/installation/" + exit 1 + ;; + esac + + # select install method based on distribution + # if ID is debian derivate, run apt-get + case "$ID" in + debian | ubuntu | kali | linuxmint | pop | raspbian | neon | zorin | parrot | elementary) + install_via_apt_get + ;; + # if ID is amazon Linux 2/RHEL/etc, run yum + centos | rhel | amzn) + install_via_yum "$ID" + ;; + sles) + install_via_zypper + ;; + *) + # before downloading manually, double check if we didn't miss any debian or + # rh/fedora derived distros using the ID_LIKE var. + case "${ID_LIKE}" in + ubuntu | debian) + install_via_apt_get + ;; + centos | fedora | rhel) + # There is no repository for "fedora", and there is no difference + # between the repositories for "centos" and "rhel", so pick an arbitrary + # one. + install_via_yum rhel + ;; + *) + if [ "$TELEPORT_EDITION" = "cloud" ]; then + echo "The system does not support a package manager, which is required for Teleport Enterprise Cloud." + exit 1 + fi + + # if ID and ID_LIKE didn't return a supported distro, download through curl + echo "There is no officially supported package for your package manager. Downloading and installing Teleport via curl." + install_via_curl + ;; + esac + ;; + esac + + GREEN='\033[0;32m' + COLOR_OFF='\033[0m' + + echo "" + echo -e "${GREEN}$(teleport version) installed successfully!${COLOR_OFF}" + echo "" + echo "The following commands are now available:" + if type teleport &>/dev/null; then + echo " teleport - The daemon that runs the Auth Service, Proxy Service, and other Teleport services." + fi + if type tsh &>/dev/null; then + echo " tsh - A tool that lets end users interact with Teleport." + fi + if type tctl &>/dev/null; then + echo " tctl - An administrative tool that can configure the Teleport Auth Service." + fi + if type tbot &>/dev/null; then + echo " tbot - Teleport Machine ID client." + fi + if type fdpass-teleport &>/dev/null; then + echo " fdpass-teleport - Teleport Machine ID client." + fi + if type teleport-update &>/dev/null; then + echo " teleport-update - Teleport auto-update agent." + fi +} + +# The suffix is "-ent" if we are installing a commercial edition of Teleport and +# empty for Teleport Community Edition. +TELEPORT_SUFFIX="" +TELEPORT_VERSION="" +TELEPORT_EDITION="" +if [ $# -ge 1 ] && [ -n "$1" ]; then + TELEPORT_VERSION=$1 +else + echo "ERROR: Please provide the version you want to install (e.g., 10.1.9)." + exit 1 +fi + +if ! echo "$1" | grep -qE "[0-9]+\.[0-9]+\.[0-9]+"; then + echo "ERROR: The first parameter must be a version number, e.g., 10.1.9." + exit 1 +fi + +if [ $# -ge 2 ] && [ -n "$2" ]; then + TELEPORT_EDITION=$2 + + case $TELEPORT_EDITION in + enterprise | cloud) + TELEPORT_SUFFIX="-ent" + ;; + # An empty edition defaults to OSS. + oss | "" ) + ;; + *) + echo 'ERROR: The second parameter must be "oss", "cloud", or "enterprise".' + exit 1 + ;; + esac +fi +install_teleport diff --git a/lib/web/scripts/install_test.go b/lib/web/scripts/install_test.go new file mode 100644 index 0000000000000..5606858d4aac8 --- /dev/null +++ b/lib/web/scripts/install_test.go @@ -0,0 +1,138 @@ +/* + * Teleport + * Copyright (C) 2025 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package scripts + +import ( + "context" + "fmt" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/lib/utils/teleportassets" +) + +func TestGetInstallScript(t *testing.T) { + ctx := context.Background() + testVersion := "1.2.3" + testProxyAddr := "proxy.example.com:443" + + tests := []struct { + name string + opts InstallScriptOptions + assertFn func(t *testing.T, script string) + }{ + { + name: "Legacy install, no autoupdate", + opts: InstallScriptOptions{AutoupdateStyle: NoAutoupdate}, + assertFn: func(t *testing.T, script string) { + require.Equal(t, legacyInstallScript, script) + }, + }, + { + name: "Legacy install, package manager autoupdate", + opts: InstallScriptOptions{AutoupdateStyle: NoAutoupdate}, + assertFn: func(t *testing.T, script string) { + require.Equal(t, legacyInstallScript, script) + }, + }, + { + name: "Oneoff install", + opts: InstallScriptOptions{ + AutoupdateStyle: UpdaterBinaryAutoupdate, + TeleportVersion: testVersion, + ProxyAddr: testProxyAddr, + TeleportFlavor: types.PackageNameOSS, + }, + assertFn: func(t *testing.T, script string) { + require.Contains(t, script, "entrypoint='teleport-update'") + require.Contains(t, script, fmt.Sprintf("teleportVersion='v%s'", testVersion)) + require.Contains(t, script, fmt.Sprintf("teleportFlavor='%s'", types.PackageNameOSS)) + require.Contains(t, script, fmt.Sprintf("cdnBaseURL='%s'", teleportassets.CDNBaseURL())) + require.Contains(t, script, fmt.Sprintf("entrypointArgs='enable --proxy %s'", testProxyAddr)) + require.Contains(t, script, "packageSuffix='bin.tar.gz'") + }, + }, + { + name: "Oneoff install custom CDN", + opts: InstallScriptOptions{ + AutoupdateStyle: UpdaterBinaryAutoupdate, + TeleportVersion: testVersion, + ProxyAddr: testProxyAddr, + TeleportFlavor: types.PackageNameOSS, + CDNBaseURL: "https://cdn.example.com", + }, + assertFn: func(t *testing.T, script string) { + require.Contains(t, script, "entrypoint='teleport-update'") + require.Contains(t, script, fmt.Sprintf("teleportVersion='v%s'", testVersion)) + require.Contains(t, script, fmt.Sprintf("teleportFlavor='%s'", types.PackageNameOSS)) + require.Contains(t, script, "cdnBaseURL='https://cdn.example.com'") + require.Contains(t, script, fmt.Sprintf("entrypointArgs='enable --proxy %s --base-url %s'", testProxyAddr, "https://cdn.example.com")) + require.Contains(t, script, "packageSuffix='bin.tar.gz'") + }, + }, + { + name: "Oneoff enterprise install", + opts: InstallScriptOptions{ + AutoupdateStyle: UpdaterBinaryAutoupdate, + TeleportVersion: testVersion, + ProxyAddr: testProxyAddr, + TeleportFlavor: types.PackageNameEnt, + }, + assertFn: func(t *testing.T, script string) { + require.Contains(t, script, "entrypoint='teleport-update'") + require.Contains(t, script, fmt.Sprintf("teleportVersion='v%s'", testVersion)) + require.Contains(t, script, fmt.Sprintf("teleportFlavor='%s'", types.PackageNameEnt)) + require.Contains(t, script, fmt.Sprintf("cdnBaseURL='%s'", teleportassets.CDNBaseURL())) + require.Contains(t, script, fmt.Sprintf("entrypointArgs='enable --proxy %s'", testProxyAddr)) + require.Contains(t, script, "packageSuffix='bin.tar.gz'") + }, + }, + { + name: "Oneoff enterprise FIPS install", + opts: InstallScriptOptions{ + AutoupdateStyle: UpdaterBinaryAutoupdate, + TeleportVersion: testVersion, + ProxyAddr: testProxyAddr, + TeleportFlavor: types.PackageNameEnt, + FIPS: true, + }, + assertFn: func(t *testing.T, script string) { + require.Contains(t, script, "entrypoint='teleport-update'") + require.Contains(t, script, fmt.Sprintf("teleportVersion='v%s'", testVersion)) + require.Contains(t, script, fmt.Sprintf("teleportFlavor='%s'", types.PackageNameEnt)) + require.Contains(t, script, fmt.Sprintf("cdnBaseURL='%s'", teleportassets.CDNBaseURL())) + require.Contains(t, script, fmt.Sprintf("entrypointArgs='enable --proxy %s'", testProxyAddr)) + require.Contains(t, script, "packageSuffix='fips-bin.tar.gz'") + }, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + // Sanity check, test input should be legal. + require.NoError(t, test.opts.Check()) + + // Test execution. + result, err := GetInstallScript(ctx, test.opts) + require.NoError(t, err) + test.assertFn(t, result) + }) + } +} diff --git a/lib/web/scripts/oneoff/oneoff.go b/lib/web/scripts/oneoff/oneoff.go index 5d12c2c938289..794749b1abc02 100644 --- a/lib/web/scripts/oneoff/oneoff.go +++ b/lib/web/scripts/oneoff/oneoff.go @@ -22,6 +22,7 @@ import ( "bytes" _ "embed" "slices" + "strings" "text/template" "github.com/gravitational/trace" @@ -63,9 +64,12 @@ type OneOffScriptParams struct { // Used for testing. binSudo string - // TeleportArgs is the arguments to pass to the teleport binary. + // Entrypoint is the name of the binary from the teleport package. Defaults to "teleport", but can be set to + // other binaries such as "teleport-update" or "tbot". + Entrypoint string + // EntrypointArgs is the arguments to pass to the Entrypoint binary. // Eg, 'version' - TeleportArgs string + EntrypointArgs string // BinUname is the binary used to get OS name and Architecture of the host. // Defaults to `uname`. @@ -88,16 +92,23 @@ type OneOffScriptParams struct { // - teleport-ent TeleportFlavor string + // TeleportFIPS represents if the script should install a FIPS build of Teleport. + TeleportFIPS bool + // SuccessMessage is a message shown to the user after the one off is completed. SuccessMessage string } // CheckAndSetDefaults checks if the required params ara present. func (p *OneOffScriptParams) CheckAndSetDefaults() error { - if p.TeleportArgs == "" { + if p.EntrypointArgs == "" { return trace.BadParameter("missing teleport args") } + if p.Entrypoint == "" { + p.Entrypoint = "teleport" + } + if p.BinUname == "" { p.BinUname = binUname } @@ -117,6 +128,7 @@ func (p *OneOffScriptParams) CheckAndSetDefaults() error { if p.CDNBaseURL == "" { p.CDNBaseURL = teleportassets.CDNBaseURL() } + p.CDNBaseURL = strings.TrimRight(p.CDNBaseURL, "/") if p.TeleportFlavor == "" { p.TeleportFlavor = types.PackageNameOSS diff --git a/lib/web/scripts/oneoff/oneoff.sh b/lib/web/scripts/oneoff/oneoff.sh index 912e4d6ab3368..eaa15841be18b 100644 --- a/lib/web/scripts/oneoff/oneoff.sh +++ b/lib/web/scripts/oneoff/oneoff.sh @@ -5,7 +5,10 @@ cdnBaseURL='{{.CDNBaseURL}}' teleportVersion='{{.TeleportVersion}}' teleportFlavor='{{.TeleportFlavor}}' # teleport or teleport-ent successMessage='{{.SuccessMessage}}' -teleportArgs='{{.TeleportArgs}}' +entrypointArgs='{{.EntrypointArgs}}' +entrypoint='{{.Entrypoint}}' +packageSuffix='{{ if .TeleportFIPS }}fips-{{ end }}bin.tar.gz' +fips='{{ if .TeleportFIPS }}true{{ end }}' # shellcheck disable=all # Use $HOME or / as base dir @@ -17,20 +20,24 @@ ARCH=$({{.BinUname}} -m) trap 'rm -rf -- "$tempDir"' EXIT teleportTarballName() { - if [ ${OS} = "Darwin" ]; then - echo ${teleportFlavor}-${teleportVersion}-darwin-universal-bin.tar.gz + if [ "${OS}" = "Darwin" ]; then + if [ "$fips" = "true"]; then + echo "FIPS version of Teleport is not compatible with MacOS. Please run this script in a Linux machine." + return 1 + fi + echo "${teleportFlavor}-${teleportVersion}-darwin-universal-${packageSuffix}" return 0 fi; - if [ ${OS} != "Linux" ]; then + if [ "${OS}" != "Linux" ]; then echo "Only MacOS and Linux are supported." >&2 return 1 fi; - if [ ${ARCH} = "armv7l" ]; then echo "${teleportFlavor}-${teleportVersion}-linux-arm-bin.tar.gz" - elif [ ${ARCH} = "aarch64" ]; then echo "${teleportFlavor}-${teleportVersion}-linux-arm64-bin.tar.gz" - elif [ ${ARCH} = "x86_64" ]; then echo "${teleportFlavor}-${teleportVersion}-linux-amd64-bin.tar.gz" - elif [ ${ARCH} = "i686" ]; then echo "${teleportFlavor}-${teleportVersion}-linux-386-bin.tar.gz" + if [ ${ARCH} = "armv7l" ]; then echo "${teleportFlavor}-${teleportVersion}-linux-arm-${packageSuffix}" + elif [ ${ARCH} = "aarch64" ]; then echo "${teleportFlavor}-${teleportVersion}-linux-arm64-${packageSuffix}" + elif [ ${ARCH} = "x86_64" ]; then echo "${teleportFlavor}-${teleportVersion}-linux-amd64-${packageSuffix}" + elif [ ${ARCH} = "i686" ]; then echo "${teleportFlavor}-${teleportVersion}-linux-386-${packageSuffix}" else echo "Invalid Linux architecture ${ARCH}." >&2 return 1 @@ -40,12 +47,12 @@ teleportTarballName() { main() { tarballName=$(teleportTarballName) echo "Downloading from ${cdnBaseURL}/${tarballName} and extracting teleport to ${tempDir} ..." - curl --show-error --fail --location ${cdnBaseURL}/${tarballName} | tar xzf - -C ${tempDir} ${teleportFlavor}/teleport + curl --show-error --fail --location "${cdnBaseURL}/${tarballName}" | tar xzf - -C "${tempDir}" "${teleportFlavor}/${entrypoint}" - mkdir -p ${tempDir}/bin - mv ${tempDir}/${teleportFlavor}/teleport ${tempDir}/bin/teleport - echo "> ${tempDir}/bin/teleport ${teleportArgs} $@" - {{.TeleportCommandPrefix}} ${tempDir}/bin/teleport ${teleportArgs} $@ && echo $successMessage + mkdir -p "${tempDir}/bin" + mv "${tempDir}/${teleportFlavor}/${entrypoint}" "${tempDir}/bin/${entrypoint}" + echo "> ${tempDir}/bin/${entrypoint} ${entrypointArgs} $@" + {{.TeleportCommandPrefix}} "${tempDir}/bin/${entrypoint}" ${entrypointArgs} $@ && echo "$successMessage" } main $@ diff --git a/lib/web/scripts/oneoff/oneoff_test.go b/lib/web/scripts/oneoff/oneoff_test.go index 963f7d2392f1d..c6da7d96e2e21 100644 --- a/lib/web/scripts/oneoff/oneoff_test.go +++ b/lib/web/scripts/oneoff/oneoff_test.go @@ -69,7 +69,7 @@ func TestOneOffScript(t *testing.T) { BinMktemp: mktempMock.Path, CDNBaseURL: "dummyURL", TeleportVersion: "v13.1.0", - TeleportArgs: "version", + EntrypointArgs: "version", }) require.NoError(t, err) @@ -99,7 +99,7 @@ func TestOneOffScript(t *testing.T) { BinMktemp: mktempMock.Path, CDNBaseURL: testServer.URL, TeleportVersion: "v13.1.0", - TeleportArgs: "version", + EntrypointArgs: "version", SuccessMessage: "Test was a success.", }) require.NoError(t, err) @@ -156,7 +156,7 @@ func TestOneOffScript(t *testing.T) { BinMktemp: mktempMock.Path, CDNBaseURL: testServer.URL, TeleportVersion: "v13.1.0", - TeleportArgs: "version", + EntrypointArgs: "version", SuccessMessage: "Test was a success.", TeleportCommandPrefix: "sudo", binSudo: sudoMock.Path, @@ -215,7 +215,7 @@ func TestOneOffScript(t *testing.T) { BinUname: unameMock.Path, BinMktemp: mktempMock.Path, CDNBaseURL: testServer.URL, - TeleportArgs: "help", + EntrypointArgs: "help", TeleportVersion: "v13.1.0", SuccessMessage: "Test was a success.", }) @@ -293,7 +293,7 @@ func TestOneOffScript(t *testing.T) { BinMktemp: mktempMock.Path, CDNBaseURL: "dummyURL", TeleportVersion: "v13.1.0", - TeleportArgs: "version", + EntrypointArgs: "version", SuccessMessage: "Test was a success.", TeleportFlavor: "../not-teleport", }) @@ -306,7 +306,7 @@ func TestOneOffScript(t *testing.T) { BinMktemp: mktempMock.Path, CDNBaseURL: "dummyURL", TeleportVersion: "v13.1.0", - TeleportArgs: "version", + EntrypointArgs: "version", SuccessMessage: "Test was a success.", TeleportFlavor: "teleport", TeleportCommandPrefix: "rm -rf thing", @@ -343,7 +343,7 @@ func TestOneOffScript(t *testing.T) { BinMktemp: mktempMock.Path, CDNBaseURL: testServer.URL, TeleportVersion: "v13.1.0", - TeleportArgs: "version", + EntrypointArgs: "version", SuccessMessage: "Test was a success.", }) require.NoError(t, err) diff --git a/tool/tctl/common/plugin/entraid.go b/tool/tctl/common/plugin/entraid.go index c537be2680da8..b175b11312665 100644 --- a/tool/tctl/common/plugin/entraid.go +++ b/tool/tctl/common/plugin/entraid.go @@ -399,7 +399,7 @@ func buildScript(proxyPublicAddr string, entraCfg entraArgs) (string, error) { } script, err := oneoff.BuildScript(oneoff.OneOffScriptParams{ - TeleportArgs: strings.Join(argsList, " "), + EntrypointArgs: strings.Join(argsList, " "), SuccessMessage: "Success! You can now go back to the Teleport Web UI to use the integration with Azure.", }) if err != nil {