diff --git a/.drone.yml b/.drone.yml index ea133d76a74f8..88088b6cb1309 100644 --- a/.drone.yml +++ b/.drone.yml @@ -831,8 +831,8 @@ steps: - set -u - export PATH=/Users/build/.cargo/bin:$PATH - mkdir -p ~/build-$DRONE_BUILD_NUMBER-$DRONE_BUILD_CREATED-toolchains - - export RUST_VERSION=$(grep RUST_VERSION $WORKSPACE_DIR/go/src/github.com/gravitational/teleport/build.assets/Dockerfile - | cut -d= -f2) + - export RUST_VERSION=$(make -C $WORKSPACE_DIR/go/src/github.com/gravitational/teleport/build.assets + print-rust-version) - export CARGO_HOME=~/build-$DRONE_BUILD_NUMBER-$DRONE_BUILD_CREATED-toolchains - export RUST_HOME=$CARGO_HOME - rustup toolchain install $RUST_VERSION @@ -841,8 +841,8 @@ steps: - name: Build Mac artifacts commands: - set -u - - export RUST_VERSION=$(grep RUST_VERSION $WORKSPACE_DIR/go/src/github.com/gravitational/teleport/build.assets/Dockerfile - | cut -d= -f2) + - export RUST_VERSION=$(make -C $WORKSPACE_DIR/go/src/github.com/gravitational/teleport/build.assets + print-rust-version) - export CARGO_HOME=~/build-$DRONE_BUILD_NUMBER-$DRONE_BUILD_CREATED-toolchains - export RUST_HOME=$CARGO_HOME - export PATH=~/build-$DRONE_BUILD_NUMBER-$DRONE_BUILD_CREATED-toolchains/go/bin:$CARGO_HOME/bin:/Users/build/.cargo/bin:$PATH @@ -861,8 +861,8 @@ steps: - export PATH=/Users/build/.cargo/bin:$PATH - export CARGO_HOME=~/build-$DRONE_BUILD_NUMBER-$DRONE_BUILD_CREATED-toolchains - export RUST_HOME=$CARGO_HOME - - export RUST_VERSION=$(grep RUST_VERSION $WORKSPACE_DIR/go/src/github.com/gravitational/teleport/build.assets/Dockerfile - | cut -d= -f2) + - export RUST_VERSION=$(make -C $WORKSPACE_DIR/go/src/github.com/gravitational/teleport/build.assets + print-rust-version) - cd $WORKSPACE_DIR/go/src/github.com/gravitational/teleport - rustup override unset - rustup toolchain uninstall $RUST_VERSION @@ -1363,7 +1363,220 @@ steps: ################################################ # Generated using dronegen, do not edit by hand! # Use 'make dronegen' to update. -# Generated at dronegen/tag.go:221 +# Generated at dronegen/tag.go:233 +################################################ + +kind: pipeline +type: kubernetes +name: build-linux-amd64-centos7 +environment: + RUNTIME: go1.17.3 +trigger: + event: + include: + - tag + ref: + include: + - refs/tags/v* + repo: + include: + - gravitational/* +workspace: + path: /go +clone: + disable: true +steps: +- name: Check out code + image: docker:git + commands: + - mkdir -p /go/src/github.com/gravitational/teleport + - cd /go/src/github.com/gravitational/teleport + - git clone https://github.com/gravitational/${DRONE_REPO_NAME}.git . + - git checkout ${DRONE_TAG:-$DRONE_COMMIT} + - mkdir -m 0700 /root/.ssh && echo -n "$GITHUB_PRIVATE_KEY" > /root/.ssh/id_rsa + && chmod 600 /root/.ssh/id_rsa + - ssh-keyscan -H github.com > /root/.ssh/known_hosts 2>/dev/null && chmod 600 /root/.ssh/known_hosts + - git submodule update --init e + - git submodule update --init --recursive webassets || true + - rm -f /root/.ssh/id_rsa + - mkdir -p /go/cache /go/artifacts + - if [[ "${DRONE_TAG}" != "" ]]; then echo "${DRONE_TAG##v}" > /go/.version.txt; + else egrep ^VERSION Makefile | cut -d= -f2 > /go/.version.txt; fi; cat /go/.version.txt + environment: + GITHUB_PRIVATE_KEY: + from_secret: GITHUB_PRIVATE_KEY +- name: Wait for docker + image: docker + commands: + - timeout 30s /bin/sh -c 'while [ ! -S /var/run/docker.sock ]; do sleep 1; done' + volumes: + - name: dockersock + path: /var/run +- name: Build artifacts + image: docker + commands: + - apk add --no-cache make + - chown -R $UID:$GID /go + - cd /go/src/github.com/gravitational/teleport + - make -C build.assets release-amd64-centos7 + environment: + ARCH: amd64 + GID: "1000" + GOCACHE: /go/cache + GOPATH: /go + OS: linux + UID: "1000" + volumes: + - name: dockersock + path: /var/run +- name: Copy artifacts + image: docker + commands: + - cd /go/src/github.com/gravitational/teleport + - find . -maxdepth 1 -iname "teleport*.tar.gz" -print -exec cp {} /go/artifacts + \; + - find e/ -maxdepth 1 -iname "teleport*.tar.gz" -print -exec cp {} /go/artifacts + \; + - export VERSION=$(cat /go/.version.txt) + - mv /go/artifacts/teleport-v$${VERSION}-linux-amd64-bin.tar.gz /go/artifacts/teleport-v$${VERSION}-linux-amd64-centos7-bin.tar.gz + - mv /go/artifacts/teleport-ent-v$${VERSION}-linux-amd64-bin.tar.gz /go/artifacts/teleport-ent-v$${VERSION}-linux-amd64-centos7-bin.tar.gz + - cd /go/artifacts && for FILE in teleport*.tar.gz; do sha256sum $FILE > $FILE.sha256; + done && ls -l +- name: Upload to S3 + image: plugins/s3 + settings: + access_key: + from_secret: AWS_ACCESS_KEY_ID + bucket: + from_secret: AWS_S3_BUCKET + region: us-west-2 + secret_key: + from_secret: AWS_SECRET_ACCESS_KEY + source: /go/artifacts/* + strip_prefix: /go/artifacts/ + target: teleport/tag/${DRONE_TAG##v} +services: +- name: Start Docker + image: docker:dind + privileged: true + volumes: + - name: dockersock + path: /var/run +volumes: +- name: dockersock + temp: {} + +--- +################################################ +# Generated using dronegen, do not edit by hand! +# Use 'make dronegen' to update. +# Generated at dronegen/tag.go:233 +################################################ + +kind: pipeline +type: kubernetes +name: build-linux-amd64-centos7-fips +environment: + RUNTIME: go1.17.3 +trigger: + event: + include: + - tag + ref: + include: + - refs/tags/v* + repo: + include: + - gravitational/* +workspace: + path: /go +clone: + disable: true +steps: +- name: Check out code + image: docker:git + commands: + - mkdir -p /go/src/github.com/gravitational/teleport + - cd /go/src/github.com/gravitational/teleport + - git clone https://github.com/gravitational/${DRONE_REPO_NAME}.git . + - git checkout ${DRONE_TAG:-$DRONE_COMMIT} + - mkdir -m 0700 /root/.ssh && echo -n "$GITHUB_PRIVATE_KEY" > /root/.ssh/id_rsa + && chmod 600 /root/.ssh/id_rsa + - ssh-keyscan -H github.com > /root/.ssh/known_hosts 2>/dev/null && chmod 600 /root/.ssh/known_hosts + - git submodule update --init e + - git submodule update --init --recursive webassets || true + - rm -f /root/.ssh/id_rsa + - mkdir -p /go/cache /go/artifacts + - if [[ "${DRONE_TAG}" != "" ]]; then echo "${DRONE_TAG##v}" > /go/.version.txt; + else egrep ^VERSION Makefile | cut -d= -f2 > /go/.version.txt; fi; cat /go/.version.txt + environment: + GITHUB_PRIVATE_KEY: + from_secret: GITHUB_PRIVATE_KEY +- name: Wait for docker + image: docker + commands: + - timeout 30s /bin/sh -c 'while [ ! -S /var/run/docker.sock ]; do sleep 1; done' + volumes: + - name: dockersock + path: /var/run +- name: Build artifacts + image: docker + commands: + - apk add --no-cache make + - chown -R $UID:$GID /go + - cd /go/src/github.com/gravitational/teleport + - export VERSION=$(cat /go/.version.txt) + - make -C build.assets release-amd64-centos7-fips + environment: + ARCH: amd64 + FIPS: "yes" + GID: "1000" + GOCACHE: /go/cache + GOPATH: /go + OS: linux + UID: "1000" + volumes: + - name: dockersock + path: /var/run +- name: Copy artifacts + image: docker + commands: + - cd /go/src/github.com/gravitational/teleport + - find e/ -maxdepth 1 -iname "teleport*.tar.gz" -print -exec cp {} /go/artifacts + \; + - export VERSION=$(cat /go/.version.txt) + - mv /go/artifacts/teleport-ent-v$${VERSION}-linux-amd64-fips-bin.tar.gz /go/artifacts/teleport-ent-v$${VERSION}-linux-amd64-centos7-fips-bin.tar.gz + - cd /go/artifacts && for FILE in teleport*.tar.gz; do sha256sum $FILE > $FILE.sha256; + done && ls -l +- name: Upload to S3 + image: plugins/s3 + settings: + access_key: + from_secret: AWS_ACCESS_KEY_ID + bucket: + from_secret: AWS_S3_BUCKET + region: us-west-2 + secret_key: + from_secret: AWS_SECRET_ACCESS_KEY + source: /go/artifacts/* + strip_prefix: /go/artifacts/ + target: teleport/tag/${DRONE_TAG##v} +services: +- name: Start Docker + image: docker:dind + privileged: true + volumes: + - name: dockersock + path: /var/run +volumes: +- name: dockersock + temp: {} + +--- +################################################ +# Generated using dronegen, do not edit by hand! +# Use 'make dronegen' to update. +# Generated at dronegen/tag.go:233 ################################################ kind: pipeline @@ -1467,7 +1680,7 @@ volumes: ################################################ # Generated using dronegen, do not edit by hand! # Use 'make dronegen' to update. -# Generated at dronegen/tag.go:221 +# Generated at dronegen/tag.go:233 ################################################ kind: pipeline @@ -1571,7 +1784,7 @@ volumes: ################################################ # Generated using dronegen, do not edit by hand! # Use 'make dronegen' to update. -# Generated at dronegen/tag.go:221 +# Generated at dronegen/tag.go:233 ################################################ kind: pipeline @@ -1678,7 +1891,7 @@ volumes: ################################################ # Generated using dronegen, do not edit by hand! # Use 'make dronegen' to update. -# Generated at dronegen/tag.go:363 +# Generated at dronegen/tag.go:375 ################################################ kind: pipeline @@ -1810,7 +2023,7 @@ volumes: ################################################ # Generated using dronegen, do not edit by hand! # Use 'make dronegen' to update. -# Generated at dronegen/tag.go:363 +# Generated at dronegen/tag.go:375 ################################################ kind: pipeline @@ -1939,7 +2152,7 @@ volumes: ################################################ # Generated using dronegen, do not edit by hand! # Use 'make dronegen' to update. -# Generated at dronegen/tag.go:363 +# Generated at dronegen/tag.go:375 ################################################ kind: pipeline @@ -2057,7 +2270,7 @@ volumes: ################################################ # Generated using dronegen, do not edit by hand! # Use 'make dronegen' to update. -# Generated at dronegen/tag.go:363 +# Generated at dronegen/tag.go:375 ################################################ kind: pipeline @@ -2172,7 +2385,7 @@ volumes: ################################################ # Generated using dronegen, do not edit by hand! # Use 'make dronegen' to update. -# Generated at dronegen/tag.go:221 +# Generated at dronegen/tag.go:233 ################################################ kind: pipeline @@ -2276,7 +2489,7 @@ volumes: ################################################ # Generated using dronegen, do not edit by hand! # Use 'make dronegen' to update. -# Generated at dronegen/tag.go:363 +# Generated at dronegen/tag.go:375 ################################################ kind: pipeline @@ -2408,7 +2621,7 @@ volumes: ################################################ # Generated using dronegen, do not edit by hand! # Use 'make dronegen' to update. -# Generated at dronegen/tag.go:363 +# Generated at dronegen/tag.go:375 ################################################ kind: pipeline @@ -2598,8 +2811,8 @@ steps: - set -u - export PATH=/Users/build/.cargo/bin:$PATH - mkdir -p ~/build-$DRONE_BUILD_NUMBER-$DRONE_BUILD_CREATED-toolchains - - export RUST_VERSION=$(grep RUST_VERSION $WORKSPACE_DIR/go/src/github.com/gravitational/teleport/build.assets/Dockerfile - | cut -d= -f2) + - export RUST_VERSION=$(make -C $WORKSPACE_DIR/go/src/github.com/gravitational/teleport/build.assets + print-rust-version) - export CARGO_HOME=~/build-$DRONE_BUILD_NUMBER-$DRONE_BUILD_CREATED-toolchains - export RUST_HOME=$CARGO_HOME - rustup toolchain install $RUST_VERSION @@ -2608,8 +2821,8 @@ steps: - name: Build Mac release artifacts commands: - set -u - - export RUST_VERSION=$(grep RUST_VERSION $WORKSPACE_DIR/go/src/github.com/gravitational/teleport/build.assets/Dockerfile - | cut -d= -f2) + - export RUST_VERSION=$(make -C $WORKSPACE_DIR/go/src/github.com/gravitational/teleport/build.assets + print-rust-version) - export CARGO_HOME=~/build-$DRONE_BUILD_NUMBER-$DRONE_BUILD_CREATED-toolchains - export RUST_HOME=$CARGO_HOME - export PATH=~/build-$DRONE_BUILD_NUMBER-$DRONE_BUILD_CREATED-toolchains/go/bin:$CARGO_HOME/bin:/Users/build/.cargo/bin:$PATH @@ -2652,8 +2865,8 @@ steps: - export PATH=/Users/build/.cargo/bin:$PATH - export CARGO_HOME=~/build-$DRONE_BUILD_NUMBER-$DRONE_BUILD_CREATED-toolchains - export RUST_HOME=$CARGO_HOME - - export RUST_VERSION=$(grep RUST_VERSION $WORKSPACE_DIR/go/src/github.com/gravitational/teleport/build.assets/Dockerfile - | cut -d= -f2) + - export RUST_VERSION=$(make -C $WORKSPACE_DIR/go/src/github.com/gravitational/teleport/build.assets + print-rust-version) - cd $WORKSPACE_DIR/go/src/github.com/gravitational/teleport - rustup override unset - rustup toolchain uninstall $RUST_VERSION @@ -2950,7 +3163,7 @@ steps: ################################################ # Generated using dronegen, do not edit by hand! # Use 'make dronegen' to update. -# Generated at dronegen/tag.go:221 +# Generated at dronegen/tag.go:233 ################################################ kind: pipeline @@ -3054,7 +3267,7 @@ volumes: ################################################ # Generated using dronegen, do not edit by hand! # Use 'make dronegen' to update. -# Generated at dronegen/tag.go:221 +# Generated at dronegen/tag.go:233 ################################################ kind: pipeline @@ -3158,7 +3371,7 @@ volumes: ################################################ # Generated using dronegen, do not edit by hand! # Use 'make dronegen' to update. -# Generated at dronegen/tag.go:363 +# Generated at dronegen/tag.go:375 ################################################ kind: pipeline @@ -3276,7 +3489,7 @@ volumes: ################################################ # Generated using dronegen, do not edit by hand! # Use 'make dronegen' to update. -# Generated at dronegen/tag.go:363 +# Generated at dronegen/tag.go:375 ################################################ kind: pipeline @@ -3394,7 +3607,7 @@ volumes: ################################################ # Generated using dronegen, do not edit by hand! # Use 'make dronegen' to update. -# Generated at dronegen/tag.go:363 +# Generated at dronegen/tag.go:375 ################################################ kind: pipeline @@ -3526,7 +3739,7 @@ volumes: ################################################ # Generated using dronegen, do not edit by hand! # Use 'make dronegen' to update. -# Generated at dronegen/tag.go:363 +# Generated at dronegen/tag.go:375 ################################################ kind: pipeline @@ -3658,7 +3871,7 @@ volumes: ################################################ # Generated using dronegen, do not edit by hand! # Use 'make dronegen' to update. -# Generated at dronegen/tag.go:221 +# Generated at dronegen/tag.go:233 ################################################ kind: pipeline @@ -4543,6 +4756,6 @@ volumes: name: drone-s3-debrepo-pvc --- kind: signature -hmac: 44e486f592ccf4d39b496be361bc2592045f7759d9dc07f9cbe85b89ce43024d +hmac: 0b4670e1eb0d589bac096a2458c18d25d57439928442e5a4253e1e336e7f08c1 ... diff --git a/api/types/desktop.go b/api/types/desktop.go index 97ee6ea7d20b3..f86870bf48131 100644 --- a/api/types/desktop.go +++ b/api/types/desktop.go @@ -150,3 +150,13 @@ func (d *WindowsDesktopV3) LabelsString() string { func (d *WindowsDesktopV3) GetDomain() string { return d.Spec.Domain } + +// Origin returns the origin value of the resource. +func (d *WindowsDesktopV3) Origin() string { + return d.Metadata.Labels[OriginLabel] +} + +// SetOrigin sets the origin value of the resource. +func (d *WindowsDesktopV3) SetOrigin(o string) { + d.Metadata.Labels[OriginLabel] = o +} diff --git a/build.assets/Dockerfile b/build.assets/Dockerfile index 41e0745486fff..714bac16b18c4 100644 --- a/build.assets/Dockerfile +++ b/build.assets/Dockerfile @@ -68,36 +68,6 @@ ARG GID RUN (groupadd ci --gid=$GID -o && useradd ci --uid=$UID --gid=$GID --create-home --shell=/bin/sh && \ mkdir -p -m0700 /var/lib/teleport && chown -R ci /var/lib/teleport) -# Install Rust -# -# Rust installation based on official rust image Dockerfile here: -# https://github.com/rust-lang/docker-rust/blob/master/1.56.0/bullseye/Dockerfile -# -# The original Rust docker image uses a script to install `rustup`, and from -# there rustc and associated tools. -# -# Rather than execute an arbitrary `rustup` installation script, we are cherry- -# picking the appropriate files off the official docker image and then installing -# the extra tooling/targets we need. - - ENV RUSTUP_HOME=/usr/local/rustup \ - CARGO_HOME=/usr/local/cargo \ - PATH=/usr/local/cargo/bin:$PATH \ - RUST_VERSION=1.56.1 - -COPY --from=rust:1.56.1 /usr/local/rustup /usr/local/rustup -COPY --from=rust:1.56.1 /usr/local/cargo /usr/local/cargo -RUN set -eux \ - rustup --version; \ - cargo --version; \ - rustup component add --toolchain 1.56.1-x86_64-unknown-linux-gnu rustfmt clippy; \ - chmod -R a+w $RUSTUP_HOME $CARGO_HOME; \ - rustup target add i686-unknown-linux-gnu; \ - rustup target add arm-unknown-linux-gnueabihf; \ - rustup target add aarch64-unknown-linux-gnu; \ - rustup target list | grep installed; \ - rustc --version; - # Install etcd. RUN (curl -L https://github.com/coreos/etcd/releases/download/v3.3.9/etcd-v3.3.9-linux-amd64.tar.gz | tar -xz && \ cp etcd-v3.3.9-linux-amd64/etcd* /bin/) @@ -163,5 +133,26 @@ RUN make -C /opt/pam_teleport install ENV SOFTHSM2_PATH "/usr/lib/softhsm/libsofthsm2.so" +# Install Rust +ARG RUST_VERSION +ENV RUSTUP_HOME=/usr/local/rustup \ + CARGO_HOME=/usr/local/cargo \ + PATH=/usr/local/cargo/bin:$PATH \ + RUST_VERSION=$RUST_VERSION + +RUN mkdir -p $RUSTUP_HOME && chmod a+w $RUSTUP_HOME && \ + mkdir -p $CARGO_HOME/registry && chmod -R a+w $CARGO_HOME + +USER ci +RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain $RUST_VERSION && \ + rustup --version && \ + cargo --version && \ + rustc --version && \ + rustup component add --toolchain $RUST_VERSION-x86_64-unknown-linux-gnu rustfmt clippy && \ + rustup target add i686-unknown-linux-gnu && \ + rustup target add arm-unknown-linux-gnueabihf && \ + rustup target add aarch64-unknown-linux-gnu && \ + cargo install cbindgen + VOLUME ["/go/src/github.com/gravitational/teleport"] EXPOSE 6600 2379 2380 diff --git a/build.assets/Dockerfile-arm b/build.assets/Dockerfile-arm index ea4c723e198a1..d98262a06a9a1 100644 --- a/build.assets/Dockerfile-arm +++ b/build.assets/Dockerfile-arm @@ -1,6 +1,8 @@ ARG RUNTIME FROM quay.io/gravitational/teleport-buildbox:$RUNTIME +USER root + RUN apt-get -y update && \ apt-get -y install gcc-arm-linux-gnueabihf gcc-aarch64-linux-gnu && \ apt-get -y autoclean && apt-get -y clean diff --git a/build.assets/Dockerfile-centos7 b/build.assets/Dockerfile-centos7 new file mode 100644 index 0000000000000..5311dd53a99cd --- /dev/null +++ b/build.assets/Dockerfile-centos7 @@ -0,0 +1,60 @@ +FROM centos:7 + +ENV LANGUAGE=en_US.UTF-8 \ + LANG=en_US.UTF-8 \ + LC_ALL=en_US.UTF-8 \ + LC_CTYPE=en_US.UTF-8 + +ARG RUNTIME +ARG RUST_VERSION + +ARG UID +ARG GID +RUN (groupadd ci --gid=$GID -o && useradd ci --uid=$UID --gid=$GID --create-home --shell=/bin/sh && \ + mkdir -p -m0700 /var/lib/teleport && chown -R ci /var/lib/teleport) + +# Install dev tools (make, etc) and a Perl package needed to build OpenSSL. +RUN yum groupinstall -y "Development Tools" +RUN yum install -y pam-devel net-tools tree git zip libatomic perl-IPC-Cmd && \ + yum clean all + +# Install etcd. +RUN (curl -L https://github.com/coreos/etcd/releases/download/v3.3.9/etcd-v3.3.9-linux-amd64.tar.gz | tar -xz && \ + cp etcd-v3.3.9-linux-amd64/etcd* /bin/) + +# Install Go. +RUN mkdir -p /opt && cd /opt && curl https://storage.googleapis.com/golang/$RUNTIME.linux-amd64.tar.gz | tar xz && \ + mkdir -p /go/src/github.com/gravitational/teleport && \ + chmod a+w /go && \ + chmod a+w /var/lib && \ + /opt/go/bin/go version + +# Install PAM module and policies for testing. +COPY pam/ /opt/pam_teleport/ +RUN make -C /opt/pam_teleport install + +# Install Rust. +ENV RUSTUP_HOME=/usr/local/rustup \ + CARGO_HOME=/usr/local/cargo \ + PATH=/usr/local/cargo/bin:$PATH \ + RUST_VERSION=$RUST_VERSION + +RUN mkdir -p $RUSTUP_HOME && chmod a+w $RUSTUP_HOME && \ + mkdir -p $CARGO_HOME/registry && chmod -R a+w $CARGO_HOME + +RUN chmod a-w / + +USER ci +RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain $RUST_VERSION && \ + rustup --version && \ + cargo --version && \ + rustc --version && \ + rustup component add --toolchain $RUST_VERSION-x86_64-unknown-linux-gnu rustfmt clippy && \ + cargo install cbindgen + +ENV GOPATH="/go" \ + GOROOT="/opt/go" \ + PATH="/opt/bin:$PATH:/opt/go/bin:/go/bin:/go/src/github.com/gravitational/teleport/build" + +VOLUME ["/go/src/github.com/gravitational/teleport"] +EXPOSE 6600 2379 2380 diff --git a/build.assets/Dockerfile-centos7-fips b/build.assets/Dockerfile-centos7-fips new file mode 100644 index 0000000000000..9c44974b6631c --- /dev/null +++ b/build.assets/Dockerfile-centos7-fips @@ -0,0 +1,69 @@ +FROM centos:7 + +ENV LANGUAGE=en_US.UTF-8 \ + LANG=en_US.UTF-8 \ + LC_ALL=en_US.UTF-8 \ + LC_CTYPE=en_US.UTF-8 + +ARG RUST_VERSION +ARG BORINGCRYPTO_RUNTIME +ARG GO_BOOTSTRAP_RUNTIME=go1.9.7 + +ARG UID +ARG GID +RUN (groupadd ci --gid=$GID -o && useradd ci --uid=$UID --gid=$GID --create-home --shell=/bin/sh && \ + mkdir -p -m0700 /var/lib/teleport && chown -R ci /var/lib/teleport) + +# Install dev tools (make, etc) and a Perl package needed to build OpenSSL. +RUN yum groupinstall -y "Development Tools" +RUN yum install -y pam-devel net-tools tree git zip libatomic perl-IPC-Cmd && \ + yum clean all + +# Install etcd. +RUN (curl -L https://github.com/coreos/etcd/releases/download/v3.3.9/etcd-v3.3.9-linux-amd64.tar.gz | tar -xz && \ + cp etcd-v3.3.9-linux-amd64/etcd* /bin/) + +# BoringCrypto (unlike regular Go) requires glibc 2.14, so we have to build from source. +# 1) Install older binary Go runtime for bootstrapping +# 2) Get source for the correct Go boringcrypto runtime and compile it with Go bootstrap runtime +# 3) Erase Go bootstrap runtime and create build directories +# 4) Print compiled Go version +RUN mkdir -p /go-bootstrap && cd /go-bootstrap && curl https://dl.google.com/go/${GO_BOOTSTRAP_RUNTIME}.linux-amd64.tar.gz | tar xz && \ + mkdir -p /opt && cd /opt && curl https://go-boringcrypto.storage.googleapis.com/${BORINGCRYPTO_RUNTIME}.src.tar.gz | tar xz && \ + cd /opt/go/src && GOROOT_BOOTSTRAP=/go-bootstrap/go ./make.bash && \ + rm -rf /go-bootstrap && \ + mkdir -p /go/src/github.com/gravitational/teleport && \ + chmod a+w /go && \ + chmod a+w /var/lib && \ + chmod a-w / && \ + /opt/go/bin/go version + +# Install PAM module and policies for testing. +COPY pam/ /opt/pam_teleport/ +RUN make -C /opt/pam_teleport install + +# Install Rust. +ENV RUSTUP_HOME=/usr/local/rustup \ + CARGO_HOME=/usr/local/cargo \ + PATH=/usr/local/cargo/bin:$PATH \ + RUST_VERSION=$RUST_VERSION + +RUN mkdir -p $RUSTUP_HOME && chmod a+w $RUSTUP_HOME && \ + mkdir -p $CARGO_HOME/registry && chmod -R a+w $CARGO_HOME + +RUN chmod a-w / + +USER ci +RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain $RUST_VERSION && \ + rustup --version && \ + cargo --version && \ + rustc --version && \ + rustup component add --toolchain $RUST_VERSION-x86_64-unknown-linux-gnu rustfmt clippy && \ + cargo install cbindgen + +ENV GOPATH="/go" \ + GOROOT="/opt/go" \ + PATH="/opt/bin:$PATH:/opt/go/bin:/go/bin:/go/src/github.com/gravitational/teleport/build" + +VOLUME ["/go/src/github.com/gravitational/teleport"] +EXPOSE 6600 2379 2380 diff --git a/build.assets/Makefile b/build.assets/Makefile index 24e8316c83326..1204350d45322 100644 --- a/build.assets/Makefile +++ b/build.assets/Makefile @@ -17,6 +17,7 @@ TEST_KUBE ?= OS ?= linux ARCH ?= amd64 RUNTIME ?= go1.17.3 +RUST_VERSION ?= 1.56.1 BORINGCRYPTO_RUNTIME=$(RUNTIME)b7 LIBBPF_VERSION ?= 0.3.1 @@ -30,6 +31,8 @@ GOGO_PROTO_TAG ?= v1.3.2 BUILDBOX=quay.io/gravitational/teleport-buildbox:$(RUNTIME) BUILDBOX_FIPS=quay.io/gravitational/teleport-buildbox-fips:$(RUNTIME) BUILDBOX_CENTOS6=quay.io/gravitational/teleport-buildbox-centos6:$(RUNTIME) +BUILDBOX_CENTOS7=quay.io/gravitational/teleport-buildbox-centos7:$(RUNTIME) +BUILDBOX_CENTOS7_FIPS=quay.io/gravitational/teleport-buildbox-centos7-fips:$(RUNTIME) BUILDBOX_ARM=quay.io/gravitational/teleport-buildbox-arm:$(RUNTIME) BUILDBOX_ARM_FIPS=quay.io/gravitational/teleport-buildbox-arm-fips:$(RUNTIME) @@ -107,6 +110,7 @@ buildbox: --build-arg UID=$(UID) \ --build-arg GID=$(GID) \ --build-arg RUNTIME=$(RUNTIME) \ + --build-arg RUST_VERSION=$(RUST_VERSION) \ --build-arg PROTOC_VER=$(PROTOC_VER) \ --build-arg GOGO_PROTO_TAG=$(GOGO_PROTO_TAG) \ --build-arg PROTOC_PLATFORM=$(PROTOC_PLATFORM) \ @@ -147,6 +151,34 @@ buildbox-centos6: # CentOS 6 FIPS builds were removed in Teleport 7.0 # https://github.com/gravitational/teleport/issues/7207 +# +# Builds a Docker buildbox for CentOS 7 builds +# +.PHONY:buildbox-centos7 +buildbox-centos7: + @if [[ $${DRONE} == "true" ]] && ! docker inspect --type=image $(BUILDBOX_CENTOS7) 2>&1 >/dev/null; then docker pull $(BUILDBOX_CENTOS7) || true; fi; + docker build \ + --build-arg UID=$(UID) \ + --build-arg GID=$(GID) \ + --build-arg RUNTIME=$(RUNTIME) \ + --build-arg RUST_VERSION=$(RUST_VERSION) \ + --cache-from $(BUILDBOX_CENTOS7) \ + --tag $(BUILDBOX_CENTOS7) -f Dockerfile-centos7 . + +# +# Builds a Docker buildbox for CentOS 7 FIPS builds +# +.PHONY:buildbox-centos7-fips +buildbox-centos7-fips: + @if [[ $${DRONE} == "true" ]] && ! docker inspect --type=image $(BUILDBOX_CENTOS7_FIPS) 2>&1 >/dev/null; then docker pull $(BUILDBOX_CENTOS7_FIPS) || true; fi; + docker build \ + --build-arg UID=$(UID) \ + --build-arg GID=$(GID) \ + --build-arg BORINGCRYPTO_RUNTIME=$(BORINGCRYPTO_RUNTIME) \ + --build-arg RUST_VERSION=$(RUST_VERSION) \ + --cache-from $(BUILDBOX_CENTOS7_FIPS) \ + --tag $(BUILDBOX_CENTOS7_FIPS) -f Dockerfile-centos7-fips . + # # Builds a Docker buildbox for ARMv7/ARM64 builds # ARM buildboxes use a regular Teleport buildbox as a base which already has a user @@ -287,6 +319,14 @@ release-arm64: buildbox-arm release-amd64-centos6: buildbox-centos6 $(MAKE) release-centos6 ARCH=amd64 +.PHONY: release-amd64-centos7 +release-amd64-centos7: buildbox-centos7 + $(MAKE) release-centos7 ARCH=amd64 + +.PHONY: release-amd64-centos7-fips +release-amd64-centos7-fips: buildbox-centos7-fips + $(MAKE) release-centos7-fips ARCH=amd64 FIPS=yes + # # Create a Teleport FIPS package using the build container. # This is a special case because it only builds and packages the Enterprise FIPS binaries, no OSS. @@ -300,12 +340,30 @@ release-fips: buildbox-fips # # Create a Teleport package for CentOS 6 using the build container. +# DELETE IN 9.0 (zmb3) # .PHONY:release-centos6 release-centos6: buildbox-centos6 docker run $(DOCKERFLAGS) -i $(NOROOT) $(BUILDBOX_CENTOS6) \ /usr/bin/make release -e ADDFLAGS="$(ADDFLAGS)" OS=$(OS) ARCH=$(ARCH) RUNTIME=$(RUNTIME) REPRODUCIBLE=no +# +# Create a Teleport package for CentOS 7 using the build container. +# +.PHONY:release-centos7 +release-centos7: buildbox-centos7 + docker run $(DOCKERFLAGS) -i $(NOROOT) $(BUILDBOX_CENTOS7) \ + /usr/bin/make release -e ADDFLAGS="$(ADDFLAGS)" OS=$(OS) ARCH=$(ARCH) RUNTIME=$(RUNTIME) REPRODUCIBLE=no + +# +# Create a Teleport FIPS package for CentOS 7 using the build container. +# This only builds and packages enterprise FIPS binaries, no OSS. +# +.PHONY:release-centos7-fips +release-centos7-fips: + docker run $(DOCKERFLAGS) -i $(NOROOT) $(BUILDBOX_CENTOS7_FIPS) \ + /usr/bin/make -C e release -e ADDFLAGS="$(ADDFLAGS)" OS=$(OS) ARCH=$(ARCH) RUNTIME=$(RUNTIME) FIPS=yes VERSION=$(VERSION) GITTAG=v$(VERSION) REPRODUCIBLE=no + # # Create a Windows Teleport package using the build container. # @@ -325,18 +383,33 @@ release-windows-unsigned: buildbox # # Run docs tester to detect problems. # -.PHONY: docsbox +.PHONY:docsbox docsbox: if ! docker inspect --type=image $(DOCSBOX) 2>&1 >/dev/null; then docker pull $(DOCSBOX) || true; fi -.PHONY: test-docs -test-docs: DOCS_VERSION := $(shell grep -E ^VERSION $(MAKEFILE_ROOT_DIR)/Makefile | cut -d= -f2 | cut -d. -f1-2) +.PHONY:test-docs test-docs: docsbox - docker run -i $(NOROOT) -v $$(pwd)/..:/src/content/$(DOCS_VERSION) $(DOCSBOX) \ + docker run --platform=linux/amd64 -i $(NOROOT) -v $$(pwd)/..:/src/content $(DOCSBOX) \ /bin/sh -c "yarn markdown-lint-external-links" -# build-centos6-assets builds assets needed by CentOS 6 in a container. -.PHONY: build-centos6-assets +# +# Builds assets needed by CentOS 6 in a container. +# +.PHONY:build-centos6-assets build-centos6-assets: docker build -t buildbox-centos6-assets -f Dockerfile-centos6-assets . docker run -v $$(pwd):/centos6.assets -it buildbox-centos6-assets cp /centos6-assets.tar.gz /centos6.assets + +# +# Print the Go version used to build Teleport. +# +.PHONY:print-go-version +print-go-version: + @echo $(RUNTIME) + +# +# Print the Rust version used to build Teleport. +# +.PHONY:print-rust-version +print-rust-version: + @echo $(RUST_VERSION) diff --git a/constants.go b/constants.go index 6acb8ad1ddbd3..92fb85b936a16 100644 --- a/constants.go +++ b/constants.go @@ -739,3 +739,6 @@ const UserSingleUseCertTTL = time.Minute // StandardHTTPSPort is the default port used for the https URI scheme, // cf. RFC 7230 § 2.7.2. const StandardHTTPSPort = 443 + +// StandardRDPPort is the default port used for RDP. +const StandardRDPPort = 3389 diff --git a/docs/img/desktop-access/ad-new-user.png b/docs/img/desktop-access/ad-new-user.png new file mode 100644 index 0000000000000..945daea915b58 Binary files /dev/null and b/docs/img/desktop-access/ad-new-user.png differ diff --git a/docs/img/desktop-access/apply-gpo.png b/docs/img/desktop-access/apply-gpo.png new file mode 100644 index 0000000000000..f0f55026f1fb2 Binary files /dev/null and b/docs/img/desktop-access/apply-gpo.png differ diff --git a/docs/img/desktop-access/create-and-link-gpo.png b/docs/img/desktop-access/create-and-link-gpo.png new file mode 100644 index 0000000000000..16fa327b851e8 Binary files /dev/null and b/docs/img/desktop-access/create-and-link-gpo.png differ diff --git a/docs/img/desktop-access/deny-interactive-login.png b/docs/img/desktop-access/deny-interactive-login.png new file mode 100644 index 0000000000000..d0022b8489347 Binary files /dev/null and b/docs/img/desktop-access/deny-interactive-login.png differ diff --git a/docs/img/desktop-access/remove-authenticated.png b/docs/img/desktop-access/remove-authenticated.png new file mode 100644 index 0000000000000..92e2bff49ddce Binary files /dev/null and b/docs/img/desktop-access/remove-authenticated.png differ diff --git a/docs/img/desktop-access/select-desktop.png b/docs/img/desktop-access/select-desktop.png new file mode 100644 index 0000000000000..f299a4ffff871 Binary files /dev/null and b/docs/img/desktop-access/select-desktop.png differ diff --git a/docs/pages/desktop-access/getting-started.mdx b/docs/pages/desktop-access/getting-started.mdx index 868b01e3a7406..bb4cc764baaa6 100644 --- a/docs/pages/desktop-access/getting-started.mdx +++ b/docs/pages/desktop-access/getting-started.mdx @@ -22,67 +22,89 @@ This guide requires you to have: - An Active Directory domain, configured for LDAPS (Teleport requires an encrypted LDAP connection) - Access to a Domain Controller -- LDAP credentials for Active Directory (usually the same credentials you use - to log into the Domain Controller) - An existing Teleport cluster and user, version 8.0 or newer - - see [Teleport Getting Started](../getting-started.mdx) if you're new to Teleport + - See [Teleport Getting Started](../getting-started.mdx) if you're new to Teleport - A Linux server to run the Teleport Desktop Access service on - - you can reuse an existing server running any other Teleport instance + - You can reuse an existing server running any other Teleport instance -## Step 1/3. Configure Teleport +## Step 1/6: Create a User, Group, and Group Policy Object + +Teleport requires a service account to connect to your Active Directory domain. We recommend creating a dedicated service account with restrictive permissions +(described below) for maximal security. + +### Create a User and Group + +From the start menu of your Domain Controller, search for “Active Directory Users and Computers” and open the corresponding program. + +Find your domain in the side menu, and right click the container you want the service account to live in (typically Users). From the context +menu, select `New > User` and fill out the fields for your Teleport LDAP Service Account. For the "User logon name" field, you can use `svc-teleport` +On the password screen, unselect “User must change password at next login” and select “Password never expires”. Make a note of what values you put +for user logon name and password, which will be used in your `teleport.yaml` configuration file later, and then click through the rest of the wizard. - Prior to v8.0, the Teleport CA was not compatible with Windows logins. If - you're setting up Desktop Access in an existing cluster created before v8.0, - you must first perform a [CA rotation](../setup/operations/ca-rotation.mdx) - in order to resolve this. + By default, domain users have Read and List permissions throughout Active Directory. If you have configured + any restrictions to Domain Users, you may have to adjust which groups this account is a member of. -First, we need to enable Desktop Access in Teleport. To do this, add the -following section in `teleport.yaml` on your Linux server: +
+ ![Create Service Account](../../img/desktop-access/ad-new-user.png) +
-```yaml -windows_desktop_service: - enabled: yes - # This is the address that windows_desktop_service will listen on. - listen_addr: "0.0.0.0:3028" - # (optional) This is the address that windows_desktop_service will advertise - # to the rest of Teleport for incoming connections. Only proxy_service should - # connect to windows_desktop_service, users connect to the proxy's web UI - # instead. - public_addr: "desktop-access.example.com:3028" - ldap: - # Address of the Domain Controller for LDAP connections. Usually, this - # address will use port 389, like: domain-controller.example.com:389. - addr: '$LDAP_SERVER_ADDRESS' - # Active Directory domain name you are connecting to. - domain: '$LDAP_DOMAIN_NAME' - # LDAP username for authentication. This username must include the domain - # NetBIOS name. - # - # For example, if your domain is "example.com", the NetBIOS name for it is - # likely "EXAMPLE". When connecting as the "Administrator" user, you should - # use the format: "EXAMPLE\Administrator". - username: '$LDAP_USERNAME' - # Plain text file containing the LDAP password for authentication. - # This is usually the same password you use to login to the Domain Controller. - password_file: /var/lib/ldap-pass -``` +Now right click the same container you did before, and select `New > Group`. Choose a Group name like `svc-teleport-group` and ensure that “Global” +is selected for “Group scope”, and “Security” for “Group type”, then hit "OK". Make note of the Group name you chose, as it will +be used later for the PowerShell variable `$TeleportLDAPGroupName`. -After updating `teleport.yaml`, start Teleport as usual using `teleport start`. +Finally, add the User to the Group you just made by right clicking the User and selecting `Add to group`. Type the name of the Group, and confirm correctness +by clicking the “Check Names” button, then click “OK”. You should see a dialog saying “The Add to Group operation was successfully completed.” + +### Create and Apply Group Policy Object + +Next, open the "Start" menu and run "Group Policy Management". On the left pane, navigate to `$FOREST > Domains > $DOMAIN`, +selecting your forest and domain names respectively. Right click `$DOMAIN` and select `Create a GPO in this domain, and Link it here...` from the context menu. +Give your GPO a name like `svc-teleport-gpo` and ensure "(none)" is selected for "Source Starter GPO", then hit "OK". + +
+ ![Create and Link GPO](../../img/desktop-access/create-and-link-gpo.png) +
+ +Now, in the left pane navigate to `$FOREST > Domains > $DOMAIN > Group Policy Objects` and click on the GPO you just created (`svc-teleport-gpo`). In the "Scope" tab +under "Security Filtering", select `Authenticated Users` and then click the "Remove" button and hit "OK". + +
+ ![Remove Authenticated](../../img/desktop-access/remove-authenticated.png) +
+ +The "Remove" step prevents the GPO from being applied to all authenticated users in this domain, since we only want it to apply to members +of the security Group created above (`svc-teleport-group`), of which only the User created above is a member (`svc-teleport`). + +To make that the case, in the same "Security Filtering" pane select "Add...", and enter the name of the Group (`svc-teleport-group`) +in the resultant popup, clicking "Check Names" to confirm the spelling before pressing "OK". After this step, only the `svc-teleport-group` +should be displayed in the "Security Filtering" pane: + +
+ ![Apply GPO](../../img/desktop-access/apply-gpo.png) +
-## Step 2/3. Configure Group Policy to allow Teleport connections +## Step 2/6. Configure Group Policy to allow Teleport connections {/* TODO: script this using PowerShell */} -Next, we need to configure Active Directory to trust Teleport for user +Next, we need to configure our Group Policy Object to trust Teleport for user authentication and allow the certificate-based mechanism that Teleport uses under the hood (smart cards). -Get the Teleport user CA certificate: + + The following step requires an existing cluster. If you don't already have a Teleport cluster up and running, + see our general [Getting Started](https://goteleport.com/docs/getting-started/) guide. + + +Get the Teleport user CA certificate by running: ``` $ tctl auth export --type=windows > user-ca.cer @@ -90,10 +112,8 @@ $ tctl auth export --type=windows > user-ca.cer Transfer the `user-ca.cer` file to your Domain Controller. -Log into your Domain Controller open "Start" menu and run "Group Policy -Management". On the left pane, navigate to `$FOREST > Domains > $DOMAIN`, -selecting your forest and domain names respectively. Right click on "Default -Domain Policy" and select "Edit...". +Then, on your Domain Controller, again open the "Group Policy Management" window. On the left pane, navigate to +`$FOREST > Domains > $DOMAIN > Group Policy Objects`, right click on the Group Policy Object you made in step 1 (`svc-teleport-gpo`), and select "Edit...". ### Import Teleport CA @@ -110,6 +130,53 @@ Click through the wizard, selecting your CA file. ![Import Teleport CA](../../img/desktop-access/ca.png) +### Enable the Smart Card service + +Teleport performs certificate based authentication by emulating a smart card. +To enable the smart card service, select: + +```text +Computer Configuration > Policies > Windows Settings > Security Settings > System Services +``` + +Double click on `Smart Card`, select `Define this policy setting` and switch to +`Automatic`. Click "OK". + +
+ ![Enable Smartcard](../../img/desktop-access/smartcard.png) +
+ +### Open firewall to inbound RDP connections + +Select: + +```text +Computer Configuration > Policies > Windows Settings > Security Settings > Windows Firewall with Advanced Security (x2) +``` + +Right click on `Inbound Rules` and select `New Rule...`. Under `Predefined` +select `Remote Desktop`. Only select the rule for `User Mode (TCP-in)`. On the +next screen, select `Allow the connection` and finish. + +### Deny interactive login + +Select: + +```text +Computer Configuration \ Policies \ Windows Settings \ Security Settings \ Local Policies \ User Rights Assignment +``` + +Double click `Deny log on locally` and in the popup, check "Define these policy settings". Then click "Add User or Group...", "Browse ...", enter the name +of the group you created above (`svc-teleport-group`) and hit "Check Names", select your Group, and then hit "OK" on all the windows. (If the system won't let you hit "OK" after +clicking "Check Names", just hit "Cancel" twice, re-click "Add User or Group", "Browse...", and keep trying again until it does). + +Repeat the process from above for `Deny log on through Remote Desktop Services`. + +
+ ![Deny Interactive Login](../../img/desktop-access/deny-interactive-login.png) +
+ + ### Allow remote RDP connections Next, select: @@ -119,7 +186,7 @@ Computer Configuration > Policies > Administrative Templates > Windows Component ``` Right click on `Allow users to connect remotely by using Remote Desktop -Services` and select "Edit". Select "Enable" and "OK". +Services` and select "Edit". Select "Enabled" and "OK".
![Enable RDP](../../img/desktop-access/rdp.png) @@ -135,45 +202,166 @@ Computer Configuration > Policies > Administrative Templates > Windows Component Right click `Require user authentication for remote connections by using Network Level Authentication`, edit, select **"Disable"** and "OK". -### Enable the Smart Card service -Teleport performs certificate based authentication by emulating a smart card. -To enable the smart card service, select: +## Step 3/6. Configure Group to allow Teleport connections + +Next, complete the service account's configuration by giving its corresponding Group the minimum necessary permissions for Teleport to work. +First, open a PowerShell terminal and figure out the `$DomainDN` and `$TeleportLDAPGroupName` variables you'll need to use: + +`$DomainDN` is the +[distinguished name](https://docs.microsoft.com/en-us/previous-versions/windows/desktop/ldap/distinguished-names) representation of your Domain Controller's +domain name. For example, for the domain name `domain-controller.example.com`, the distinguished name representation is `DC=domain-controller,DC=example,DC=com`. +If you aren't sure, you can get the DN by running `(Get-ADDomain).DistinguishedName` in PowerShell. + +`$TeleportLDAPGroupName` is the Group name you defined in step 1. + +Run the PowerShell script below, replacing variable values where appropriate. + +```powershell +$DomainDN="DC=domain-controller,DC=example,DC=com" # replace this with your Domain Controller's domain +$TeleportLDAPGroupName="svc-teleport-group" # replace this with your Group name if different + +# Gives Teleport the ability to create LDAP containers in the CDP container. +dsacls "CN=CDP,CN=Public Key Services,CN=Services,CN=Configuration,$DomainDN" /I:T /G "$($TeleportLDAPGroupName):CC;container;" +# Gives Teleport the ability to create and delete cRLDistributionPoint objects in the CDP/Teleport container. +dsacls "CN=TELEPORT,CN=CDP,CN=Public Key Services,CN=Services,CN=Configuration,$DomainDN" /I:T /G "$($TeleportLDAPGroupName):CCDC;cRLDistributionPoint;" +# Gives Teleport the ability to write the certificateRevocationList property in the CDP/Teleport container. +dsacls "CN=TELEPORT,CN=CDP,CN=Public Key Services,CN=Services,CN=Configuration,$DomainDN " /I:T /G "$($TeleportLDAPGroupName):WP;certificateRevocationList;" +# Gives Teleport the ability to create and delete certificationAuthority objects in the NTAuthCertificates container. +dsacls "CN=NTAuthCertificates,CN=Public Key Services,CN=Services,CN=Configuration,$DomainDN" /I:T /G "$($TeleportLDAPGroupName):CCDC;certificationAuthority;" +# Gives Teleport the ability to write the cACertificate property in the NTAuthCertificates container. +dsacls "CN=NTAuthCertificates,CN=Public Key Services,CN=Services,CN=Configuration,$DomainDN" /I:T /G "$($TeleportLDAPGroupName):WP;cACertificate;" -```text -Computer Configuration > Policies > Windows Settings > Security Settings > System Services ``` -Double click on `Smart Card`, select `Define this policy setting` and switch to -`Automatic`. Click "OK". +## Step 4/6. Export your Domain Controller's CA Certificate -
- ![Enable Smartcard](../../img/desktop-access/smartcard.png) -
+Teleport connects to your Domain Controller via LDAPS. This means that you must let Teleport know that the certificate sent +by your Domain Controller during the initial SSL connection is trusted. If your Domain Controller's certificate is trusted by +the system repository on the system running Teleport, you can skip this step. Atlernately, if you trust your network, you can +skip LDAPS certificate verification by setting the `insecure_skip_verify` configuration variable to `false`. +### To Export a CA Certificate -### Open firewall to inbound RDP connections +{/* Adapted from https://www.ibm.com/docs/it/rds/5.2.1?topic=security-exporting-certificate-from-active-directory-server */} +1. Begin by navigating to `Start > Control Panel > Administrative Tools > Certificate Authority` to open the CA Microsoft Management Console (MMC) GUI. +2. Right click on your CA computer and select "Properties". +3. From "General" tab, click "View Certificate". +4. Select the "Details" view and click "Copy to File". +5. Click "Next" in the Certificate Export Wizard, and ensure that "DER encoded binary X.509 (.CER)" is selected +6. Select a name and location for you certificate and click through the wizard. + +Now transfer the exported file to the system where you're running Teleport. You can either add this certificate to your system's +trusted repository or provide the filepath to the `der_ca_file` configuration variable. + +## Step 5/6. Configure Teleport + + + Prior to v8.0, the Teleport CA was not compatible with Windows logins. If + you're setting up Desktop Access in an existing cluster created before v8.0, + you must first perform a [CA rotation](../setup/operations/ca-rotation.mdx) + in order to resolve this. + + +In order to enable Desktop Access in Teleport, add the +following section in `teleport.yaml` on your Linux server: + +```yaml +windows_desktop_service: + enabled: yes + # This is the address that windows_desktop_service will listen on. + listen_addr: "0.0.0.0:3028" + # (optional) This is the address that windows_desktop_service will advertise + # to the rest of Teleport for incoming connections. Only proxy_service should + # connect to windows_desktop_service, users connect to the proxy's web UI + # instead. + public_addr: "desktop-access.example.com:3028" + ldap: + # Address of the Domain Controller for LDAP connections. Usually, this + # address will use port 636, like: domain-controller.example.com:636. + addr: '$LDAP_SERVER_ADDRESS' + # Active Directory domain name you are connecting to, like: domain-controller.example.com. + domain: '$LDAP_DOMAIN_NAME' + # LDAP username for authentication. This will be the user logon name you chose for the service + # account User in step 1 (svc-teleport). This username must include the domain NetBIOS name. + # + # For example, if your domain is "example.com", the NetBIOS name for it is + # likely "EXAMPLE". When connecting as the "svc-teleport" user, you should + # use the format: "EXAMPLE\svc-teleport". + # + # If you are unsure of your NetBIOS name, you can find it by opening a PowerShell command prompt + # and running: + # ``` + # (Get-ADDomain).NetBIOSName + # ``` + username: '$LDAP_USERNAME' + # Plain text file containing the LDAP password for authentication. + # This is the same password you chose in step 1. + password_file: /var/lib/ldap-pass + # You can skip LDAPS certificate verification by setting + # this to true. It is recommended that this be set to false + # and the certificate added your system's trusted repository, + # or its filepath provided in with the der_ca_file variable below. + insecure_skip_verify: false + # DER encoded certificate. + # This should be the path to the certificate exported in step 4. + der_ca_file: /path/to/cert + hosts: + # DNS entries for any static hosts yRou want to connect to. + # These are only needed to connect to desktops that aren't covered by the discovery setting below. + - "100.104.52.89" + - "example.com" + discovery: + # The wildcard '*' character tells Teleport to discover all the hosts in + # the Active Directory Domain. To refine the search, specify a custom DN. + base_dn: '*' -Finally, select: -```text -Computer Configuration > Policies > Windows Settings > Security Settings > Windows Firewall and Advanced Security ``` -Right click on `Inbound Rules` and select `New Rule...`. Under `Predefined` -select `Remote Desktop`. Only select the rule for `User Mode (TCP-in)`. On the -next screen, select `Allow the connection` and finish. +After updating `teleport.yaml`, start Teleport as usual using `teleport start`. -## Step 3/3. Log in using Teleport -{/* TODO: add screenshots */} +## Step 6/6. Log in using Teleport -At this point everything should be ready for Desktop Access connections. Open -the Teleport web UI and log in. +### Create a Teleport User/Role for Desktop Access -On the left pane, select `Desktops`. You should see the list of all computers +{/* TODO: remove the "/ver/8.0/" in the link once these docs are the primary version */} +In order to gain access to a remote desktop, a Teleport user needs to have the appropriate permissions for that desktop. +For example, you can create a role that gives its users access to all windows desktop labels and the `"Administrator"` user: +```yaml +kind: role +version: v4 +metadata: + name: windows-desktop-admins +spec: + allow: + windows_desktop_labels: + "*": "*" + windows_desktop_logins: ["Administrator"] +``` + +See the [RBAC section](https://goteleport.com/docs/ver/8.0/desktop-access/reference/#rbac) in the Reference documentation +for more information about setting up windows desktop permissions. + +See the Access Controls [Getting Started](https://goteleport.com/docs/access-controls/getting-started/#step-13-add-local-users-with-preset-roles) +guide for instructions on how to create or update a user with a given role. + +### Connect to Your Desktop + +At this point everything is ready for Desktop Access connections. Open +the Teleport web UI and log in with a user with the role created above. + +On the left pane, select `Desktops (preview)`. You should see the list of all computers and Domain Controllers connected to your domain. Select one and click `CONNECT` -on the right. +on the right, selecting one of the available logins: + +
+ ![Select Desktop](../../img/desktop-access/select-desktop.png) +
A new tab will open and, after a few seconds, you should be logged in to your target Windows host. diff --git a/docs/pages/desktop-access/reference.mdx b/docs/pages/desktop-access/reference.mdx index 444809f902c84..4abdfc03a17fb 100644 --- a/docs/pages/desktop-access/reference.mdx +++ b/docs/pages/desktop-access/reference.mdx @@ -50,10 +50,15 @@ windows_desktop_service: password_file: /var/lib/ldap-pass # (optional) settings for enabling automatic desktop discovery via LDAP discovery: - # currently supported values: - # '*' to search from the root of the domain - # '' to disable desktop discovery + # The wildcard '*' character tells Teleport to discover all the hosts in + # the Active Directory Domain. To refine the search, specify a custom DN. + # To disable automatic discovery, leave this field blank. base_dn: '*' + # (optional) LDAP filters for further customizing the LDAP search. + # See https://ldap.com/ldap-filters for details on LDAP filter syntax. + filters: + - '(location=Oakland)' + - '(!(primaryGroupID=516))' # exclude domain controllers # Rules for applying labels to Windows hosts based on regular expressions # matched against the host name. If multiple rules match, the desktop will # get the union of all matching labels. @@ -89,12 +94,18 @@ spec: # See above for how labels are applied to desktops. windows_desktop_labels: environment: ["dev", "stage"] - + # Windows user accounts this role can connect as. - windows_desktop_logins: ["Administrator"] + windows_desktop_logins: ["Administrator", "{{internal.windows_logins}}"] ``` -It is possible to use wildcards to match all desktop labels. +It is possible to use wildcards (`"*"`) to match all desktop labels. + +Like with SSH access, the `windows_desktop_logins` field supports the special `{{internal.windows_logins}}` variable +for local users which will map to any logins that are supplied when the user is created with +`tctl users add alice --windows-logins=Administrator,DBUser`. + +For new clusters, the `"access"` role will have `windows_desktop_logins: ["{{internal.windows_logins}}"]` set by default. ## CLI diff --git a/dronegen/common.go b/dronegen/common.go index 9f107812730b9..854b6d00f89c9 100644 --- a/dronegen/common.go +++ b/dronegen/common.go @@ -14,7 +14,12 @@ package main -import "fmt" +import ( + "bytes" + "fmt" + "log" + "os/exec" +) var ( triggerPullRequest = trigger{ @@ -70,18 +75,24 @@ var ( Name: "tmp-integration", Path: "/tmp", } - - // TODO(gus): Set this from `make -C build.assets print-runtime-version` or similar rather - // than hardcoding it. Also remove the usage of RUNTIME as a pipeline-level environment variable - // (as support for these varies among Drone runners) and only set it for steps that need it. - goRuntime = value{raw: "go1.17.3"} ) +var goRuntime value + +func init() { + v, err := exec.Command("make", "-C", "build.assets", "print-go-version").Output() + if err != nil { + log.Fatalf("could not get Go version: %v", err) + } + goRuntime = value{raw: string(bytes.TrimSpace(v))} +} + type buildType struct { os string arch string fips bool centos6 bool + centos7 bool windowsUnsigned bool } @@ -108,11 +119,13 @@ func dockerVolumeRefs(v ...volumeRef) []volumeRef { return append(v, volumeRefDocker) } -// releaseMakefileTarget gets the correct Makefile target for a given arch/fips/centos6 combo +// releaseMakefileTarget gets the correct Makefile target for a given arch/fips/centos combo func releaseMakefileTarget(b buildType) string { makefileTarget := fmt.Sprintf("release-%s", b.arch) if b.centos6 { makefileTarget += "-centos6" + } else if b.centos7 { + makefileTarget += "-centos7" } if b.fips { makefileTarget += "-fips" diff --git a/dronegen/mac.go b/dronegen/mac.go index 9dd5e2c13acc9..da0e9cf69d021 100644 --- a/dronegen/mac.go +++ b/dronegen/mac.go @@ -194,7 +194,7 @@ func installRustToolchainStep(path string) step { `set -u`, `export PATH=/Users/build/.cargo/bin:$PATH`, `mkdir -p ~/build-$DRONE_BUILD_NUMBER-$DRONE_BUILD_CREATED-toolchains`, - `export RUST_VERSION=$(grep RUST_VERSION $WORKSPACE_DIR/go/src/github.com/gravitational/teleport/build.assets/Dockerfile | cut -d= -f2)`, + `export RUST_VERSION=$(make -C $WORKSPACE_DIR/go/src/github.com/gravitational/teleport/build.assets print-rust-version)`, `export CARGO_HOME=~/build-$DRONE_BUILD_NUMBER-$DRONE_BUILD_CREATED-toolchains`, `export RUST_HOME=$CARGO_HOME`, `rustup toolchain install $RUST_VERSION`, @@ -214,7 +214,7 @@ func cleanUpToolchainsStep(path string) step { `export PATH=/Users/build/.cargo/bin:$PATH`, `export CARGO_HOME=~/build-$DRONE_BUILD_NUMBER-$DRONE_BUILD_CREATED-toolchains`, `export RUST_HOME=$CARGO_HOME`, - `export RUST_VERSION=$(grep RUST_VERSION $WORKSPACE_DIR/go/src/github.com/gravitational/teleport/build.assets/Dockerfile | cut -d= -f2)`, + `export RUST_VERSION=$(make -C $WORKSPACE_DIR/go/src/github.com/gravitational/teleport/build.assets print-rust-version)`, `cd $WORKSPACE_DIR/go/src/github.com/gravitational/teleport`, // clean up the rust toolchain even though we're about to delete the directory // this ensures we don't leave behind a broken link @@ -248,7 +248,7 @@ func darwinTagCheckoutCommands() []string { func darwinTagBuildCommands() []string { return []string{ `set -u`, - `export RUST_VERSION=$(grep RUST_VERSION $WORKSPACE_DIR/go/src/github.com/gravitational/teleport/build.assets/Dockerfile | cut -d= -f2)`, + `export RUST_VERSION=$(make -C $WORKSPACE_DIR/go/src/github.com/gravitational/teleport/build.assets print-rust-version)`, `export CARGO_HOME=~/build-$DRONE_BUILD_NUMBER-$DRONE_BUILD_CREATED-toolchains`, `export RUST_HOME=$CARGO_HOME`, `export PATH=~/build-$DRONE_BUILD_NUMBER-$DRONE_BUILD_CREATED-toolchains/go/bin:$CARGO_HOME/bin:/Users/build/.cargo/bin:$PATH`, diff --git a/dronegen/push.go b/dronegen/push.go index 0870a12afc963..6b28c049767a9 100644 --- a/dronegen/push.go +++ b/dronegen/push.go @@ -89,12 +89,12 @@ func pushPipeline(b buildType) pipeline { pipelineName := fmt.Sprintf("push-build-%s-%s", b.os, b.arch) pushEnvironment := map[string]value{ - "UID": value{raw: "1000"}, - "GID": value{raw: "1000"}, - "GOCACHE": value{raw: "/go/cache"}, - "GOPATH": value{raw: "/go"}, - "OS": value{raw: b.os}, - "ARCH": value{raw: b.arch}, + "UID": {raw: "1000"}, + "GID": {raw: "1000"}, + "GOCACHE": {raw: "/go/cache"}, + "GOPATH": {raw: "/go"}, + "OS": {raw: b.os}, + "ARCH": {raw: b.arch}, } if b.fips { pipelineName += "-fips" @@ -104,8 +104,8 @@ func pushPipeline(b buildType) pipeline { p := newKubePipeline(pipelineName) p.Environment = map[string]value{ "RUNTIME": goRuntime, - "UID": value{raw: "1000"}, - "GID": value{raw: "1000"}, + "UID": {raw: "1000"}, + "GID": {raw: "1000"}, } p.Trigger = triggerPush p.Workspace = workspace{Path: "/go"} @@ -118,7 +118,7 @@ func pushPipeline(b buildType) pipeline { Name: "Check out code", Image: "docker:git", Environment: map[string]value{ - "GITHUB_PRIVATE_KEY": value{fromSecret: "GITHUB_PRIVATE_KEY"}, + "GITHUB_PRIVATE_KEY": {fromSecret: "GITHUB_PRIVATE_KEY"}, }, Commands: pushCheckoutCommands(b.fips), }, @@ -134,7 +134,7 @@ func pushPipeline(b buildType) pipeline { Name: "Send Slack notification", Image: "plugins/slack", Settings: map[string]value{ - "webhook": value{fromSecret: "SLACK_WEBHOOK_DEV_TELEPORT"}, + "webhook": {fromSecret: "SLACK_WEBHOOK_DEV_TELEPORT"}, }, Template: []string{ `*{{#success build.status}}✔{{ else }}✘{{/success}} {{ uppercasefirst build.status }}: Build #{{ build.number }}* (type: ` + "`{{ build.event }}`" + `) diff --git a/dronegen/tag.go b/dronegen/tag.go index bc3339282ad2f..d27451adf7628 100644 --- a/dronegen/tag.go +++ b/dronegen/tag.go @@ -113,18 +113,26 @@ func tagCopyArtifactCommands(b buildType) []string { ) } - // we need to specifically rename artifacts which are created for CentOS 6 + // we need to specifically rename artifacts which are created for CentOS // these is the only special case where renaming is not handled inside the Makefile if b.centos6 { + // for CentOS 6 we don't support FIPS, just OSS and enterprise + commands = append(commands, + `export VERSION=$(cat /go/.version.txt)`, + `mv /go/artifacts/teleport-v$${VERSION}-linux-amd64-bin.tar.gz /go/artifacts/teleport-v$${VERSION}-linux-amd64-centos6-bin.tar.gz`, + `mv /go/artifacts/teleport-ent-v$${VERSION}-linux-amd64-bin.tar.gz /go/artifacts/teleport-ent-v$${VERSION}-linux-amd64-centos6-bin.tar.gz`, + ) + } else if b.centos7 { + // for CentOS 7, we support OSS, Enterprise, and FIPS (Enterprise only) commands = append(commands, `export VERSION=$(cat /go/.version.txt)`) if !b.fips { commands = append(commands, - `mv /go/artifacts/teleport-v$${VERSION}-linux-amd64-bin.tar.gz /go/artifacts/teleport-v$${VERSION}-linux-amd64-centos6-bin.tar.gz`, - `mv /go/artifacts/teleport-ent-v$${VERSION}-linux-amd64-bin.tar.gz /go/artifacts/teleport-ent-v$${VERSION}-linux-amd64-centos6-bin.tar.gz`, + `mv /go/artifacts/teleport-v$${VERSION}-linux-amd64-bin.tar.gz /go/artifacts/teleport-v$${VERSION}-linux-amd64-centos7-bin.tar.gz`, + `mv /go/artifacts/teleport-ent-v$${VERSION}-linux-amd64-bin.tar.gz /go/artifacts/teleport-ent-v$${VERSION}-linux-amd64-centos7-bin.tar.gz`, ) } else { commands = append(commands, - `mv /go/artifacts/teleport-ent-v$${VERSION}-linux-amd64-fips-bin.tar.gz /go/artifacts/teleport-ent-v$${VERSION}-linux-amd64-centos6-fips-bin.tar.gz`, + `mv /go/artifacts/teleport-ent-v$${VERSION}-linux-amd64-fips-bin.tar.gz /go/artifacts/teleport-ent-v$${VERSION}-linux-amd64-centos7-fips-bin.tar.gz`, ) } } @@ -147,13 +155,13 @@ func uploadToS3Step(s s3Settings) step { Name: "Upload to S3", Image: "plugins/s3", Settings: map[string]value{ - "bucket": value{fromSecret: "AWS_S3_BUCKET"}, - "access_key": value{fromSecret: "AWS_ACCESS_KEY_ID"}, - "secret_key": value{fromSecret: "AWS_SECRET_ACCESS_KEY"}, - "region": value{raw: s.region}, - "source": value{raw: s.source}, - "target": value{raw: s.target}, - "strip_prefix": value{raw: s.stripPrefix}, + "bucket": {fromSecret: "AWS_S3_BUCKET"}, + "access_key": {fromSecret: "AWS_ACCESS_KEY_ID"}, + "secret_key": {fromSecret: "AWS_SECRET_ACCESS_KEY"}, + "region": {raw: s.region}, + "source": {raw: s.source}, + "target": {raw: s.target}, + "strip_prefix": {raw: s.stripPrefix}, }, } } @@ -180,9 +188,11 @@ func tagPipelines() []pipeline { // Only amd64 Windows is supported for now. ps = append(ps, tagPipeline(buildType{os: "windows", arch: "amd64"})) - // Also add the two CentOS 6 artifacts. + // Also add CentOS artifacts // CentOS 6 FIPS builds have been removed in Teleport 7.0. See https://github.com/gravitational/teleport/issues/7207 ps = append(ps, tagPipeline(buildType{os: "linux", arch: "amd64", centos6: true})) + ps = append(ps, tagPipeline(buildType{os: "linux", arch: "amd64", centos7: true})) + ps = append(ps, tagPipeline(buildType{os: "linux", arch: "amd64", centos7: true, fips: true})) ps = append(ps, darwinTagPipeline(), darwinTeleportPkgPipeline(), darwinTshPkgPipeline()) return ps @@ -200,14 +210,16 @@ func tagPipeline(b buildType) pipeline { pipelineName := fmt.Sprintf("build-%s-%s", b.os, b.arch) if b.centos6 { pipelineName += "-centos6" + } else if b.centos7 { + pipelineName += "-centos7" } tagEnvironment := map[string]value{ - "UID": value{raw: "1000"}, - "GID": value{raw: "1000"}, - "GOCACHE": value{raw: "/go/cache"}, - "GOPATH": value{raw: "/go"}, - "OS": value{raw: b.os}, - "ARCH": value{raw: b.arch}, + "UID": {raw: "1000"}, + "GID": {raw: "1000"}, + "GOCACHE": {raw: "/go/cache"}, + "GOPATH": {raw: "/go"}, + "OS": {raw: b.os}, + "ARCH": {raw: b.arch}, } if b.fips { pipelineName += "-fips" @@ -233,7 +245,7 @@ func tagPipeline(b buildType) pipeline { Name: "Check out code", Image: "docker:git", Environment: map[string]value{ - "GITHUB_PRIVATE_KEY": value{fromSecret: "GITHUB_PRIVATE_KEY"}, + "GITHUB_PRIVATE_KEY": {fromSecret: "GITHUB_PRIVATE_KEY"}, }, Commands: tagCheckoutCommands(b.fips), }, @@ -309,9 +321,9 @@ func tagPackagePipeline(packageType string, b buildType) pipeline { } environment := map[string]value{ - "ARCH": value{raw: b.arch}, - "TMPDIR": value{raw: "/go"}, - "ENT_TARBALL_PATH": value{raw: "/go/artifacts"}, + "ARCH": {raw: b.arch}, + "TMPDIR": {raw: "/go"}, + "ENT_TARBALL_PATH": {raw: "/go/artifacts"}, } dependentPipeline := fmt.Sprintf("build-%s-%s", b.os, b.arch) @@ -373,7 +385,7 @@ func tagPackagePipeline(packageType string, b buildType) pipeline { Name: "Check out code", Image: "docker:git", Environment: map[string]value{ - "GITHUB_PRIVATE_KEY": value{fromSecret: "GITHUB_PRIVATE_KEY"}, + "GITHUB_PRIVATE_KEY": {fromSecret: "GITHUB_PRIVATE_KEY"}, }, Commands: tagCheckoutCommands(b.fips), }, @@ -382,10 +394,10 @@ func tagPackagePipeline(packageType string, b buildType) pipeline { Name: "Download artifacts from S3", Image: "amazon/aws-cli", Environment: map[string]value{ - "AWS_REGION": value{raw: "us-west-2"}, - "AWS_S3_BUCKET": value{fromSecret: "AWS_S3_BUCKET"}, - "AWS_ACCESS_KEY_ID": value{fromSecret: "AWS_ACCESS_KEY_ID"}, - "AWS_SECRET_ACCESS_KEY": value{fromSecret: "AWS_SECRET_ACCESS_KEY"}, + "AWS_REGION": {raw: "us-west-2"}, + "AWS_S3_BUCKET": {fromSecret: "AWS_S3_BUCKET"}, + "AWS_ACCESS_KEY_ID": {fromSecret: "AWS_ACCESS_KEY_ID"}, + "AWS_SECRET_ACCESS_KEY": {fromSecret: "AWS_SECRET_ACCESS_KEY"}, }, Commands: tagDownloadArtifactCommands(b), }, diff --git a/dronegen/tests.go b/dronegen/tests.go index 35eeee20fd4db..b0ab6430adae0 100644 --- a/dronegen/tests.go +++ b/dronegen/tests.go @@ -84,8 +84,8 @@ func testCodePipeline() pipeline { p := newKubePipeline("test") p.Environment = map[string]value{ "RUNTIME": goRuntime, - "UID": value{raw: "1000"}, - "GID": value{raw: "1000"}, + "UID": {raw: "1000"}, + "GID": {raw: "1000"}, } p.Trigger = triggerPullRequest p.Workspace = workspace{Path: "/go"} @@ -102,15 +102,15 @@ func testCodePipeline() pipeline { ), } goEnvironment := map[string]value{ - "GOCACHE": value{raw: "/tmpfs/go/cache"}, - "GOPATH": value{raw: "/tmpfs/go"}, + "GOCACHE": {raw: "/tmpfs/go/cache"}, + "GOPATH": {raw: "/tmpfs/go"}, } p.Steps = []step{ { Name: "Check out code", Image: "docker:git", Environment: map[string]value{ - "GITHUB_PRIVATE_KEY": value{fromSecret: "GITHUB_PRIVATE_KEY"}, + "GITHUB_PRIVATE_KEY": {fromSecret: "GITHUB_PRIVATE_KEY"}, }, Volumes: []volumeRef{ volumeRefTmpfs, @@ -201,11 +201,11 @@ echo "" Name: "Run integration tests", Image: "docker", Environment: map[string]value{ - "GOCACHE": value{raw: "/tmpfs/go/cache"}, - "GOPATH": value{raw: "/tmpfs/go"}, - "INTEGRATION_CI_KUBECONFIG": value{fromSecret: "INTEGRATION_CI_KUBECONFIG"}, - "KUBECONFIG": value{raw: "/tmpfs/go/kubeconfig.ci"}, - "TEST_KUBE": value{raw: "true"}, + "GOCACHE": {raw: "/tmpfs/go/cache"}, + "GOPATH": {raw: "/tmpfs/go"}, + "INTEGRATION_CI_KUBECONFIG": {fromSecret: "INTEGRATION_CI_KUBECONFIG"}, + "KUBECONFIG": {raw: "/tmpfs/go/kubeconfig.ci"}, + "TEST_KUBE": {raw: "true"}, }, Volumes: dockerVolumeRefs(volumeRefTmpfs, volumeRefTmpIntegration), Commands: []string{ @@ -250,9 +250,9 @@ func testDocsPipeline() pipeline { Name: "Run docs tests", Image: "docker:git", Environment: map[string]value{ - "GOCACHE": value{raw: "/tmpfs/go/cache"}, - "UID": value{raw: "1000"}, - "GID": value{raw: "1000"}, + "GOCACHE": {raw: "/tmpfs/go/cache"}, + "UID": {raw: "1000"}, + "GID": {raw: "1000"}, }, Volumes: dockerVolumeRefs(volumeRefTmpfs), Commands: []string{ diff --git a/e b/e index 876444f173c91..0f8574e8e7b6a 160000 --- a/e +++ b/e @@ -1 +1 @@ -Subproject commit 876444f173c9163a2ef33f001c4e16e4aadfe5aa +Subproject commit 0f8574e8e7b6a282313777e8dd0e9866a812357c diff --git a/lib/backend/backend.go b/lib/backend/backend.go index e3b7074d31106..ccc9272fb64bd 100644 --- a/lib/backend/backend.go +++ b/lib/backend/backend.go @@ -124,7 +124,7 @@ type Batch interface { // // lease, err := backend.Create() // lease.Expires = time.Now().Add(time.Second) -// // Item TTL is extended +// Item TTL is extended // err = backend.KeepAlive(lease) // type Lease struct { diff --git a/lib/config/configuration.go b/lib/config/configuration.go index 30614a47c899f..fbc12942522e0 100644 --- a/lib/config/configuration.go +++ b/lib/config/configuration.go @@ -1192,7 +1192,7 @@ func applyWindowsDesktopConfig(fc *FileConfig, cfg *service.Config) error { } for _, filter := range fc.WindowsDesktop.Discovery.Filters { - if _, err := ldap.CompileFilter(ldap.EscapeFilter(filter)); err != nil { + if _, err := ldap.CompileFilter(filter); err != nil { return trace.BadParameter("WindowsDesktopService specifies invalid LDAP filter %q", filter) } } diff --git a/lib/service/db.go b/lib/service/db.go index 625bd8502b394..9b727f8524746 100644 --- a/lib/service/db.go +++ b/lib/service/db.go @@ -180,14 +180,8 @@ func (process *TeleportProcess) initDatabaseService() (retErr error) { Databases: databases, ResourceMatchers: process.Config.Databases.ResourceMatchers, AWSMatchers: process.Config.Databases.AWSMatchers, - OnHeartbeat: func(err error) { - if err != nil { - process.BroadcastEvent(Event{Name: TeleportDegradedEvent, Payload: teleport.ComponentDatabase}) - } else { - process.BroadcastEvent(Event{Name: TeleportOKEvent, Payload: teleport.ComponentDatabase}) - } - }, - LockWatcher: lockWatcher, + OnHeartbeat: process.onHeartbeat(teleport.ComponentDatabase), + LockWatcher: lockWatcher, }) if err != nil { return trace.Wrap(err) diff --git a/lib/service/desktop.go b/lib/service/desktop.go index f4c41e8d08e67..f1ad420809b8a 100644 --- a/lib/service/desktop.go +++ b/lib/service/desktop.go @@ -218,16 +218,11 @@ func (process *TeleportProcess) initWindowsDesktopServiceRegistered(log *logrus. HostUUID: cfg.HostUUID, PublicAddr: publicAddr, StaticHosts: cfg.WindowsDesktop.Hosts, - OnHeartbeat: func(err error) { - if err != nil { - process.BroadcastEvent(Event{Name: TeleportDegradedEvent, Payload: teleport.ComponentWindowsDesktop}) - } else { - process.BroadcastEvent(Event{Name: TeleportOKEvent, Payload: teleport.ComponentWindowsDesktop}) - } - }, + OnHeartbeat: process.onHeartbeat(teleport.ComponentWindowsDesktop), }, - LDAPConfig: desktop.LDAPConfig(cfg.WindowsDesktop.LDAP), - DiscoveryBaseDN: cfg.WindowsDesktop.Discovery.BaseDN, + LDAPConfig: desktop.LDAPConfig(cfg.WindowsDesktop.LDAP), + DiscoveryBaseDN: cfg.WindowsDesktop.Discovery.BaseDN, + DiscoveryLDAPFilters: cfg.WindowsDesktop.Discovery.Filters, }) if err != nil { return trace.Wrap(err) diff --git a/lib/service/kubernetes.go b/lib/service/kubernetes.go index 98e18fdefd819..a72aebc2d7c07 100644 --- a/lib/service/kubernetes.go +++ b/lib/service/kubernetes.go @@ -248,13 +248,7 @@ func (process *TeleportProcess) initKubernetesService(log *logrus.Entry, conn *C TLS: tlsConfig, AccessPoint: accessPoint, LimiterConfig: cfg.Kube.Limiter, - OnHeartbeat: func(err error) { - if err != nil { - process.BroadcastEvent(Event{Name: TeleportDegradedEvent, Payload: teleport.ComponentKube}) - } else { - process.BroadcastEvent(Event{Name: TeleportOKEvent, Payload: teleport.ComponentKube}) - } - }, + OnHeartbeat: process.onHeartbeat(teleport.ComponentKube), }) if err != nil { return trace.Wrap(err) diff --git a/lib/service/service.go b/lib/service/service.go index f657d6a9aa53c..aff47d596ed05 100644 --- a/lib/service/service.go +++ b/lib/service/service.go @@ -350,6 +350,17 @@ func (process *TeleportProcess) GetBackend() backend.Backend { return process.backend } +// onHeartbeat generates the default OnHeartbeat callback for the specified component. +func (process *TeleportProcess) onHeartbeat(component string) func(err error) { + return func(err error) { + if err != nil { + process.BroadcastEvent(Event{Name: TeleportDegradedEvent, Payload: component}) + } else { + process.BroadcastEvent(Event{Name: TeleportOKEvent, Payload: component}) + } + } +} + func (process *TeleportProcess) findStaticIdentity(id auth.IdentityID) (*auth.Identity, error) { for i := range process.Config.Identities { identity := process.Config.Identities[i] @@ -1436,13 +1447,7 @@ func (process *TeleportProcess) initAuthService() error { AnnouncePeriod: apidefaults.ServerAnnounceTTL/2 + utils.RandomDuration(apidefaults.ServerAnnounceTTL/10), CheckPeriod: defaults.HeartbeatCheckPeriod, ServerTTL: apidefaults.ServerAnnounceTTL, - OnHeartbeat: func(err error) { - if err != nil { - process.BroadcastEvent(Event{Name: TeleportDegradedEvent, Payload: teleport.ComponentAuth}) - } else { - process.BroadcastEvent(Event{Name: TeleportOKEvent, Payload: teleport.ComponentAuth}) - } - }, + OnHeartbeat: process.onHeartbeat(teleport.ComponentAuth), }) if err != nil { return trace.Wrap(err) @@ -1945,13 +1950,7 @@ func (process *TeleportProcess) initSSH() error { regular.SetFIPS(cfg.FIPS), regular.SetBPF(ebpf), regular.SetRestrictedSessionManager(rm), - regular.SetOnHeartbeat(func(err error) { - if err != nil { - process.BroadcastEvent(Event{Name: TeleportDegradedEvent, Payload: teleport.ComponentNode}) - } else { - process.BroadcastEvent(Event{Name: TeleportOKEvent, Payload: teleport.ComponentNode}) - } - }), + regular.SetOnHeartbeat(process.onHeartbeat(teleport.ComponentNode)), regular.SetAllowTCPForwarding(cfg.SSH.AllowTCPForwarding), regular.SetLockWatcher(lockWatcher), ) @@ -2957,13 +2956,7 @@ func (process *TeleportProcess) initProxyEndpoint(conn *Connector) error { regular.SetNamespace(apidefaults.Namespace), regular.SetRotationGetter(process.getRotation), regular.SetFIPS(cfg.FIPS), - regular.SetOnHeartbeat(func(err error) { - if err != nil { - process.BroadcastEvent(Event{Name: TeleportDegradedEvent, Payload: teleport.ComponentProxy}) - } else { - process.BroadcastEvent(Event{Name: TeleportOKEvent, Payload: teleport.ComponentProxy}) - } - }), + regular.SetOnHeartbeat(process.onHeartbeat(teleport.ComponentProxy)), regular.SetEmitter(streamEmitter), regular.SetLockWatcher(lockWatcher), ) @@ -3054,13 +3047,7 @@ func (process *TeleportProcess) initProxyEndpoint(conn *Connector) error { TLS: tlsConfig, LimiterConfig: cfg.Proxy.Limiter, AccessPoint: accessPoint, - OnHeartbeat: func(err error) { - if err != nil { - process.BroadcastEvent(Event{Name: TeleportDegradedEvent, Payload: component}) - } else { - process.BroadcastEvent(Event{Name: TeleportOKEvent, Payload: component}) - } - }, + OnHeartbeat: process.onHeartbeat(component), }) if err != nil { return trace.Wrap(err) @@ -3594,13 +3581,7 @@ func (process *TeleportProcess) initApps() { GetRotation: process.getRotation, Apps: applications, ResourceMatchers: process.Config.Apps.ResourceMatchers, - OnHeartbeat: func(err error) { - if err != nil { - process.BroadcastEvent(Event{Name: TeleportDegradedEvent, Payload: teleport.ComponentApp}) - } else { - process.BroadcastEvent(Event{Name: TeleportOKEvent, Payload: teleport.ComponentApp}) - } - }, + OnHeartbeat: process.onHeartbeat(teleport.ComponentApp), }) if err != nil { return trace.Wrap(err) diff --git a/lib/services/reconciler.go b/lib/services/reconciler.go index 1a7339885a1b9..7fa8fd4154f0b 100644 --- a/lib/services/reconciler.go +++ b/lib/services/reconciler.go @@ -86,8 +86,8 @@ func NewReconciler(cfg ReconcilerConfig) (*Reconciler, error) { // Reconcile reconciles currently registered resources with new resources and // creates/updates/deletes them appropriately. // -// It's used in combination with watchers by agents (app, database) to enable -// dynamically registered resources. +// It's used in combination with watchers by agents (app, database, desktop) +// to enable dynamically registered resources. type Reconciler struct { cfg ReconcilerConfig log logrus.FieldLogger @@ -145,19 +145,19 @@ func (r *Reconciler) processNewResource(ctx context.Context, currentResources ty registered := currentResources.Find(new.GetName()) if registered == nil { if r.cfg.Matcher(new) { - r.log.Infof("%v matches, creating.", new) + r.log.Infof("%v %v matches, creating.", new.GetKind(), new.GetMetadata().Name) if err := r.cfg.OnCreate(ctx, new); err != nil { return trace.Wrap(err, "failed to create %v", new) } return nil } - r.log.Debugf("%v doesn't match, not creating.", new) + r.log.Debugf("%v %v doesn't match, not creating.", new.GetKind(), new.GetMetadata().Name) return nil } // Don't overwrite resource of a different origin. if registered.Origin() != new.Origin() { - r.log.Debugf("%v has different origin (%v vs %v), not updating.", new, + r.log.Debugf("%v has different origin (%v vs %v), not updating.", new.GetMetadata().Name, new.Origin(), registered.Origin()) return nil } @@ -166,19 +166,19 @@ func (r *Reconciler) processNewResource(ctx context.Context, currentResources ty // labels still match. if CompareResources(new, registered) != Equal { if r.cfg.Matcher(new) { - r.log.Infof("%v updated, updating.", new) + r.log.Infof("%v %v updated, updating.", new.GetKind(), new.GetMetadata().Name) if err := r.cfg.OnUpdate(ctx, new); err != nil { return trace.Wrap(err, "failed to update %v", new) } return nil } - r.log.Infof("%v updated and no longer matches, deleting.", new) + r.log.Infof("%v %v updated and no longer matches, deleting.", new.GetKind(), new.GetMetadata().Name) if err := r.cfg.OnDelete(ctx, registered); err != nil { return trace.Wrap(err, "failed to delete %v", new) } return nil } - r.log.Debugf("%v is already registered.", new) + r.log.Debugf("%v %v is already registered.", new.GetKind(), new.GetMetadata().Name) return nil } diff --git a/lib/services/reconciler_test.go b/lib/services/reconciler_test.go index 516eda4feb0a5..7fe5ef4e62701 100644 --- a/lib/services/reconciler_test.go +++ b/lib/services/reconciler_test.go @@ -223,6 +223,14 @@ func (r *testResource) GetName() string { return r.Metadata.Name } +func (r *testResource) GetKind() string { + return "TestResource" +} + +func (r *testResource) GetMetadata() types.Metadata { + return r.Metadata +} + func (r *testResource) Origin() string { return r.Metadata.Labels[types.OriginLabel] } diff --git a/lib/srv/desktop/discovery.go b/lib/srv/desktop/discovery.go new file mode 100644 index 0000000000000..d01dd8a7349cf --- /dev/null +++ b/lib/srv/desktop/discovery.go @@ -0,0 +1,201 @@ +// Copyright 2021 Gravitational, Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package desktop + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/go-ldap/ldap/v3" + apidefaults "github.com/gravitational/teleport/api/defaults" + "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/lib/defaults" + "github.com/gravitational/teleport/lib/services" + "github.com/gravitational/teleport/lib/utils" + "github.com/gravitational/trace" +) + +// computerAttributes are the attributes we fetch when discovering +// Windows hosts via LDAP +// see: https://docs.microsoft.com/en-us/windows/win32/adschema/c-computer#windows-server-2012-attributes +var computerAttribtes = []string{ + attrName, + attrDNSHostName, + attrObjectGUID, + attrOS, + attrOSVersion, +} + +const ( + // computerClass is the object class for computers in Active Directory + computerClass = "computer" + // containerClass is the object class for containers in Active Directory + containerClass = "container" + // gmsaClass is the object class for group managed service accounts in Active Directory. + gmsaClass = "msDS-GroupManagedServiceAccount" + + attrName = "name" + attrDNSHostName = "dNSHostName" // unusual capitalization is correct + attrObjectGUID = "objectGUID" + attrOS = "operatingSystem" + attrOSVersion = "operatingSystemVersion" +) + +// startDesktopDiscovery starts fetching desktops from LDAP, periodically +// registering and unregistering them as necessary. +// Desktop discovery runs in the background until the context is canceled. +func (s *WindowsService) startDesktopDiscovery(ctx context.Context) error { + reconciler, err := services.NewReconciler(services.ReconcilerConfig{ + // Use a matcher that matches all resources, since our desktops are + // pre-filtered by nature of using an LDAP search with filters. + Matcher: func(r types.ResourceWithLabels) bool { return true }, + + GetCurrentResources: func() types.ResourcesWithLabels { return s.lastDiscoveryResults }, + GetNewResources: s.getDesktopsFromLDAP, + OnCreate: s.createDesktop, + OnUpdate: s.updateDesktop, + OnDelete: s.deleteDesktop, + Log: s.cfg.Log, + }) + if err != nil { + return trace.Wrap(err) + } + + go func() { + // reconcile once before starting the ticker, so that desktops show up immediately + if err := reconciler.Reconcile(ctx); err != nil && err != context.Canceled { + s.cfg.Log.Errorf("desktop reconciliation failed: %v", err) + } + + // TODO(zmb3): consider making the discovery period configurable + // (it's currently hard coded to 5 minutes in order to match DB access discovery behavior) + t := s.cfg.Clock.NewTicker(5 * time.Minute) + defer t.Stop() + for { + select { + case <-ctx.Done(): + return + case <-t.Chan(): + if err := reconciler.Reconcile(ctx); err != nil && err != context.Canceled { + s.cfg.Log.Errorf("desktop reconciliation failed: %v", err) + } + } + } + }() + + return nil +} + +func (s *WindowsService) ldapSearchFilter() string { + var filters []string + filters = append(filters, fmt.Sprintf("(objectClass=%s)", computerClass)) + filters = append(filters, fmt.Sprintf("(!(objectClass=%s))", gmsaClass)) + filters = append(filters, s.cfg.DiscoveryLDAPFilters...) + + return combineLDAPFilters(filters) +} + +// getDesktopsFromLDAP discovers Windows hosts via LDAP +func (s *WindowsService) getDesktopsFromLDAP() types.ResourcesWithLabels { + filter := s.ldapSearchFilter() + s.cfg.Log.Debugf("searching for desktops with LDAP filter %v", filter) + + entries, err := s.lc.readWithFilter(s.cfg.DiscoveryBaseDN, filter, computerAttribtes) + if err != nil { + s.cfg.Log.Warnf("could not discover Windows Desktops: %v", err) + return nil + } + + s.cfg.Log.Debugf("discovered %d Windows Desktops", len(entries)) + + var result types.ResourcesWithLabels + for _, entry := range entries { + desktop, err := s.ldapEntryToWindowsDesktop(s.closeCtx, entry, s.cfg.HostLabelsFn) + if err != nil { + s.cfg.Log.Warnf("could not create Windows Desktop from LDAP entry: %v", err) + continue + } + result = append(result, desktop) + } + + // capture the result, which will be used on the next reconcile loop + s.lastDiscoveryResults = result + + return result +} + +func (s *WindowsService) createDesktop(ctx context.Context, r types.ResourceWithLabels) error { + d, ok := r.(types.WindowsDesktop) + if !ok { + return trace.Errorf("create: expected a WindowsDesktop, got %T", r) + } + return s.cfg.AccessPoint.CreateWindowsDesktop(ctx, d) +} + +func (s *WindowsService) updateDesktop(ctx context.Context, r types.ResourceWithLabels) error { + d, ok := r.(types.WindowsDesktop) + if !ok { + return trace.Errorf("update: expected a WindowsDesktop, got %T", r) + } + return s.cfg.AccessPoint.UpdateWindowsDesktop(ctx, d) +} + +func (s *WindowsService) deleteDesktop(ctx context.Context, r types.ResourceWithLabels) error { + return s.cfg.AuthClient.DeleteWindowsDesktop(ctx, r.GetName()) +} + +// ldapEntryToWindowsDesktop generates the Windows Desktop resource +// from an LDAP search result +func (s *WindowsService) ldapEntryToWindowsDesktop(ctx context.Context, entry *ldap.Entry, getHostLabels func(string) map[string]string) (types.ResourceWithLabels, error) { + hostname := entry.GetAttributeValue(attrDNSHostName) + + labels := getHostLabels(hostname) + labels["teleport.dev/dns_host_name"] = hostname + labels["teleport.dev/computer_name"] = entry.GetAttributeValue(attrName) + labels["teleport.dev/os"] = entry.GetAttributeValue(attrOS) + labels["teleport.dev/os_version"] = entry.GetAttributeValue(attrOSVersion) + labels["teleport.dev/windows_domain"] = s.cfg.Domain + labels[types.OriginLabel] = types.OriginDynamic + + addrs, err := s.dnsResolver.LookupHost(ctx, hostname) + if err != nil || len(addrs) == 0 { + return nil, trace.WrapWithMessage(err, "couldn't resolve %q", hostname) + } + + s.cfg.Log.Debugf("resolved %v => %v", hostname, addrs) + addr, err := utils.ParseHostPortAddr(addrs[0], defaults.RDPListenPort) + if err != nil { + return nil, trace.Wrap(err) + } + + desktop, err := types.NewWindowsDesktopV3( + // ensure no '.' in name, because we use SNI to route to the right + // desktop, and our cert is valid for *.desktop.teleport.cluster.local + strings.ReplaceAll(hostname, ".", "-"), + labels, + types.WindowsDesktopSpecV3{ + Addr: addr.String(), + Domain: s.cfg.Domain, + }, + ) + if err != nil { + return nil, trace.Wrap(err) + } + + desktop.SetExpiry(s.cfg.Clock.Now().UTC().Add(apidefaults.ServerAnnounceTTL)) + return desktop, nil +} diff --git a/lib/srv/desktop/discovery_test.go b/lib/srv/desktop/discovery_test.go new file mode 100644 index 0000000000000..4bd7085a89747 --- /dev/null +++ b/lib/srv/desktop/discovery_test.go @@ -0,0 +1,59 @@ +// Copyright 2021 Gravitational, Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package desktop + +import ( + "testing" + + "github.com/go-ldap/ldap/v3" + "github.com/stretchr/testify/require" +) + +// TestDiscoveryLDAPFilter verifies that WindowsService produces a valid +// LDAP filter when given valid configuration. +func TestDiscoveryLDAPFilter(t *testing.T) { + for _, test := range []struct { + desc string + filters []string + assert require.ErrorAssertionFunc + }{ + { + desc: "OK - no custom filters", + assert: require.NoError, + }, + { + desc: "OK - custom filters", + filters: []string{"(computerName=test)", "(location=Oakland)"}, + assert: require.NoError, + }, + { + desc: "NOK - invalid custom filter", + filters: []string{"invalid"}, + assert: require.Error, + }, + } { + t.Run(test.desc, func(t *testing.T) { + s := &WindowsService{ + cfg: WindowsServiceConfig{ + DiscoveryLDAPFilters: test.filters, + }, + } + + filter := s.ldapSearchFilter() + _, err := ldap.CompileFilter(filter) + test.assert(t, err) + }) + } +} diff --git a/lib/srv/desktop/ldap.go b/lib/srv/desktop/ldap.go index 9ed4ae45f3742..f7877ee13d8c2 100644 --- a/lib/srv/desktop/ldap.go +++ b/lib/srv/desktop/ldap.go @@ -20,6 +20,7 @@ import ( "crypto/tls" "crypto/x509" "fmt" + "strings" "github.com/go-ldap/ldap/v3" "github.com/gravitational/trace" @@ -76,16 +77,9 @@ func (c *ldapClient) close() { c.client.Close() } -// read fetches an LDAP entry at path and its children, if any. Only -// entries with the given class are returned and only with the specified -// attributes. -// -// You can browse LDAP on the Windows host to find the objectClass for a -// specific entry using ADSIEdit.msc. -// You can find the list of all AD classes at -// https://docs.microsoft.com/en-us/windows/win32/adschema/classes-all -func (c *ldapClient) read(path ldapPath, class string, attrs []string) ([]*ldap.Entry, error) { - dn := c.cfg.dn(path) +// readWithFilter searches the specified DN (and its children) using the specified LDAP filter. +// See https://ldap.com/ldap-filters/ for more information on LDAP filter syntax. +func (c *ldapClient) readWithFilter(dn string, filter string, attrs []string) ([]*ldap.Entry, error) { req := ldap.NewSearchRequest( dn, ldap.ScopeWholeSubtree, @@ -93,7 +87,7 @@ func (c *ldapClient) read(path ldapPath, class string, attrs []string) ([]*ldap. 0, // no SizeLimit 0, // no TimeLimit false, // TypesOnly == false, we want attribute values - fmt.Sprintf("(objectClass=%s)", class), + filter, attrs, nil, // no Controls ) @@ -102,6 +96,19 @@ func (c *ldapClient) read(path ldapPath, class string, attrs []string) ([]*ldap. return nil, trace.Wrap(err, "fetching LDAP object %q: %v", dn, err) } return res.Entries, nil + +} + +// read fetches an LDAP entry at path and its children, if any. Only +// entries with the given class are returned and only with the specified +// attributes. +// +// You can browse LDAP on the Windows host to find the objectClass for a +// specific entry using ADSIEdit.msc. +// You can find the list of all AD classes at +// https://docs.microsoft.com/en-us/windows/win32/adschema/classes-all +func (c *ldapClient) read(dn string, class string, attrs []string) ([]*ldap.Entry, error) { + return c.readWithFilter(dn, fmt.Sprintf("(objectClass=%s)", class), attrs) } // create creates an LDAP entry at the given path, with the given class and @@ -112,8 +119,7 @@ func (c *ldapClient) read(path ldapPath, class string, attrs []string) ([]*ldap. // attributes for similar entries using ADSIEdit.msc. // You can find the list of all AD classes at // https://docs.microsoft.com/en-us/windows/win32/adschema/classes-all -func (c *ldapClient) create(path ldapPath, class string, attrs map[string][]string) error { - dn := c.cfg.dn(path) +func (c *ldapClient) create(dn string, class string, attrs map[string][]string) error { req := ldap.NewAddRequest(dn, nil) for k, v := range attrs { req.Attribute(k, v) @@ -136,9 +142,10 @@ func (c *ldapClient) create(path ldapPath, class string, attrs map[string][]stri return nil } -// createContainer creates an LDAP container entry at the given path. -func (c *ldapClient) createContainer(path ldapPath) error { - err := c.create(path, "container", nil) +// createContainer creates an LDAP container entry if +// it doesn't already exist. +func (c *ldapClient) createContainer(dn string) error { + err := c.create(dn, containerClass, nil) // Ignore the error if container already exists. if trace.IsAlreadyExists(err) { return nil @@ -154,8 +161,7 @@ func (c *ldapClient) createContainer(path ldapPath) error { // // You can browse LDAP on the Windows host to find attributes of existing // entries using ADSIEdit.msc. -func (c *ldapClient) update(path ldapPath, replaceAttrs map[string][]string) error { - dn := c.cfg.dn(path) +func (c *ldapClient) update(dn string, replaceAttrs map[string][]string) error { req := ldap.NewModifyRequest(dn, nil) for k, v := range replaceAttrs { req.Replace(k, v) @@ -165,3 +171,7 @@ func (c *ldapClient) update(path ldapPath, replaceAttrs map[string][]string) err } return nil } + +func combineLDAPFilters(filters []string) string { + return "(&" + strings.Join(filters, "") + ")" +} diff --git a/lib/srv/desktop/windows_server.go b/lib/srv/desktop/windows_server.go index bfa72b797c491..7efe411ad3f5b 100644 --- a/lib/srv/desktop/windows_server.go +++ b/lib/srv/desktop/windows_server.go @@ -76,6 +76,10 @@ type WindowsService struct { lc *ldapClient + // lastDisoveryResults stores the results of the most recent LDAP search + // when desktop discovery is enabled + lastDiscoveryResults types.ResourcesWithLabels + // Windows hosts discovered via LDAP likely won't resolve with the // default DNS resolver, so we need a custom resolver that will // query the domain controller. @@ -119,7 +123,10 @@ type WindowsServiceConfig struct { // DiscoveryBaseDN is the base DN for searching for Windows Desktops. // Desktop discovery is disabled if this field is empty. DiscoveryBaseDN string - // TODO(zmb3): add support for LDAP filters as defined in RFD #34 + // DiscoveryLDAPFilters are additional LDAP filters for searching for + // Windows Desktops. If multiple filters are specified, they are ANDed + // together into a single search. + DiscoveryLDAPFilters []string } // LDAPConfig contains parameters for connecting to an LDAP server. @@ -157,37 +164,17 @@ func (cfg LDAPConfig) check() error { return nil } -// ldapPath is a helper type representing a hierarchical path in LDAP. -type ldapPath []string - -func (cfg LDAPConfig) dn(path ldapPath) string { - // Here's an example DN: - // - // CN=mycluster,CN=Certification Authorities,CN=Public Key Services,CN=Services,CN=Configuration,DC=example,DC=com - // - // You read it backwards: - // - DC=example,DC=com means "example.com" domain - // - CN=mycluster,CN=Certification Authorities,CN=Public Key Services,CN=Services,CN=Configuration - // means "Configuration/Services/Public Key Services/Certification Authorities/mycluster" - // entry, where "mycluster" is the Teleport cluster name - // - // The path argument is expected to be a normal hierarchical path, like: - // ["Configuration", "Services", "Public Key Services", "Certification Authorities", "mycluster"] - s := new(strings.Builder) - for i := len(path) - 1; i >= 0; i-- { - fmt.Fprintf(s, "CN=%s", path[i]) - if i != 0 { - s.WriteRune(',') +func (cfg LDAPConfig) domainDN() string { + var sb strings.Builder + parts := strings.Split(cfg.Domain, ".") + for _, p := range parts { + if sb.Len() > 0 { + sb.WriteString(",") } + sb.WriteString("DC=") + sb.WriteString(p) } - for _, dc := range strings.Split(cfg.Domain, ".") { - // we always want to emit a leading ',' unless path was empty - if s.Len() > 0 { - s.WriteString(",") - } - fmt.Fprintf(s, "DC=%s", dc) - } - return s.String() + return sb.String() } // HeartbeatConfig contains the configuration for service heartbeats. @@ -203,6 +190,25 @@ type HeartbeatConfig struct { StaticHosts []utils.NetAddr } +func (cfg *WindowsServiceConfig) checkAndSetDiscoveryDefaults() error { + switch { + case cfg.DiscoveryBaseDN == types.Wildcard: + cfg.DiscoveryBaseDN = cfg.domainDN() + case len(cfg.DiscoveryBaseDN) > 0: + if _, err := ldap.ParseDN(cfg.DiscoveryBaseDN); err != nil { + return trace.BadParameter("WindowsServiceConfig contains an invalid base_dn: %v", err) + } + } + + for _, filter := range cfg.DiscoveryLDAPFilters { + if _, err := ldap.CompileFilter(filter); err != nil { + return trace.BadParameter("WindowsServiceConfig contains an invalid LDAP filter %q: %v", filter, err) + } + } + + return nil +} + func (cfg *WindowsServiceConfig) CheckAndSetDefaults() error { if cfg.Log == nil { cfg.Log = logrus.New().WithField(trace.Component, teleport.ComponentWindowsDesktop) @@ -237,6 +243,10 @@ func (cfg *WindowsServiceConfig) CheckAndSetDefaults() error { if err := cfg.LDAPConfig.check(); err != nil { return trace.Wrap(err) } + if err := cfg.checkAndSetDiscoveryDefaults(); err != nil { + return trace.Wrap(err) + } + return nil } @@ -331,17 +341,15 @@ func NewWindowsService(cfg WindowsServiceConfig) (*WindowsService, error) { return nil, trace.Wrap(err) } - // for now, the only valid base DN is '*' - // TODO(zmb3): allow further customizing the search - if s.cfg.DiscoveryBaseDN == types.Wildcard { - if err := s.startDiscoveredHostHeartbeats(); err != nil { + if len(s.cfg.DiscoveryBaseDN) > 0 { + if err := s.startDesktopDiscovery(ctx); err != nil { s.Close() return nil, trace.Wrap(err) } } else if len(s.cfg.Heartbeat.StaticHosts) == 0 { s.cfg.Log.Warnln("desktop discovery via LDAP is disabled, and no hosts are defined in the configuration; there will be no Windows desktops available to connect") } else { - s.cfg.Log.Infoln("desktop discovery via LDAP is disabled, set 'base_dn: *' to enable") + s.cfg.Log.Infoln("desktop discovery via LDAP is disabled, set 'base_dn' to enable") } return s, nil @@ -371,122 +379,6 @@ func (s *WindowsService) startServiceHeartbeat() error { return nil } -// computerAttributes are the attributes we fetch when discovering -// Windows hosts via LDAP -// see: https://docs.microsoft.com/en-us/windows/win32/adschema/c-computer#windows-server-2012-attributes -var computerAttribtes = []string{ - attrName, - attrDNSHostName, - attrObjectGUID, - attrOS, - attrOSVersion, -} - -const ( - // computerClass is the object class for computers in Active Directory - computerClass = "computer" - - attrName = "name" - attrDNSHostName = "dNSHostName" // unusual capitalization is correct - attrObjectGUID = "objectGUID" - attrOS = "operatingSystem" - attrOSVersion = "operatingSystemVersion" -) - -// startDiscoveredHostHeartbeats kicks off background processing to discover Windows -// hosts via LDAP and perform heartbeats to record them with the auth server -func (s *WindowsService) startDiscoveredHostHeartbeats() error { - // here we're searching for computers using an empty ldapPath - // in order to search from the root - entries, err := s.lc.read(ldapPath{}, computerClass, computerAttribtes) - if err != nil { - return trace.Wrap(err) - } - - s.cfg.Log.Infof("discovered %d Windows hosts", len(entries)) - - // NOTE: for now, we only search for hosts once, and then kick off a heartbeat - // for each discovered host. - // - // TODO(zmb3): periodically refresh hosts from LDAP in order to detect new hosts - // and stop heartbeating for hosts that no longer exist - // (this may require updates to srv.Heartbeat) - for _, entry := range entries { - desktop := *entry // make a copy to avoid capturing a loop variable that may change - - heartbeat, err := srv.NewHeartbeat(srv.HeartbeatConfig{ - Context: s.closeCtx, - Component: teleport.ComponentWindowsDesktop, - Mode: srv.HeartbeatModeWindowsDesktop, - Announcer: s.cfg.AccessPoint, - GetServerInfo: func() (types.Resource, error) { - return s.dynamicHostHeartbeatInfo(s.closeCtx, &desktop, s.cfg.HostLabelsFn) - }, - // Larger than normal periods are due to the fact that we don't currently refresh - // the list of hosts from LDAP. Since the heartbeat data is static we don't need - // to announce it as frequently as we do for dynamic resources. - // TODO(zmb3): reconsider timeouts when #8644 is addressed - KeepAlivePeriod: apidefaults.ServerKeepAliveTTL() * 2, - AnnouncePeriod: apidefaults.ServerAnnounceTTL + utils.RandomDuration(apidefaults.ServerAnnounceTTL/10), - CheckPeriod: defaults.HeartbeatCheckPeriod * 60, - ServerTTL: apidefaults.ServerAnnounceTTL, - }) - if err != nil { - return trace.Wrap(err) - } - - go func() { - if err := heartbeat.Run(); err != nil { - s.cfg.Log.WithError(err).Errorf("heartbeat for Windows host %v ended with error", desktop.DN) - } - }() - } - - return nil -} - -// dynamicHostHeartbeatInfo generates the Windows Desktop resource -// for heartbeating hosts discovered via LDAP -func (s *WindowsService) dynamicHostHeartbeatInfo(ctx context.Context, entry *ldap.Entry, getHostLabels func(string) map[string]string) (types.Resource, error) { - hostname := entry.GetAttributeValue(attrDNSHostName) - - labels := getHostLabels(hostname) - labels["teleport.dev/dns_host_name"] = hostname - labels["teleport.dev/computer_name"] = entry.GetAttributeValue(attrName) - labels["teleport.dev/os"] = entry.GetAttributeValue(attrOS) - labels["teleport.dev/os_version"] = entry.GetAttributeValue(attrOSVersion) - labels["teleport.dev/windows_domain"] = s.cfg.Domain - labels[types.OriginLabel] = types.OriginDynamic - - addrs, err := s.dnsResolver.LookupHost(ctx, hostname) - if err != nil || len(addrs) == 0 { - return nil, trace.WrapWithMessage(err, "couldn't resolve %q", hostname) - } - - s.cfg.Log.Debugf("resolved %v => %v", hostname, addrs) - addr, err := utils.ParseHostPortAddr(addrs[0], defaults.RDPListenPort) - if err != nil { - return nil, trace.Wrap(err) - } - - desktop, err := types.NewWindowsDesktopV3( - // ensure no '.' in name, because we use SNI to route to the right - // desktop, and our cert is valid for *.desktop.teleport.cluster.local - strings.ReplaceAll(hostname, ".", "-"), - labels, - types.WindowsDesktopSpecV3{ - Addr: addr.String(), - Domain: s.cfg.Domain, - }, - ) - if err != nil { - return nil, trace.Wrap(err) - } - - desktop.SetExpiry(s.cfg.Clock.Now().UTC().Add(apidefaults.ServerAnnounceTTL)) - return desktop, nil -} - // startStaticHostHeartbeats spawns heartbeat routines for all static hosts in // this service. We use heartbeats instead of registering once at startup to // support expiration. @@ -725,8 +617,8 @@ func (s *WindowsService) staticHostHeartbeatInfo(netAddr utils.NetAddr, } } -// nameForStaticHost attempts to find the UUID of an existing Windows desktop -// with the same address. If no matching address is found, a new UUID is +// nameForStaticHost attempts to find the name of an existing Windows desktop +// with the same address. If no matching address is found, a new name is // generated. // // The list of WindowsDesktop objects should be read from the local cache. It @@ -807,14 +699,13 @@ func (s *WindowsService) updateCA(ctx context.Context) error { func (s *WindowsService) updateCAInNTAuthStore(ctx context.Context, caDER []byte) error { // Check if our CA is already in the store. The LDAP entry for NTAuth store // is constant and it should always exist. - // TODO(zmb3): NTAuthCertificates may not exist, create it if necessary. - ntauthPath := ldapPath{"Configuration", "Services", "Public Key Services", "NTAuthCertificates"} - entries, err := s.lc.read(ntauthPath, "certificationAuthority", []string{"cACertificate"}) + ntAuthDN := "CN=NTAuthCertificates,CN=Public Key Services,CN=Services,CN=Configuration," + s.cfg.LDAPConfig.domainDN() + entries, err := s.lc.read(ntAuthDN, "certificationAuthority", []string{"cACertificate"}) if err != nil { return trace.Wrap(err, "fetching existing CAs: %v", err) } if len(entries) != 1 { - return trace.BadParameter("expected exactly 1 NTAuthCertificates CA store at %q, but found %d", ntauthPath, len(entries)) + return trace.BadParameter("expected exactly 1 NTAuthCertificates CA store at %q, but found %d", ntAuthDN, len(entries)) } // TODO(zmb3): during CA rotation, find the old CA in NTAuthStore and remove it. // Right now we just append the active CA and let the old ones hang around. @@ -827,6 +718,8 @@ func (s *WindowsService) updateCAInNTAuthStore(ctx context.Context, caDER []byte } } + s.cfg.Log.Debugf("None of the %d existing NTAuthCertificates matched Teleport's", len(existingCAs)) + // CA is not in the store, append it. updatedCAs := make([]string, 0, len(existingCAs)+1) for _, existingCADER := range existingCAs { @@ -834,7 +727,7 @@ func (s *WindowsService) updateCAInNTAuthStore(ctx context.Context, caDER []byte } updatedCAs = append(updatedCAs, string(caDER)) - if err := s.lc.update(ntauthPath, map[string][]string{ + if err := s.lc.update(ntAuthDN, map[string][]string{ "cACertificate": updatedCAs, }); err != nil { return trace.Wrap(err, "updating CA entry: %v", err) @@ -856,17 +749,17 @@ func (s *WindowsService) updateCRL(ctx context.Context, crlDER []byte) error { // after the Teleport cluster name. For example, CRL for cluster "prod" // will be placed at: // ... > CDP > Teleport > prod - containerPath := ldapPath{"Configuration", "Services", "Public Key Services", "CDP", "Teleport"} - crlPath := append(containerPath, s.clusterName) + containerDN := "CN=Teleport,CN=CDP,CN=Public Key Services,CN=Services,CN=Configuration," + s.cfg.LDAPConfig.domainDN() + crlDN := "CN=" + s.clusterName + "," + containerDN // Create the parent container. - if err := s.lc.createContainer(containerPath); err != nil { + if err := s.lc.createContainer(containerDN); err != nil { return trace.Wrap(err, "creating CRL container: %v", err) } // Create the CRL object itself. if err := s.lc.create( - crlPath, + crlDN, "cRLDistributionPoint", map[string][]string{"certificateRevocationList": {string(crlDER)}}, ); err != nil { @@ -875,7 +768,7 @@ func (s *WindowsService) updateCRL(ctx context.Context, crlDER []byte) error { } // CRL already exists, update it. if err := s.lc.update( - crlPath, + crlDN, map[string][]string{"certificateRevocationList": {string(crlDER)}}, ); err != nil { return trace.Wrap(err) @@ -935,11 +828,7 @@ func (s *WindowsService) generateCredentials(ctx context.Context, username, doma // CRLs in it. Each service can also handle RDP connections for a different // domain, with the assumption that some other windows_desktop_service // published a CRL there. - // - // In other words, the domain var below may not be the same as - // s.cfg.LDAPConfig.Domain and that's expected. - crlPath := ldapPath{"Configuration", "Services", "Public Key Services", "CDP", "Teleport", s.clusterName} - crlDN := s.cfg.LDAPConfig.dn(crlPath) + crlDN := "CN=Teleport,CN=CDP,CN=Public Key Services,CN=Services,CN=Configuration," + s.cfg.LDAPConfig.domainDN() genResp, err := s.cfg.AuthClient.GenerateWindowsDesktopCert(ctx, &proto.WindowsDesktopCertRequest{ CSR: csrPEM, // LDAP URI pointing at the CRL created with updateCRL. diff --git a/lib/srv/desktop/windows_server_test.go b/lib/srv/desktop/windows_server_test.go new file mode 100644 index 0000000000000..e9fb29da68fbb --- /dev/null +++ b/lib/srv/desktop/windows_server_test.go @@ -0,0 +1,77 @@ +// Copyright 2021 Gravitational, Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package desktop + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestConfigWildcardBaseDN(t *testing.T) { + cfg := &WindowsServiceConfig{ + DiscoveryBaseDN: "*", + LDAPConfig: LDAPConfig{ + Domain: "test.goteleport.com", + }, + } + require.NoError(t, cfg.checkAndSetDiscoveryDefaults()) + require.Equal(t, "DC=test,DC=goteleport,DC=com", cfg.DiscoveryBaseDN) +} + +func TestConfigDesktopDiscovery(t *testing.T) { + for _, test := range []struct { + desc string + baseDN string + filters []string + assert require.ErrorAssertionFunc + }{ + { + desc: "NOK - invalid base DN", + baseDN: "example.com", + assert: require.Error, + }, + { + desc: "NOK - invalid filter", + baseDN: "DC=example,DC=goteleport,DC=com", + filters: []string{"invalid!"}, + assert: require.Error, + }, + { + desc: "OK - wildcard base DN", + baseDN: "*", + assert: require.NoError, + }, + { + desc: "OK - no filters", + baseDN: "DC=example,DC=goteleport,DC=com", + assert: require.NoError, + }, + { + desc: "OK - valid filters", + baseDN: "DC=example,DC=goteleport,DC=com", + filters: []string{"(!(primaryGroupID=516))"}, + assert: require.NoError, + }, + } { + t.Run(test.desc, func(t *testing.T) { + cfg := &WindowsServiceConfig{ + DiscoveryBaseDN: test.baseDN, + DiscoveryLDAPFilters: test.filters, + } + test.assert(t, cfg.checkAndSetDiscoveryDefaults()) + }) + } +} diff --git a/lib/web/apiserver.go b/lib/web/apiserver.go index 9b85ae4314899..0f61f5bd8ae40 100644 --- a/lib/web/apiserver.go +++ b/lib/web/apiserver.go @@ -410,7 +410,9 @@ func NewHandler(cfg Config, opts ...HandlerOption) (*RewritingHandler, error) { // Desktop access endpoints. h.GET("/webapi/sites/:site/desktops", h.WithClusterAuth(h.getDesktopsHandle)) - h.GET("/webapi/sites/:site/desktop/:desktopUUID/connect", h.WithClusterAuth(h.handleDesktopAccessWebsocket)) + h.GET("/webapi/sites/:site/desktops/:desktopName", h.WithClusterAuth(h.getDesktopHandle)) + // GET //webapi/sites/:site/desktops/:desktopName/connect?access_token=&username=&width=&height= + h.GET("/webapi/sites/:site/desktops/:desktopName/connect", h.WithClusterAuth(h.desktopConnectHandle)) // if Web UI is enabled, check the assets dir: var indexPage *template.Template diff --git a/lib/web/desktop.go b/lib/web/desktop.go index f1c6f1a11e2c6..38999fcc0fc3e 100644 --- a/lib/web/desktop.go +++ b/lib/web/desktop.go @@ -22,8 +22,10 @@ import ( "math/rand" "net" "net/http" + "strconv" "github.com/julienschmidt/httprouter" + "github.com/sirupsen/logrus" "golang.org/x/net/websocket" "github.com/gravitational/trace" @@ -31,33 +33,69 @@ import ( "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/lib/reversetunnel" "github.com/gravitational/teleport/lib/srv/desktop" + "github.com/gravitational/teleport/lib/srv/desktop/tdp" "github.com/gravitational/teleport/lib/utils" ) -func (h *Handler) handleDesktopAccessWebsocket( +// GET /webapi/sites/:site/desktops/:desktopName/connect?access_token=&username=&width=&height= +func (h *Handler) desktopConnectHandle( w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *SessionContext, site reversetunnel.RemoteSite, ) (interface{}, error) { - desktopUUID := p.ByName("desktopUUID") - if desktopUUID == "" { - return nil, trace.BadParameter("missing desktopUUID in request URL") + desktopName := p.ByName("desktopName") + if desktopName == "" { + return nil, trace.BadParameter("missing desktopName in request URL") } - log := ctx.log.WithField("desktop-uuid", desktopUUID) + + log := ctx.log.WithField("desktop-name", desktopName) log.Debug("New desktop access websocket connection") + if err := createDesktopConnection(w, r, desktopName, log, ctx, site); err != nil { + log.Error(err) + return nil, trace.Wrap(err) + } + + return nil, nil +} + +func createDesktopConnection( + w http.ResponseWriter, + r *http.Request, + desktopName string, + log *logrus.Entry, + ctx *SessionContext, + site reversetunnel.RemoteSite, +) error { + + q := r.URL.Query() + username := q.Get("username") + if username == "" { + return trace.BadParameter("missing username") + } + width, err := strconv.Atoi(q.Get("width")) + if err != nil { + return trace.BadParameter("width missing or invalid") + } + height, err := strconv.Atoi(q.Get("height")) + if err != nil { + return trace.BadParameter("height missing or invalid") + } + + log.Debugf("Attempting to connect to desktop using username=%v, width=%v, height=%v\n", username, width, height) + winServices, err := ctx.unsafeCachedAuthClient.GetWindowsDesktopServices(r.Context()) if err != nil { - return nil, trace.Wrap(err) + return trace.Wrap(err) } // TODO(awly): trusted cluster support - if this request is for a different // cluster, dial their proxy and forward the websocket request as is. if len(winServices) == 0 { - return nil, trace.NotFound("No windows_desktop_services are registered in this cluster") + return trace.NotFound("No windows_desktop_services are registered in this cluster") } // Pick a random Windows desktop service as our gateway. // When agent mode is implemented in the service, we'll have to filter out @@ -75,21 +113,31 @@ func (h *Handler) handleDesktopAccessWebsocket( ServerID: service.GetName(), }) if err != nil { - return nil, trace.WrapWithMessage(err, "failed to connect to windows_desktop_service at %q: %v", service.GetAddr(), err) + return trace.WrapWithMessage(err, "failed to connect to windows_desktop_service at %q: %v", service.GetAddr(), err) } defer serviceCon.Close() tlsConfig := ctx.clt.Config() // Pass target desktop UUID via SNI. - tlsConfig.ServerName = desktopUUID + desktop.SNISuffix + tlsConfig.ServerName = desktopName + desktop.SNISuffix serviceConTLS := tls.Client(serviceCon, ctx.clt.Config()) log.Debug("Connected to windows_desktop_service") + tdpConn := tdp.NewConn(serviceConTLS) + err = tdpConn.OutputMessage(tdp.ClientUsername{Username: username}) + if err != nil { + return trace.Wrap(err) + } + err = tdpConn.OutputMessage(tdp.ClientScreenSpec{Width: uint32(width), Height: uint32(height)}) + if err != nil { + return trace.Wrap(err) + } + websocket.Handler(func(conn *websocket.Conn) { if err := proxyWebsocketConn(conn, serviceConTLS); err != nil { log.WithError(err).Warningf("Error proxying a desktop protocol websocket to windows_desktop_service") } }).ServeHTTP(w, r) - return nil, nil + return nil } func proxyWebsocketConn(ws *websocket.Conn, con net.Conn) error { diff --git a/lib/web/servers.go b/lib/web/servers.go index ef8f83290b30d..4de46ea44dc52 100644 --- a/lib/web/servers.go +++ b/lib/web/servers.go @@ -80,3 +80,20 @@ func (h *Handler) getDesktopsHandle(w http.ResponseWriter, r *http.Request, p ht return ui.MakeDesktops(windowsDesktops), nil } + +// getDesktopHandle returns a desktop. +func (h *Handler) getDesktopHandle(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *SessionContext, site reversetunnel.RemoteSite) (interface{}, error) { + clt, err := ctx.GetUserClient(site) + if err != nil { + return nil, trace.Wrap(err) + } + + desktopName := p.ByName("desktopName") + + windowsDesktop, err := clt.GetWindowsDesktop(r.Context(), desktopName) + if err != nil { + return nil, trace.Wrap(err) + } + + return ui.MakeDesktop(windowsDesktop), nil +} diff --git a/lib/web/ui/server.go b/lib/web/ui/server.go index 81be50ced1f43..72f3c7ebfc86d 100644 --- a/lib/web/ui/server.go +++ b/lib/web/ui/server.go @@ -18,7 +18,10 @@ package ui import ( "sort" + "strconv" + "strings" + "github.com/gravitational/teleport" "github.com/gravitational/teleport/api/constants" "github.com/gravitational/teleport/api/types" ) @@ -207,28 +210,41 @@ type Desktop struct { Labels []Label `json:"labels"` } +// MakeDesktop converts a desktop from its API form to a type the UI can display. +func MakeDesktop(windowsDesktop types.WindowsDesktop) Desktop { + // stripRdpPort strips the default rdp port from an ip address since it is unimportant to display + stripRdpPort := func(addr string) string { + splitAddr := strings.Split(addr, ":") + if len(splitAddr) > 1 && splitAddr[1] == strconv.Itoa(teleport.StandardRDPPort) { + return splitAddr[0] + } + return addr + } + uiLabels := []Label{} + + for name, value := range windowsDesktop.GetAllLabels() { + uiLabels = append(uiLabels, Label{ + Name: name, + Value: value, + }) + } + + sort.Sort(sortedLabels(uiLabels)) + + return Desktop{ + OS: constants.WindowsOS, + Name: windowsDesktop.GetName(), + Addr: stripRdpPort(windowsDesktop.GetAddr()), + Labels: uiLabels, + } +} + // MakeDesktops converts desktops from their API form to a type the UI can display. func MakeDesktops(windowsDesktops []types.WindowsDesktop) []Desktop { uiDesktops := make([]Desktop, 0, len(windowsDesktops)) for _, windowsDesktop := range windowsDesktops { - uiLabels := []Label{} - - for name, value := range windowsDesktop.GetAllLabels() { - uiLabels = append(uiLabels, Label{ - Name: name, - Value: value, - }) - } - - sort.Sort(sortedLabels(uiLabels)) - - uiDesktops = append(uiDesktops, Desktop{ - OS: constants.WindowsOS, - Name: windowsDesktop.GetName(), - Addr: windowsDesktop.GetAddr(), - Labels: uiLabels, - }) + uiDesktops = append(uiDesktops, MakeDesktop(windowsDesktop)) } return uiDesktops diff --git a/webassets b/webassets index a1039e35e86ae..db4dbe5a7ec2d 160000 --- a/webassets +++ b/webassets @@ -1 +1 @@ -Subproject commit a1039e35e86aec770db6cdb32321c93356477757 +Subproject commit db4dbe5a7ec2d9bd1540f4fd89d0a6d1a52b8181