diff --git a/examples/envoy-federated/.gitignore b/examples/envoy-federated/.gitignore new file mode 100644 index 0000000..9ba5b5b --- /dev/null +++ b/examples/envoy-federated/.gitignore @@ -0,0 +1,3 @@ +docker/web-domain/web-server +docker/web-federated/web-server +docker/echo/echo-server diff --git a/examples/envoy-federated/1-start-services.sh b/examples/envoy-federated/1-start-services.sh new file mode 100755 index 0000000..9641aa4 --- /dev/null +++ b/examples/envoy-federated/1-start-services.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +set -e + +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +bb=$(tput bold) +nn=$(tput sgr0) + +# Start up the web server +echo "${bb}Starting web server...${nn}" +docker-compose -f "${DIR}"/docker-compose.yml exec -d web-domain web-server -log /tmp/web-server.log + +echo "${bb}Starting federated web server...${nn}" +docker-compose -f "${DIR}"/docker-compose.yml exec -d web-federated web-server -log /tmp/web-server.log + +# Start up the echo server +echo "${bb}Starting echo server...${nn}" +docker-compose -f "${DIR}"/docker-compose.yml exec -d echo echo-server -log /tmp/echo-server.log diff --git a/examples/envoy-federated/2-start-spire-agents.sh b/examples/envoy-federated/2-start-spire-agents.sh new file mode 100755 index 0000000..cfeca06 --- /dev/null +++ b/examples/envoy-federated/2-start-spire-agents.sh @@ -0,0 +1,36 @@ +#!/bin/bash + +set -e + +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +bb=$(tput bold) +nn=$(tput sgr0) + +# Bootstrap trust to the SPIRE server for each agent by copying over the +# trust bundle into each agent container. Alternatively, an upstream CA could +# be configured on the SPIRE server and each agent provided with the upstream +# trust bundle (see UpstreamAuthority under +# https://github.com/spiffe/spire/blob/master/doc/spire_server.md#plugin-types) +echo "${bb}Bootstrapping trust between SPIRE agents and federated SPIRE server...${nn}" +docker-compose -f "${DIR}"/docker-compose.yml exec -T spire-server-federated bin/spire-server bundle show | + docker-compose -f "${DIR}"/docker-compose.yml exec -T web-federated tee conf/agent/bootstrap.crt > /dev/null + +echo "${bb}Bootstrapping trust between SPIRE agents and SPIRE server...${nn}" +docker-compose -f "${DIR}"/docker-compose.yml exec -T spire-server-domain bin/spire-server bundle show | + docker-compose -f "${DIR}"/docker-compose.yml exec -T echo tee conf/agent/bootstrap.crt > /dev/null + +docker-compose -f "${DIR}"/docker-compose.yml exec -T spire-server-domain bin/spire-server bundle show | + docker-compose -f "${DIR}"/docker-compose.yml exec -T web-domain tee conf/agent/bootstrap.crt > /dev/null + +# Start up the web server SPIRE agent. +echo "${bb}Starting web server SPIRE agent...${nn}" +docker-compose -f "${DIR}"/docker-compose.yml exec -d web-domain bin/spire-agent run + +# Start up the web-federated server SPIRE agent. +echo "${bb}Starting web-federated server SPIRE agent...${nn}" +docker-compose -f "${DIR}"/docker-compose.yml exec -d web-federated bin/spire-agent run + +# Start up the echo server SPIRE agent. +echo "${bb}Starting echo server SPIRE agent...${nn}" +docker-compose -f "${DIR}"/docker-compose.yml exec -d echo bin/spire-agent run diff --git a/examples/envoy-federated/3-create-registration-entries.sh b/examples/envoy-federated/3-create-registration-entries.sh new file mode 100755 index 0000000..e93b109 --- /dev/null +++ b/examples/envoy-federated/3-create-registration-entries.sh @@ -0,0 +1,55 @@ +#/bin/bash + +set -e + +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +bb=$(tput bold) +nn=$(tput sgr0) + +fingerprint() { + # calculate the SHA1 digest of the DER bytes of the certificate using the + # "coreutils" output format (`-r`) to provide uniform output from + # `openssl sha1` on macOS and linux. + cat $1 | openssl x509 -outform DER | openssl sha1 -r | awk '{print $1}' +} + +WEB_FEDERATED_AGENT_FINGERPRINT=$(fingerprint "${DIR}"/docker/web-federated/conf/agent.crt.pem) +WEB_AGENT_FINGERPRINT=$(fingerprint "${DIR}"/docker/web-domain/conf/agent.crt.pem) +ECHO_AGENT_FINGERPRINT=$(fingerprint "${DIR}"/docker/echo/conf/agent.crt.pem) + +echo "${bb}Storing bundles...${nn}" +docker-compose -f "${DIR}"/docker-compose.yml exec spire-server-federated bin/spire-server bundle show | + docker-compose -f "${DIR}"/docker-compose.yml exec -T spire-server-domain tee /opt/spire/federatedServerBundle.pem > /dev/null +docker-compose -f "${DIR}"/docker-compose.yml exec spire-server-domain bin/spire-server bundle show | + docker-compose -f "${DIR}"/docker-compose.yml exec -T spire-server-federated tee /opt/spire/domainServerBundle.pem > /dev/null + +echo "${bb}Creating federated bundle: domain...${nn}" +docker-compose -f "${DIR}"/docker-compose.yml exec spire-server-federated bin/spire-server bundle set \ + -id spiffe://domain.test \ + -path /opt/spire/domainServerBundle.pem + +echo "${bb}Creating federated bundle: federated...${nn}" +docker-compose -f "${DIR}"/docker-compose.yml exec spire-server-domain bin/spire-server bundle set \ + -id spiffe://federated.test \ + -path /opt/spire/federatedServerBundle.pem + +echo "${bb}Creating registration entry for the federated web server...${nn}" +docker-compose -f "${DIR}"/docker-compose.yml exec spire-server-federated bin/spire-server entry create \ + -parentID spiffe://federated.test/spire/agent/x509pop/${WEB_FEDERATED_AGENT_FINGERPRINT} \ + -spiffeID spiffe://federated.test/web-server \ + -selector unix:user:envoy \ + -federatesWith spiffe://domain.test + +echo "${bb}Creating registration entry for the domain server...${nn}" +docker-compose -f "${DIR}"/docker-compose.yml exec spire-server-domain bin/spire-server entry create \ + -parentID spiffe://domain.test/spire/agent/x509pop/${ECHO_AGENT_FINGERPRINT} \ + -spiffeID spiffe://domain.test/echo-server \ + -selector unix:user:envoy \ + -federatesWith spiffe://federated.test + +echo "${bb}Creating registration entry for the web server...${nn}" +docker-compose -f "${DIR}"/docker-compose.yml exec spire-server-domain bin/spire-server entry create \ + -parentID spiffe://domain.test/spire/agent/x509pop/${WEB_AGENT_FINGERPRINT} \ + -spiffeID spiffe://domain.test/web-server \ + -selector unix:user:envoy diff --git a/examples/envoy-federated/README.md b/examples/envoy-federated/README.md new file mode 100644 index 0000000..2b5c43e --- /dev/null +++ b/examples/envoy-federated/README.md @@ -0,0 +1,85 @@ +# Envoy 1.19.0 SDS Example with SPIRE 1.0.2 + +## Requirements + +- Go 1.14 +- docker-compose + +## Diagram + +![diagram](./images/envoy-federated.png) + +## Build + +``` +$ ./build.sh +``` + +## Run + +``` +$ docker-compose up -d +``` + +## Start Web and Echo Servers + +``` +$ ./1-start-services.sh +``` + +## Start SPIRE Agents + +``` +$ ./2-start-spire-agents.sh +``` + +## Create Workload Registration Entries + +``` +$ ./3-create-registration-entries.sh +``` + +## Interact with Web Server + +Open up a browser to http://localhost:8080 to test out: + +- Direct connection between Web and Echo servers +- mTLS connection between Web and Echo servers via Envoy +- TLS connection between Web and Echo servers via Envoy + +## Interact with Federated Web Server + +Open up a browser to http://localhost:8088 to test out: + +- Direct connection between Web and Echo servers +- mTLS connection between Web and Echo servers via Envoy +- TLS connection between Web and Echo servers via Envoy +**NOTE** It may take a 30 seconds or so for Envoy to reconnect to upstream +after the registration entries are created. + +## Clean up + +``` +$ docker-compose down +``` + +## Miscellaneous + +### Regenerating Agent Certificates for X509PoP attestation + +If you want to regenerate the agent certificates, you use +the gencerts.go Go script, supplying the names of all the services. + +``` +$ go run gencerts.go web echo +74613bb92549782b5d01ad5e6e93cedd78841683 web +14e6255ca2a1f32198656fc400f82d242ccf44f2 echo +``` + +This script creates a new CA keypair that is used to sign the service +certificates. It replaces the agent keypairs in the docker +configurations for each service, as well as the agent cacert in the +spire-server configuration. + +After running, you must rerun `build.sh` to rebuild the containers with the +changes. diff --git a/examples/envoy-federated/build.sh b/examples/envoy-federated/build.sh new file mode 100755 index 0000000..993e2f9 --- /dev/null +++ b/examples/envoy-federated/build.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +set -e + +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +(cd "${DIR}"/src/web-server && GOOS=linux go build -v -o $DIR/docker/web-domain/web-server) +(cd "${DIR}"/src/web-server && GOOS=linux go build -v -o $DIR/docker/web-federated/web-server) +(cd "${DIR}"/src/echo-server && GOOS=linux go build -v -o $DIR/docker/echo/echo-server) + +docker-compose -f "${DIR}"/docker-compose.yml build diff --git a/examples/envoy-federated/docker-compose.yml b/examples/envoy-federated/docker-compose.yml new file mode 100644 index 0000000..fc1427f --- /dev/null +++ b/examples/envoy-federated/docker-compose.yml @@ -0,0 +1,39 @@ +version: '3' +services: + spire-server-federated: + build: ./docker/spire-server-federated + hostname: spire-server-federated + tty: true + privileged: true + spire-server-domain: + build: ./docker/spire-server-domain + hostname: spire-server-domain + tty: true + privileged: true + echo: + build: ./docker/echo + hostname: echo + tty: true + privileged: true + links: + - spire-server-domain + web-domain: + build: ./docker/web-domain + hostname: web-domain + tty: true + privileged: true + links: + - spire-server-domain + - echo + ports: + - 8080:8080 + web-federated: + build: ./docker/web-federated + hostname: web-federated + tty: true + privileged: true + links: + - spire-server-federated + - echo + ports: + - 8088:8080 diff --git a/examples/envoy-federated/docker/echo/Dockerfile b/examples/envoy-federated/docker/echo/Dockerfile new file mode 100644 index 0000000..f4247c9 --- /dev/null +++ b/examples/envoy-federated/docker/echo/Dockerfile @@ -0,0 +1,14 @@ +FROM gcr.io/spiffe-io/spire-agent:1.0.2 as spire + +FROM envoyproxy/envoy-alpine:v1.19.0 +RUN mkdir -p /opt/spire/conf/agent +RUN mkdir -p /opt/spire/data/agent +COPY --from=spire /opt/spire/bin/spire-agent /opt/spire/bin/spire-agent +COPY conf/envoy.yaml /etc/envoy/envoy.yaml +COPY conf/spire-agent.conf /opt/spire/conf/agent/agent.conf +COPY conf/agent.key.pem /opt/spire/conf/agent/agent.key.pem +COPY conf/agent.crt.pem /opt/spire/conf/agent/agent.crt.pem +COPY echo-server /usr/local/bin/echo-server + +WORKDIR /opt/spire +CMD /usr/local/bin/envoy -l debug -c /etc/envoy/envoy.yaml --log-path /tmp/envoy.log diff --git a/examples/envoy-federated/docker/echo/conf/agent.crt.pem b/examples/envoy-federated/docker/echo/conf/agent.crt.pem new file mode 100644 index 0000000..ae7d3f9 --- /dev/null +++ b/examples/envoy-federated/docker/echo/conf/agent.crt.pem @@ -0,0 +1,10 @@ +-----BEGIN CERTIFICATE----- +MIIBZjCB8aADAgECAgEBMA0GCSqGSIb3DQEBCwUAMAAwIhgPMDAwMTAxMDEwMDAw +MDBaGA85OTk5MTIzMTIzNTk1OVowIDEeMBwGA1UEAxMVRWNobyBYNTA5UG9QIElk +ZW50aXR5MHwwDQYJKoZIhvcNAQEBBQADawAwaAJhAMyss5CqRpvL6jzXDggSV/+A +NBaYhAyWgRRkRKKcKE+1GfXutd4ewrrsC5DRncSH1HxTnwO9H7YmobKu7kVUZegB +EfeydefNQqvBnHqMooUWUgfiwAN01CKOxEURn+dtzQIDAQABoxIwEDAOBgNVHQ8B +Af8EBAMCB4AwDQYJKoZIhvcNAQELBQADYQAj+ukSTrT5aJFOtDkpA4dHGOgJNFQ+ +Lf03GEBuvNCzM/XKaxr+/j0PbtcE+SwtOn/rTV1aGcKHT+jOSQuLUW7wguB7Scvd +p/ecQJ2D1C98EEiJ0zEDuuCEXQhPmFjSOkk= +-----END CERTIFICATE----- diff --git a/examples/envoy-federated/docker/echo/conf/agent.key.pem b/examples/envoy-federated/docker/echo/conf/agent.key.pem new file mode 100644 index 0000000..124fcfb --- /dev/null +++ b/examples/envoy-federated/docker/echo/conf/agent.key.pem @@ -0,0 +1,13 @@ +-----BEGIN PRIVATE KEY----- +MIIB5gIBADANBgkqhkiG9w0BAQEFAASCAdAwggHMAgEAAmEAzKyzkKpGm8vqPNcO +CBJX/4A0FpiEDJaBFGREopwoT7UZ9e613h7CuuwLkNGdxIfUfFOfA70ftiahsq7u +RVRl6AER97J1581Cq8GceoyihRZSB+LAA3TUIo7ERRGf523NAgMBAAECYGKA94a3 +PHY8xIX1Zk5iLbHMzMTXMC4yvPrIavPrvcbmvRDMiwDUwxldAw/2ojx0teVHsdxM +AJmQ7WIfEgseRdZIalQREOscdapzOM/UA3VIJwZkaAWbnTt7A6E52xavrQIxAPMv +eqvzIKX+GTCXWYVH5/ITebDVByxkS7xXk/1T66W7MqtDbI+RV083KkxcqSnm9wIx +ANd1uA3AjsrMm2kyA9BR1LqSF+2OioNc8ivuXofpi2HfAwf03JAxi2TNe8KfjNdM +WwIxAMF5SLN8nsDxQ8Ew6HvtFU+2xt403Zwnv0l8s7ScC6KzKECxR7GBBGF1liVi +owM/SQIxALGTpjQ/ZfCjHdtfi2EgcJGsYug5uNN3wWiPt32Q8H2jjh36o+Z7zwUv +jjbjxPKVTQIxANEUsYQBcq1dvpwfmIzy73vrZTIdCyCzgm12KIQHxKvaRiGLFcxD +3iNplWkDRezhLg== +-----END PRIVATE KEY----- diff --git a/examples/envoy-federated/docker/echo/conf/envoy.yaml b/examples/envoy-federated/docker/echo/conf/envoy.yaml new file mode 100644 index 0000000..2137ce0 --- /dev/null +++ b/examples/envoy-federated/docker/echo/conf/envoy.yaml @@ -0,0 +1,154 @@ +node: + id: "id_01" + cluster: "cluster_01" +static_resources: + listeners: + - name: listener-sds-mtls + address: + socket_address: + address: 0.0.0.0 + port_value: 8001 + filter_chains: + - filters: + - name: envoy.filters.network.http_connection_manager + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + common_http_protocol_options: + idle_timeout: 1s + forward_client_cert_details: sanitize_set + set_current_client_cert_details: + uri: true + codec_type: auto + access_log: + - name: envoy.access_loggers.file + typed_config: + "@type": type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog + path: "/tmp/envoy-access-8001.log" + stat_prefix: ingress_http + route_config: + name: local_route + virtual_hosts: + - name: local_service + domains: ["*"] + routes: + - match: + prefix: "/" + route: + cluster: echo_server + http_filters: + - name: envoy.filters.http.router + transport_socket: + name: envoy.transport_sockets.tls + typed_config: + "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext + common_tls_context: + tls_certificate_sds_secret_configs: + - name: "spiffe://domain.test/echo-server" + sds_config: + api_config_source: + api_type: GRPC + grpc_services: + envoy_grpc: + cluster_name: spire_agent + transport_api_version: V3 + resource_api_version: V3 + combined_validation_context: + # validate the SPIFFE ID of incoming clients (optionally) + default_validation_context: + match_subject_alt_names: + safe_regex: + google_re2: + max_program_size: 100 + regex: "spiffe://(domain|federated)\\.test/web-server" + # obtain the trust bundle from SDS + validation_context_sds_secret_config: + name: "ALL" + sds_config: + api_config_source: + api_type: GRPC + grpc_services: + envoy_grpc: + cluster_name: spire_agent + transport_api_version: V3 + resource_api_version: V3 + tls_params: + ecdh_curves: + - X25519:P-256:P-521:P-384 + - name: listener-sds-tls + address: + socket_address: + address: 0.0.0.0 + port_value: 8002 + filter_chains: + - filters: + - name: envoy.filters.network.http_connection_manager + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + common_http_protocol_options: + idle_timeout: 1s + forward_client_cert_details: sanitize_set + set_current_client_cert_details: + uri: true + codec_type: auto + access_log: + - name: envoy.access_loggers.file + typed_config: + "@type": type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog + path: "/tmp/envoy-access-8002.log" + stat_prefix: ingress_http + route_config: + name: local_route + virtual_hosts: + - name: local_service + domains: ["*"] + routes: + - match: + prefix: "/" + route: + cluster: echo_server + http_filters: + - name: envoy.filters.http.router + transport_socket: + name: envoy.transport_sockets.tls + typed_config: + "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext + common_tls_context: + tls_certificate_sds_secret_configs: + - name: "spiffe://domain.test/echo-server" + sds_config: + api_config_source: + api_type: GRPC + grpc_services: + envoy_grpc: + cluster_name: spire_agent + transport_api_version: V3 + resource_api_version: V3 + tls_params: + ecdh_curves: + - X25519:P-256:P-521:P-384 + + clusters: + - name: spire_agent + connect_timeout: 0.25s + http2_protocol_options: {} + load_assignment: + cluster_name: spire_agent + endpoints: + - lb_endpoints: + - endpoint: + address: + pipe: + path: /tmp/agent.sock + - name: echo_server + connect_timeout: 0.25s + type: strict_dns + lb_policy: ROUND_ROBIN + load_assignment: + cluster_name: echo_server + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: 127.0.0.1 + port_value: 8081 diff --git a/examples/envoy-federated/docker/echo/conf/spire-agent.conf b/examples/envoy-federated/docker/echo/conf/spire-agent.conf new file mode 100644 index 0000000..80f4c1d --- /dev/null +++ b/examples/envoy-federated/docker/echo/conf/spire-agent.conf @@ -0,0 +1,28 @@ +agent { + data_dir = "/opt/spire/data/agent" + log_level = "DEBUG" + log_file = "/opt/spire/agent.log" + server_address = "spire-server-domain" + server_port = "9081" + socket_path ="/tmp/agent.sock" + trust_bundle_path = "/opt/spire/conf/agent/bootstrap.crt" + trust_domain = "domain.test" +} + +plugins { + NodeAttestor "x509pop" { + plugin_data { + private_key_path = "/opt/spire/conf/agent/agent.key.pem" + certificate_path = "/opt/spire/conf/agent/agent.crt.pem" + } + } + KeyManager "disk" { + plugin_data { + directory = "/opt/spire/data/agent" + } + } + WorkloadAttestor "unix" { + plugin_data { + } + } +} diff --git a/examples/envoy-federated/docker/spire-server-domain/Dockerfile b/examples/envoy-federated/docker/spire-server-domain/Dockerfile new file mode 100644 index 0000000..2b78560 --- /dev/null +++ b/examples/envoy-federated/docker/spire-server-domain/Dockerfile @@ -0,0 +1,7 @@ +FROM gcr.io/spiffe-io/spire-server:1.0.2 + +# Override spire configurations +COPY conf/spire-server.conf /opt/spire/conf/server/server.conf +COPY conf/agent-cacert.pem /opt/spire/conf/server/agent-cacert.pem + +WORKDIR /opt/spire diff --git a/examples/envoy-federated/docker/spire-server-domain/conf/agent-cacert.pem b/examples/envoy-federated/docker/spire-server-domain/conf/agent-cacert.pem new file mode 100644 index 0000000..15185a5 --- /dev/null +++ b/examples/envoy-federated/docker/spire-server-domain/conf/agent-cacert.pem @@ -0,0 +1,9 @@ +-----BEGIN CERTIFICATE----- +MIIBRzCB0qADAgECAgEBMA0GCSqGSIb3DQEBCwUAMAAwIhgPMDAwMTAxMDEwMDAw +MDBaGA85OTk5MTIzMTIzNTk1OVowADB8MA0GCSqGSIb3DQEBAQUAA2sAMGgCYQC0 +8d4KBYOCzOUR2mGBA7/HDI+cprZkABrCtjv0HMPR5FeNo1XuLzHoKYB5saDqNWOW +I7KzvDOIvwXUEnPsHNe8CX41NrjocMc3BvqIMTAqUTaQwN/2bPc3IqQ1WEywGzEC +AwEAAaMTMBEwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAANhAHAKDl1a +u0pP0doHcYJcyKfYFbUPaPUvPZHfVQ2J7e6c1vrKVr2hAFQ2jFRKc6v3QEvGF7V4 +1PfudcS9A/GnXvU8uGjWiaXsz60OA9BskRWE7Gy1ZAc/70TvKp4/jKujrA== +-----END CERTIFICATE----- diff --git a/examples/envoy-federated/docker/spire-server-domain/conf/spire-server.conf b/examples/envoy-federated/docker/spire-server-domain/conf/spire-server.conf new file mode 100644 index 0000000..951897e --- /dev/null +++ b/examples/envoy-federated/docker/spire-server-domain/conf/spire-server.conf @@ -0,0 +1,47 @@ +server { + bind_address = "0.0.0.0" + bind_port = "9081" + socket_path = "/tmp/spire-server/private/api.sock" + trust_domain = "domain.test" + data_dir = "/opt/spire/data/server" + log_level = "DEBUG" + /* log_file = "/opt/spire/server.log" */ + default_svid_ttl = "1h" + ca_subject = { + country = ["US"], + organization = ["SPIFFE"], + common_name = "", + } + federation { + bundle_endpoint { + port = 8443 + } + + federates_with "federated.test" { + bundle_endpoint_url = "https://spire-server-federated:8443" + bundle_endpoint_profile "https_spiffe" { + endpoint_spiffe_id = "spiffe://federated.test/spire/server" + } + } + } +} + +plugins { + DataStore "sql" { + plugin_data { + database_type = "sqlite3" + connection_string = "/opt/spire/data/server/datastore.sqlite3" + } + } + NodeAttestor "x509pop" { + plugin_data { + ca_bundle_path = "/opt/spire/conf/server/agent-cacert.pem" + } + } + + KeyManager "disk" { + plugin_data = { + keys_path = "/opt/spire/data/server/keys.json" + } + } +} diff --git a/examples/envoy-federated/docker/spire-server-federated/Dockerfile b/examples/envoy-federated/docker/spire-server-federated/Dockerfile new file mode 100644 index 0000000..2b78560 --- /dev/null +++ b/examples/envoy-federated/docker/spire-server-federated/Dockerfile @@ -0,0 +1,7 @@ +FROM gcr.io/spiffe-io/spire-server:1.0.2 + +# Override spire configurations +COPY conf/spire-server.conf /opt/spire/conf/server/server.conf +COPY conf/agent-cacert.pem /opt/spire/conf/server/agent-cacert.pem + +WORKDIR /opt/spire diff --git a/examples/envoy-federated/docker/spire-server-federated/conf/agent-cacert.pem b/examples/envoy-federated/docker/spire-server-federated/conf/agent-cacert.pem new file mode 100644 index 0000000..15185a5 --- /dev/null +++ b/examples/envoy-federated/docker/spire-server-federated/conf/agent-cacert.pem @@ -0,0 +1,9 @@ +-----BEGIN CERTIFICATE----- +MIIBRzCB0qADAgECAgEBMA0GCSqGSIb3DQEBCwUAMAAwIhgPMDAwMTAxMDEwMDAw +MDBaGA85OTk5MTIzMTIzNTk1OVowADB8MA0GCSqGSIb3DQEBAQUAA2sAMGgCYQC0 +8d4KBYOCzOUR2mGBA7/HDI+cprZkABrCtjv0HMPR5FeNo1XuLzHoKYB5saDqNWOW +I7KzvDOIvwXUEnPsHNe8CX41NrjocMc3BvqIMTAqUTaQwN/2bPc3IqQ1WEywGzEC +AwEAAaMTMBEwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAANhAHAKDl1a +u0pP0doHcYJcyKfYFbUPaPUvPZHfVQ2J7e6c1vrKVr2hAFQ2jFRKc6v3QEvGF7V4 +1PfudcS9A/GnXvU8uGjWiaXsz60OA9BskRWE7Gy1ZAc/70TvKp4/jKujrA== +-----END CERTIFICATE----- diff --git a/examples/envoy-federated/docker/spire-server-federated/conf/spire-server.conf b/examples/envoy-federated/docker/spire-server-federated/conf/spire-server.conf new file mode 100644 index 0000000..c415e3f --- /dev/null +++ b/examples/envoy-federated/docker/spire-server-federated/conf/spire-server.conf @@ -0,0 +1,47 @@ +server { + bind_address = "0.0.0.0" + bind_port = "9081" + socket_path = "/tmp/spire-server/private/api.sock" + trust_domain = "federated.test" + data_dir = "/opt/spire/data/server" + log_level = "DEBUG" + /* log_file = "/opt/spire/server.log" */ + default_svid_ttl = "1h" + ca_subject = { + country = ["US"], + organization = ["SPIFFE"], + common_name = "", + } + federation { + bundle_endpoint { + port = 8443 + } + + federates_with "domain.test" { + bundle_endpoint_url = "https://spire-server-domain:8443" + bundle_endpoint_profile "https_spiffe" { + endpoint_spiffe_id = "spiffe://domain.test/spire/server" + } + } + } +} + +plugins { + DataStore "sql" { + plugin_data { + database_type = "sqlite3" + connection_string = "/opt/spire/data/server/datastore.sqlite3" + } + } + NodeAttestor "x509pop" { + plugin_data { + ca_bundle_path = "/opt/spire/conf/server/agent-cacert.pem" + } + } + + KeyManager "disk" { + plugin_data = { + keys_path = "/opt/spire/data/server/keys.json" + } + } +} diff --git a/examples/envoy-federated/docker/web-domain/Dockerfile b/examples/envoy-federated/docker/web-domain/Dockerfile new file mode 100644 index 0000000..858ef6a --- /dev/null +++ b/examples/envoy-federated/docker/web-domain/Dockerfile @@ -0,0 +1,15 @@ +FROM gcr.io/spiffe-io/spire-agent:1.0.2 as spire + +FROM envoyproxy/envoy-alpine:v1.19.0 +RUN mkdir -p /opt/spire/conf/agent +RUN mkdir -p /opt/spire/data/agent +COPY --from=spire /opt/spire/bin/spire-agent /opt/spire/bin/spire-agent +COPY conf/envoy.yaml /etc/envoy/envoy.yaml +COPY conf/spire-agent.conf /opt/spire/conf/agent/agent.conf +COPY conf/agent.key.pem /opt/spire/conf/agent/agent.key.pem +COPY conf/agent.crt.pem /opt/spire/conf/agent/agent.crt.pem +COPY web-server /usr/local/bin/web-server + +WORKDIR /opt/spire +CMD /usr/local/bin/envoy -l debug -c /etc/envoy/envoy.yaml --log-path /tmp/envoy.log + diff --git a/examples/envoy-federated/docker/web-domain/conf/agent.crt.pem b/examples/envoy-federated/docker/web-domain/conf/agent.crt.pem new file mode 100644 index 0000000..b99e1b5 --- /dev/null +++ b/examples/envoy-federated/docker/web-domain/conf/agent.crt.pem @@ -0,0 +1,10 @@ +-----BEGIN CERTIFICATE----- +MIIBZTCB8KADAgECAgEBMA0GCSqGSIb3DQEBCwUAMAAwIhgPMDAwMTAxMDEwMDAw +MDBaGA85OTk5MTIzMTIzNTk1OVowHzEdMBsGA1UEAxMUV2ViIFg1MDlQb3AgSWRl +bnRpdHkwfDANBgkqhkiG9w0BAQEFAANrADBoAmEAs1NjsJBicAi16HOTb7Qs0TbE +dGkknN+AeaeKyCBD1AeWszGIxKY0BGK35M7yT8xywfdA4iPVyoxniTRs2dbipJos +QE6MnC63lyroyP4fDT/IOhID19IkeNXp2oYt7JMLAgMBAAGjEjAQMA4GA1UdDwEB +/wQEAwIHgDANBgkqhkiG9w0BAQsFAANhAJtPzDYWP+ZqA+B0MNTb4yuhgm/8R5NL +cXO3+RHuJx2F6URezNRclubxZwarILTWgfeD/kwhSv+//4BfCIseSURNq/15/DnS +ns5ZoezRYSQhtYzQYsSKGeZWlrjLcYOj9Q== +-----END CERTIFICATE----- diff --git a/examples/envoy-federated/docker/web-domain/conf/agent.key.pem b/examples/envoy-federated/docker/web-domain/conf/agent.key.pem new file mode 100644 index 0000000..7f67a9f --- /dev/null +++ b/examples/envoy-federated/docker/web-domain/conf/agent.key.pem @@ -0,0 +1,13 @@ +-----BEGIN PRIVATE KEY----- +MIIB5QIBADANBgkqhkiG9w0BAQEFAASCAc8wggHLAgEAAmEAs1NjsJBicAi16HOT +b7Qs0TbEdGkknN+AeaeKyCBD1AeWszGIxKY0BGK35M7yT8xywfdA4iPVyoxniTRs +2dbipJosQE6MnC63lyroyP4fDT/IOhID19IkeNXp2oYt7JMLAgMBAAECYGQJkQNe +iIuXUGNslkMqcBYGR3HpddNNm45CONmLpksSMK+mYnXRghMwPwYVrHd3xV3mcURQ +9khZeYa/Xj9zB/FLr7jTiFo06BsF/Xi+USJyk9TbRSOMcLB70EnHMJBSgQIxANAA +Ul877XbRzW1FV2z0fUtQlzVjKDhsshAd4Bc+4eDol2jcTxmfwE/+S/ICf1lOrQIx +ANy1D5kvbHaS0QE1EhKTP1avajaz0ITwf/t398kxHAFnulgbMh5P06fqGOIwJLg3 +lwIxAKq2WOTIrpDCiLOSIbrOvra9ohyzESOeMlZ/7peliIWq3/m0N0d0kcYVlAdA +fXyy0QIwfPNo3FHT9yFRgHx6OdfxQPNYj5WnlMvPuR+HQfkyoL0H63+iFTUeX+8R +nWSiNUULAjEApWCigs9ycON1fOcwUdXgqm7PsfbtBslq512qA4AjfRnkwmwjeSYf +DZXZztuqcBW/ +-----END PRIVATE KEY----- diff --git a/examples/envoy-federated/docker/web-domain/conf/envoy.yaml b/examples/envoy-federated/docker/web-domain/conf/envoy.yaml new file mode 100644 index 0000000..e451824 --- /dev/null +++ b/examples/envoy-federated/docker/web-domain/conf/envoy.yaml @@ -0,0 +1,161 @@ +node: + id: "id_02" + cluster: "cluster_02" +static_resources: + listeners: + - name: web_to_echo_mtls_listener + address: + socket_address: + address: 127.0.0.1 + port_value: 8001 + filter_chains: + - filters: + - name: envoy.filters.network.http_connection_manager + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + common_http_protocol_options: + idle_timeout: 1s + codec_type: auto + access_log: + - name: envoy.access_loggers.file + typed_config: + "@type": type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog + path: "/tmp/envoy-to-envoy-mtls.log" + stat_prefix: ingress_http + route_config: + name: service_route + virtual_hosts: + - name: web_to_echo_mtls + domains: ["*"] + routes: + - match: + prefix: "/" + route: + cluster: web_to_echo_mtls + http_filters: + - name: envoy.filters.http.router + - name: web_to_echo_tls_listener + address: + socket_address: + address: 127.0.0.1 + port_value: 8002 + filter_chains: + - filters: + - name: envoy.filters.network.http_connection_manager + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + common_http_protocol_options: + idle_timeout: 1s + codec_type: auto + access_log: + - name: envoy.access_loggers.file + typed_config: + "@type": type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog + path: "/tmp/envoy-to-envoy-tls.log" + stat_prefix: ingress_http + route_config: + name: service_route + virtual_hosts: + - name: web_to_echo_tls + domains: ["*"] + routes: + - match: + prefix: "/" + route: + cluster: web_to_echo_tls + http_filters: + - name: envoy.filters.http.router + clusters: + - name: spire_agent + connect_timeout: 0.25s + http2_protocol_options: {} + load_assignment: + cluster_name: spire_agent + endpoints: + - lb_endpoints: + - endpoint: + address: + pipe: + path: /tmp/agent.sock + - name: web_to_echo_mtls + connect_timeout: 0.25s + type: strict_dns + lb_policy: ROUND_ROBIN + load_assignment: + cluster_name: web_to_echo_mtls + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: echo + port_value: 8001 + transport_socket: + name: envoy.transport_sockets.tls + typed_config: + "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext + common_tls_context: + tls_certificate_sds_secret_configs: + - name: "spiffe://domain.test/web-server" + sds_config: + api_config_source: + api_type: GRPC + grpc_services: + envoy_grpc: + cluster_name: spire_agent + transport_api_version: V3 + resource_api_version: V3 + combined_validation_context: + # validate the SPIFFE ID of the server (recommended) + default_validation_context: + match_subject_alt_names: + exact: "spiffe://domain.test/echo-server" + validation_context_sds_secret_config: + name: "ALL" + sds_config: + api_config_source: + api_type: GRPC + grpc_services: + envoy_grpc: + cluster_name: spire_agent + transport_api_version: V3 + resource_api_version: V3 + tls_params: + ecdh_curves: + - X25519:P-256:P-521:P-384 + - name: web_to_echo_tls + connect_timeout: 0.25s + type: strict_dns + lb_policy: ROUND_ROBIN + load_assignment: + cluster_name: web_to_echo_tls + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: echo + port_value: 8002 + transport_socket: + name: envoy.transport_sockets.tls + typed_config: + "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext + common_tls_context: + combined_validation_context: + # validate the SPIFFE ID of the server (recommended) + default_validation_context: + match_subject_alt_names: + exact: "spiffe://domain.test/echo-server" + validation_context_sds_secret_config: + name: "ALL" + sds_config: + api_config_source: + api_type: GRPC + grpc_services: + envoy_grpc: + cluster_name: spire_agent + transport_api_version: V3 + resource_api_version: V3 + tls_params: + ecdh_curves: + - X25519:P-256:P-521:P-384 diff --git a/examples/envoy-federated/docker/web-domain/conf/spire-agent.conf b/examples/envoy-federated/docker/web-domain/conf/spire-agent.conf new file mode 100644 index 0000000..80f4c1d --- /dev/null +++ b/examples/envoy-federated/docker/web-domain/conf/spire-agent.conf @@ -0,0 +1,28 @@ +agent { + data_dir = "/opt/spire/data/agent" + log_level = "DEBUG" + log_file = "/opt/spire/agent.log" + server_address = "spire-server-domain" + server_port = "9081" + socket_path ="/tmp/agent.sock" + trust_bundle_path = "/opt/spire/conf/agent/bootstrap.crt" + trust_domain = "domain.test" +} + +plugins { + NodeAttestor "x509pop" { + plugin_data { + private_key_path = "/opt/spire/conf/agent/agent.key.pem" + certificate_path = "/opt/spire/conf/agent/agent.crt.pem" + } + } + KeyManager "disk" { + plugin_data { + directory = "/opt/spire/data/agent" + } + } + WorkloadAttestor "unix" { + plugin_data { + } + } +} diff --git a/examples/envoy-federated/docker/web-federated/Dockerfile b/examples/envoy-federated/docker/web-federated/Dockerfile new file mode 100644 index 0000000..858ef6a --- /dev/null +++ b/examples/envoy-federated/docker/web-federated/Dockerfile @@ -0,0 +1,15 @@ +FROM gcr.io/spiffe-io/spire-agent:1.0.2 as spire + +FROM envoyproxy/envoy-alpine:v1.19.0 +RUN mkdir -p /opt/spire/conf/agent +RUN mkdir -p /opt/spire/data/agent +COPY --from=spire /opt/spire/bin/spire-agent /opt/spire/bin/spire-agent +COPY conf/envoy.yaml /etc/envoy/envoy.yaml +COPY conf/spire-agent.conf /opt/spire/conf/agent/agent.conf +COPY conf/agent.key.pem /opt/spire/conf/agent/agent.key.pem +COPY conf/agent.crt.pem /opt/spire/conf/agent/agent.crt.pem +COPY web-server /usr/local/bin/web-server + +WORKDIR /opt/spire +CMD /usr/local/bin/envoy -l debug -c /etc/envoy/envoy.yaml --log-path /tmp/envoy.log + diff --git a/examples/envoy-federated/docker/web-federated/conf/agent.crt.pem b/examples/envoy-federated/docker/web-federated/conf/agent.crt.pem new file mode 100644 index 0000000..b99e1b5 --- /dev/null +++ b/examples/envoy-federated/docker/web-federated/conf/agent.crt.pem @@ -0,0 +1,10 @@ +-----BEGIN CERTIFICATE----- +MIIBZTCB8KADAgECAgEBMA0GCSqGSIb3DQEBCwUAMAAwIhgPMDAwMTAxMDEwMDAw +MDBaGA85OTk5MTIzMTIzNTk1OVowHzEdMBsGA1UEAxMUV2ViIFg1MDlQb3AgSWRl +bnRpdHkwfDANBgkqhkiG9w0BAQEFAANrADBoAmEAs1NjsJBicAi16HOTb7Qs0TbE +dGkknN+AeaeKyCBD1AeWszGIxKY0BGK35M7yT8xywfdA4iPVyoxniTRs2dbipJos +QE6MnC63lyroyP4fDT/IOhID19IkeNXp2oYt7JMLAgMBAAGjEjAQMA4GA1UdDwEB +/wQEAwIHgDANBgkqhkiG9w0BAQsFAANhAJtPzDYWP+ZqA+B0MNTb4yuhgm/8R5NL +cXO3+RHuJx2F6URezNRclubxZwarILTWgfeD/kwhSv+//4BfCIseSURNq/15/DnS +ns5ZoezRYSQhtYzQYsSKGeZWlrjLcYOj9Q== +-----END CERTIFICATE----- diff --git a/examples/envoy-federated/docker/web-federated/conf/agent.key.pem b/examples/envoy-federated/docker/web-federated/conf/agent.key.pem new file mode 100644 index 0000000..7f67a9f --- /dev/null +++ b/examples/envoy-federated/docker/web-federated/conf/agent.key.pem @@ -0,0 +1,13 @@ +-----BEGIN PRIVATE KEY----- +MIIB5QIBADANBgkqhkiG9w0BAQEFAASCAc8wggHLAgEAAmEAs1NjsJBicAi16HOT +b7Qs0TbEdGkknN+AeaeKyCBD1AeWszGIxKY0BGK35M7yT8xywfdA4iPVyoxniTRs +2dbipJosQE6MnC63lyroyP4fDT/IOhID19IkeNXp2oYt7JMLAgMBAAECYGQJkQNe +iIuXUGNslkMqcBYGR3HpddNNm45CONmLpksSMK+mYnXRghMwPwYVrHd3xV3mcURQ +9khZeYa/Xj9zB/FLr7jTiFo06BsF/Xi+USJyk9TbRSOMcLB70EnHMJBSgQIxANAA +Ul877XbRzW1FV2z0fUtQlzVjKDhsshAd4Bc+4eDol2jcTxmfwE/+S/ICf1lOrQIx +ANy1D5kvbHaS0QE1EhKTP1avajaz0ITwf/t398kxHAFnulgbMh5P06fqGOIwJLg3 +lwIxAKq2WOTIrpDCiLOSIbrOvra9ohyzESOeMlZ/7peliIWq3/m0N0d0kcYVlAdA +fXyy0QIwfPNo3FHT9yFRgHx6OdfxQPNYj5WnlMvPuR+HQfkyoL0H63+iFTUeX+8R +nWSiNUULAjEApWCigs9ycON1fOcwUdXgqm7PsfbtBslq512qA4AjfRnkwmwjeSYf +DZXZztuqcBW/ +-----END PRIVATE KEY----- diff --git a/examples/envoy-federated/docker/web-federated/conf/envoy.yaml b/examples/envoy-federated/docker/web-federated/conf/envoy.yaml new file mode 100644 index 0000000..ad6c894 --- /dev/null +++ b/examples/envoy-federated/docker/web-federated/conf/envoy.yaml @@ -0,0 +1,161 @@ +node: + id: "id_02" + cluster: "cluster_02" +static_resources: + listeners: + - name: web_to_echo_mtls_listener + address: + socket_address: + address: 127.0.0.1 + port_value: 8001 + filter_chains: + - filters: + - name: envoy.filters.network.http_connection_manager + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + common_http_protocol_options: + idle_timeout: 1s + codec_type: auto + access_log: + - name: envoy.access_loggers.file + typed_config: + "@type": type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog + path: "/tmp/envoy-to-envoy-mtls.log" + stat_prefix: ingress_http + route_config: + name: service_route + virtual_hosts: + - name: web_to_echo_mtls + domains: ["*"] + routes: + - match: + prefix: "/" + route: + cluster: web_to_echo_mtls + http_filters: + - name: envoy.filters.http.router + - name: web_to_echo_tls_listener + address: + socket_address: + address: 127.0.0.1 + port_value: 8002 + filter_chains: + - filters: + - name: envoy.filters.network.http_connection_manager + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + common_http_protocol_options: + idle_timeout: 1s + codec_type: auto + access_log: + - name: envoy.access_loggers.file + typed_config: + "@type": type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog + path: "/tmp/envoy-to-envoy-tls.log" + stat_prefix: ingress_http + route_config: + name: service_route + virtual_hosts: + - name: web_to_echo_tls + domains: ["*"] + routes: + - match: + prefix: "/" + route: + cluster: web_to_echo_tls + http_filters: + - name: envoy.filters.http.router + clusters: + - name: spire_agent + connect_timeout: 0.25s + http2_protocol_options: {} + load_assignment: + cluster_name: spire_agent + endpoints: + - lb_endpoints: + - endpoint: + address: + pipe: + path: /tmp/agent.sock + - name: web_to_echo_mtls + connect_timeout: 0.25s + type: strict_dns + lb_policy: ROUND_ROBIN + load_assignment: + cluster_name: web_to_echo_mtls + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: echo + port_value: 8001 + transport_socket: + name: envoy.transport_sockets.tls + typed_config: + "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext + common_tls_context: + tls_certificate_sds_secret_configs: + - name: "spiffe://federated.test/web-server" + sds_config: + api_config_source: + api_type: GRPC + grpc_services: + envoy_grpc: + cluster_name: spire_agent + transport_api_version: V3 + resource_api_version: V3 + combined_validation_context: + # validate the SPIFFE ID of the server (recommended) + default_validation_context: + match_subject_alt_names: + exact: "spiffe://domain.test/echo-server" + validation_context_sds_secret_config: + name: "ALL" + sds_config: + api_config_source: + api_type: GRPC + grpc_services: + envoy_grpc: + cluster_name: spire_agent + transport_api_version: V3 + resource_api_version: V3 + tls_params: + ecdh_curves: + - X25519:P-256:P-521:P-384 + - name: web_to_echo_tls + connect_timeout: 0.25s + type: strict_dns + lb_policy: ROUND_ROBIN + load_assignment: + cluster_name: web_to_echo_tls + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: echo + port_value: 8002 + transport_socket: + name: envoy.transport_sockets.tls + typed_config: + "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext + common_tls_context: + combined_validation_context: + # validate the SPIFFE ID of the server (recommended) + default_validation_context: + match_subject_alt_names: + exact: "spiffe://domain.test/echo-server" + validation_context_sds_secret_config: + name: "ALL" + sds_config: + api_config_source: + api_type: GRPC + grpc_services: + envoy_grpc: + cluster_name: spire_agent + transport_api_version: V3 + resource_api_version: V3 + tls_params: + ecdh_curves: + - X25519:P-256:P-521:P-384 diff --git a/examples/envoy-federated/docker/web-federated/conf/spire-agent.conf b/examples/envoy-federated/docker/web-federated/conf/spire-agent.conf new file mode 100644 index 0000000..e20e5ef --- /dev/null +++ b/examples/envoy-federated/docker/web-federated/conf/spire-agent.conf @@ -0,0 +1,28 @@ +agent { + data_dir = "/opt/spire/data/agent" + log_level = "DEBUG" + log_file = "/opt/spire/agent.log" + server_address = "spire-server-federated" + server_port = "9081" + socket_path ="/tmp/agent.sock" + trust_bundle_path = "/opt/spire/conf/agent/bootstrap.crt" + trust_domain = "federated.test" +} + +plugins { + NodeAttestor "x509pop" { + plugin_data { + private_key_path = "/opt/spire/conf/agent/agent.key.pem" + certificate_path = "/opt/spire/conf/agent/agent.crt.pem" + } + } + KeyManager "disk" { + plugin_data { + directory = "/opt/spire/data/agent" + } + } + WorkloadAttestor "unix" { + plugin_data { + } + } +} diff --git a/examples/envoy-federated/gencerts.go b/examples/envoy-federated/gencerts.go new file mode 100644 index 0000000..1cf3e03 --- /dev/null +++ b/examples/envoy-federated/gencerts.go @@ -0,0 +1,106 @@ +package main + +import ( + "bytes" + "crypto/rand" + "crypto/rsa" + "crypto/sha1" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "fmt" + "io/ioutil" + "math/big" + "os" + "path/filepath" + "time" +) + +var ( + // The "never expires" timestamp from RFC5280. Probably not a good + // idea in practice. + neverExpires = time.Date(9999, 12, 31, 23, 59, 59, 0, time.UTC) +) + +func main() { + if len(os.Args) < 2 { + fmt.Fprintln(os.Stderr, "usage: gencerts NAME1 [NAME2...]") + os.Exit(1) + } + + caKey := generateRSAKey() + caCert := createRootCertificate(caKey, &x509.Certificate{ + SerialNumber: big.NewInt(1), + BasicConstraintsValid: true, + IsCA: true, + NotAfter: neverExpires, + Subject: pkix.Name{CommonName: "agent CA"}, + }) + + writeCerts(filepath.Join("docker", "spire-server", "conf", "agent-cacert.pem"), caCert) + + for _, name := range os.Args[1:] { + agentKey := generateRSAKey() + agentCert := createCertificate(agentKey, &x509.Certificate{ + SerialNumber: big.NewInt(1), + KeyUsage: x509.KeyUsageDigitalSignature, + NotAfter: neverExpires, + Subject: pkix.Name{CommonName: fmt.Sprintf("%s agent", name)}, + }, caKey, caCert) + + dir := filepath.Join("docker", name, "conf") + writeKey(filepath.Join(dir, "agent.key.pem"), agentKey) + writeCerts(filepath.Join(dir, "agent.crt.pem"), agentCert) + + fingerprint := sha1.Sum(agentCert.Raw) + fmt.Printf("%x %s\n", fingerprint, name) + } +} + +func createRootCertificate(key *rsa.PrivateKey, tmpl *x509.Certificate) *x509.Certificate { + return createCertificate(key, tmpl, key, tmpl) +} + +func createCertificate(key *rsa.PrivateKey, tmpl *x509.Certificate, parentKey *rsa.PrivateKey, parent *x509.Certificate) *x509.Certificate { + certDER, err := x509.CreateCertificate(rand.Reader, tmpl, parent, &key.PublicKey, parentKey) + checkErr(err) + cert, err := x509.ParseCertificate(certDER) + checkErr(err) + return cert +} + +func generateRSAKey() *rsa.PrivateKey { + key, err := rsa.GenerateKey(rand.Reader, 768) + checkErr(err) + return key +} + +func writeKey(path string, key interface{}) { + keyBytes, err := x509.MarshalPKCS8PrivateKey(key) + checkErr(err) + pemBytes := pem.EncodeToMemory(&pem.Block{ + Type: "PRIVATE KEY", + Bytes: keyBytes, + }) + err = ioutil.WriteFile(path, pemBytes, 0600) + checkErr(err) +} + +func writeCerts(path string, certs ...*x509.Certificate) { + data := new(bytes.Buffer) + for _, cert := range certs { + err := pem.Encode(data, &pem.Block{ + Type: "CERTIFICATE", + Bytes: cert.Raw, + }) + checkErr(err) + } + err := ioutil.WriteFile(path, data.Bytes(), 0644) + checkErr(err) +} + +func checkErr(err error) { + if err != nil { + panic(err) + } +} diff --git a/examples/envoy-federated/images/envoy-federated.png b/examples/envoy-federated/images/envoy-federated.png new file mode 100644 index 0000000..2de2705 Binary files /dev/null and b/examples/envoy-federated/images/envoy-federated.png differ diff --git a/examples/envoy-federated/src/echo-server/go.mod b/examples/envoy-federated/src/echo-server/go.mod new file mode 100644 index 0000000..98e9fa3 --- /dev/null +++ b/examples/envoy-federated/src/echo-server/go.mod @@ -0,0 +1,3 @@ +module echo-server + +go 1.14 diff --git a/examples/envoy-federated/src/echo-server/main.go b/examples/envoy-federated/src/echo-server/main.go new file mode 100644 index 0000000..255cb88 --- /dev/null +++ b/examples/envoy-federated/src/echo-server/main.go @@ -0,0 +1,66 @@ +package main + +import ( + "context" + "encoding/json" + "flag" + "fmt" + "log" + "net" + "net/http" + "os" +) + +var ( + addrFlag = flag.String("addr", ":8081", "address to bind the echo server to") + logFlag = flag.String("log", "", "path to log to (empty=stderr)") +) + +func handler(w http.ResponseWriter, r *http.Request) { + log.Printf("[%s] %s %s", r.RemoteAddr, r.Method, r.URL) + responseBytes, err := json.Marshal(r.Header) + if err != nil { + http.Error(w, "unable to marshal response", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Connection", "close") + w.Write(responseBytes) +} + +func main() { + if err := run(context.Background()); err != nil { + fmt.Fprintf(os.Stderr, "%+v\n", err) + os.Exit(1) + } +} + +func run(ctx context.Context) (err error) { + flag.Parse() + log.SetPrefix("echo> ") + log.SetFlags(log.Ltime) + if *logFlag != "" { + logFile, err := os.OpenFile(*logFlag, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644) + if err != nil { + return fmt.Errorf("unable to open log file: %v", err) + } + defer logFile.Close() + log.SetOutput(logFile) + } else { + log.SetOutput(os.Stdout) + } + log.Printf("starting echo server...") + + ln, err := net.Listen("tcp", *addrFlag) + if err != nil { + return fmt.Errorf("unable to listen: %v", err) + } + defer ln.Close() + + log.Printf("listening on %s...", ln.Addr()) + server := &http.Server{ + Handler: http.HandlerFunc(handler), + } + return server.Serve(ln) +} diff --git a/examples/envoy-federated/src/web-server/css.go b/examples/envoy-federated/src/web-server/css.go new file mode 100644 index 0000000..3fe4e92 --- /dev/null +++ b/examples/envoy-federated/src/web-server/css.go @@ -0,0 +1,102 @@ +package main + +import "net/http" + +func serveCSS(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/css") + w.Write([]byte(` +body {background-color: 999999;} +button { + -webkit-transition-duration: 0.4s; /* Safari */ + transition-duration: 0.4s; + background-color: #333333; + background-color: #02bdd9; + border-radius: 8px; + border: none; + color: white; + padding: 15px 32px; + text-align: center; + text-decoration: none; + display: inline-block; + float: none; + font-size: 24px; + font-weight: bold; + text-shadow: 1px 1px 1px #000000; + width: 40%; +} +button:hover { + background-color: #029db9; + color: white; +} +.footer { + position: fixed; + left: 0; + bottom: 0; + height: 40px; + padding-left: 12px; + line-height: 40px; + width: 100%; + background-color: #333333; + color: white; + text-align: left; +} +.error-text{ + font-family: monospace; + font-size: 24px; + white-space: pre-wrap; + hyphens: auto; + word-break: break-word; + word-wrap: break-word; + overflow-wrap: break-word; +} +.header-title { + font-size: 24px; +} +.header-key { + font-family: monospace; + font-size: 24px; + white-space: pre; +} +.header-value { + font-family: monospace; + font-size: 24px; + white-space: pre-wrap; /* css-3 */ + hyphens: auto; + word-break: break-all; + word-wrap: break-word; + overflow-wrap: break-word; +} +.header-grid-container { + display: inline-grid; + grid-template-columns: min-content auto; +} +.button-grid-container { + width: 100%; + display: inline-grid; + grid-gap: 10px; +} +#success { + background-color: #bcd819; +} +#error { + background-color: #e32d31; + color: white; +} +.card { + /* Add shadows to create the "card" effect */ + box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2); + transition: 0.3s; + width: 100%; +} + +/* On mouse-over, add a deeper shadow */ +.card:hover { + box-shadow: 0 8px 16px 0 rgba(0,0,0,0.2); +} + +/* Add some padding inside the card container */ +.container { + padding: 2px 16px; +} +`)) +} diff --git a/examples/envoy-federated/src/web-server/go.mod b/examples/envoy-federated/src/web-server/go.mod new file mode 100644 index 0000000..7f032bd --- /dev/null +++ b/examples/envoy-federated/src/web-server/go.mod @@ -0,0 +1,5 @@ +module web-server + +require github.com/go-chi/chi v4.1.2+incompatible + +go 1.14 diff --git a/examples/envoy-federated/src/web-server/go.sum b/examples/envoy-federated/src/web-server/go.sum new file mode 100644 index 0000000..fa1444a --- /dev/null +++ b/examples/envoy-federated/src/web-server/go.sum @@ -0,0 +1,2 @@ +github.com/go-chi/chi v4.1.2+incompatible h1:fGFk2Gmi/YKXk0OmGfBh0WgmN3XB8lVnEyNz34tQRec= +github.com/go-chi/chi v4.1.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= diff --git a/examples/envoy-federated/src/web-server/main.go b/examples/envoy-federated/src/web-server/main.go new file mode 100644 index 0000000..b41f7ae --- /dev/null +++ b/examples/envoy-federated/src/web-server/main.go @@ -0,0 +1,245 @@ +package main + +import ( + "context" + "encoding/json" + "flag" + "fmt" + "html/template" + "io" + "log" + "net" + "net/http" + "os" + "sync/atomic" + "time" + + "github.com/go-chi/chi" +) + +var ( + addrFlag = flag.String("addr", ":8080", "address to bind the web server to") + directFlag = flag.String("direct", "echo:8081", "address to the echo server") + envoyMTLSFlag = flag.String("envoy-to-envoy-mtls", "localhost:8001", "address to the envoy-to-envoy mTLS endpoint") + envoyTLSFlag = flag.String("envoy-to-envoy-tls", "localhost:8002", "address to the envoy-to-envoy TLS endpoint") + logFlag = flag.String("log", "", "path to log to (empty=stderr)") + + pageHTML = template.Must(template.New("").Parse(` + + + + +
+
+ +
+
+ +
+
+ +
+
+
+{{- if .WebHeader }} +
+
+
+

Headers Sent by Web Server

+
+{{- range $k, $v := .WebHeader }} +
+
{{ printf "%-30s: " $k }}
+
+
+
{{- range $i, $v := $v }}{{ if $i }},{{ end }}{{ $v }}{{ end }}
+
+{{- end }} +
+
+
+
+{{- end }} + +{{- if .EchoHeader }} +
+
+
+

Headers Received by Echo Server

+
+{{- range $k, $v := .EchoHeader }} +
+
{{ printf "%-30s: " $k }}
+
+
+
{{- range $i, $v := $v }}{{ if $i }},{{ end }}{{ $v }}{{ end }}
+
+{{- end }} +
+
+
+
+{{- end }} + +{{- if .Error }} +
+
+
+

Error:

+
{{ .Error }}
+
+
+
+{{- end }} +{{- if .Attempt }} + +{{- end }} + + +`)) + attempt int32 = 0 +) + +func serveRoot(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/" { + http.NotFound(w, r) + return + } + + var host string + route := r.URL.Query().Get("route") + switch route { + case "": + pageHTML.Execute(w, successParams(0, nil, nil)) + return + case "direct": + host = *directFlag + case "envoy-to-envoy-mtls": + host = *envoyMTLSFlag + case "envoy-to-envoy-tls": + host = *envoyTLSFlag + default: + pageHTML.Execute(w, errorParams(attempt, nil, "invalid route value %q", route)) + return + } + + echoURL := fmt.Sprintf("http://%s", host) + + atomic.AddInt32(&attempt, 1) + w.Header().Set("Content-Type", "text/html") + + req, err := http.NewRequest("GET", echoURL, nil) + if err != nil { + log.Printf("[%s] failed to create request for echo server %q: %v", r.RemoteAddr, route, err) + pageHTML.Execute(w, errorParams(attempt, nil, "failed to create request: %v", err)) + return + } + req.Header.Set("Date", httpDate(time.Now())) + req.Header.Set("User-Agent", "") + req.Header.Set("X-Super-Secret-Password", "hunter2") + + log.Printf("[%s] Issuing GET %s", r.RemoteAddr, echoURL) + resp, err := http.DefaultClient.Do(req) + req.Header.Del("User-Agent") + if err != nil { + log.Printf("[%s] failed to send request to echo server %q: %v", r.RemoteAddr, route, err) + pageHTML.Execute(w, errorParams(attempt, req.Header, "failed to contact echo server: %v", err)) + return + } + defer resp.Body.Close() + log.Printf("[%s] GOT %s", r.RemoteAddr, echoURL) + + if resp.StatusCode != http.StatusOK { + body := tryRead(resp.Body) + log.Printf("[%s] unexpected echo server %q response: %d\n%s", r.RemoteAddr, route, resp.StatusCode, body) + pageHTML.Execute(w, errorParams(attempt, req.Header, "unexpected echo server response status: %d\n\n%s", resp.StatusCode, body)) + return + } + + var header http.Header + if err := json.NewDecoder(resp.Body).Decode(&header); err != nil { + log.Printf("[%s] failed to parse echo server %q response: %v", r.RemoteAddr, route, err) + pageHTML.Execute(w, errorParams(attempt, req.Header, "failed to parse echo server response: %v", err)) + return + } + + header.Del("Accept-Encoding") + + log.Printf("[%s] echo server %q response OK", r.RemoteAddr, route) + pageHTML.Execute(w, successParams(attempt, req.Header, header)) +} + +func successParams(attempt int32, webHeader, echoHeader http.Header) map[string]interface{} { + return map[string]interface{}{ + "Attempt": attempt, + "WebHeader": webHeader, + "EchoHeader": echoHeader, + } +} + +func errorParams(attempt int32, webHeader http.Header, format string, args ...interface{}) map[string]interface{} { + return map[string]interface{}{ + "Attempt": attempt, + "WebHeader": webHeader, + "Error": fmt.Sprintf(format, args...), + } +} + +func tryRead(r io.Reader) string { + b := make([]byte, 1024) + n, _ := r.Read(b) + return string(b[:n]) +} + +func main() { + if err := run(context.Background()); err != nil { + fmt.Fprintf(os.Stderr, "%+v\n", err) + os.Exit(1) + } +} + +func run(ctx context.Context) (err error) { + flag.Parse() + log.SetPrefix("web> ") + log.SetFlags(log.Ltime) + if *logFlag != "" { + logFile, err := os.OpenFile(*logFlag, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644) + if err != nil { + return fmt.Errorf("unable to open log file: %v", err) + } + defer logFile.Close() + log.SetOutput(logFile) + } else { + log.SetOutput(os.Stdout) + } + log.Printf("starting web server...") + + ln, err := net.Listen("tcp", *addrFlag) + if err != nil { + return fmt.Errorf("unable to listen: %v", err) + } + defer ln.Close() + + r := chi.NewRouter() + r.Use(noCache) + r.Get("/styles.css", http.HandlerFunc(serveCSS)) + r.Get("/", http.HandlerFunc(serveRoot)) + + log.Printf("listening on %s...", ln.Addr()) + server := &http.Server{ + Handler: r, + } + return server.Serve(ln) +} + +func noCache(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Cache-Control", "no-cache") + w.Header().Set("Expires", "0") + h.ServeHTTP(w, r) + }) +} + +func httpDate(t time.Time) string { + return t.UTC().Format("Mon, 02 Jan 2006 15:04:05 GMT") +} diff --git a/examples/envoy-federated/test.sh b/examples/envoy-federated/test.sh new file mode 100755 index 0000000..f19517b --- /dev/null +++ b/examples/envoy-federated/test.sh @@ -0,0 +1,127 @@ +#!/bin/bash + +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +bold=$(tput bold) || true +norm=$(tput sgr0) || true +red=$(tput setaf 1) || true +green=$(tput setaf 2) || true + +search_occurrences() { + echo "$1" | grep -e "$2" +} + +cleanup() { + echo "${bold}Cleaning up...${norm}" + docker-compose -f "${DIR}"/docker-compose.yml down +} + +set_env() { + "${DIR}"/build.sh > /dev/null + docker-compose -f "${DIR}"/docker-compose.yml up -d + "${DIR}"/1-start-services.sh + "${DIR}"/2-start-spire-agents.sh + "${DIR}"/3-create-registration-entries.sh > /dev/null +} + +run_test() { + PORT=$1 + CLIENT_SPIFFE_ID=$2 + + SECRET_PASS_HEADER="X-Super-Secret-Password" + LOCAL_URL="http://localhost:${PORT}" + DIRECT_RESPONSE=$(curl -s ${LOCAL_URL}/?route=direct) + + if [[ "$( echo "$(search_occurrences "$DIRECT_RESPONSE" "$SECRET_PASS_HEADER")" | wc -l)" -eq 2 ]] ; then + echo "${green}Direct connection test succeded${norm}" + else + echo "${red}Direct connection test failed${norm}" + FAILED=true + fi + if [ -n "${FAILED}" ]; then + echo "${red}There were test failures${norm}" + exit 1 + fi + + EXPECTED_TIMEOUT_HEADER="X-Envoy-Expected-Rq-Timeout-Ms" + FORWARDED_PROTOCOL_HEADER="X-Forwarded-Proto" + REQUEST_ID_HEADER="X-Request-Id" + + for i in {1..5}; do + TLS_RESPONSE=$(curl -s ${LOCAL_URL}/?route=envoy-to-envoy-tls) + + if [[ $( echo "$(search_occurrences "$TLS_RESPONSE" "$SECRET_PASS_HEADER")" | wc -l) -eq 2 ]] && + [[ -n $(search_occurrences "$TLS_RESPONSE" "$EXPECTED_TIMEOUT_HEADER") ]] && + [[ -n $(search_occurrences "$TLS_RESPONSE" "$FORWARDED_PROTOCOL_HEADER") ]] && + [[ -n $(search_occurrences "$TLS_RESPONSE" "$REQUEST_ID_HEADER") ]]; then + echo "${green}Envoy to Envoy TLS connection test succeded${norm}" + FAILED= + break + else + echo "${red}Envoy to Envoy TLS connection test failed, retry...${norm}" + sleep 5 + FAILED=true + fi + done + if [ -n "${FAILED}" ]; then + echo "${red}There were test failures${norm}" + exit 1 + fi + + FORWARDED_CLIENT_CERT_HEADER="X-Forwarded-Client-Cert" + for i in {1..5}; do + MTLS_RESPONSE=$(curl -s ${LOCAL_URL}/?route=envoy-to-envoy-mtls) + + if [[ "$( echo "$(search_occurrences "$MTLS_RESPONSE" "$SECRET_PASS_HEADER")" | wc -l)" -eq 2 ]] && + [[ -n $(search_occurrences "$MTLS_RESPONSE" "$EXPECTED_TIMEOUT_HEADER") ]] && + [[ -n $(search_occurrences "$MTLS_RESPONSE" "$FORWARDED_PROTOCOL_HEADER") ]] && + [[ -n $(search_occurrences "$MTLS_RESPONSE" "$REQUEST_ID_HEADER") ]] && + [[ -n $(search_occurrences "$MTLS_RESPONSE" "$FORWARDED_CLIENT_HEADER") ]] && + [[ -n $(search_occurrences "$MTLS_RESPONSE" "$CLIENT_SPIFFE_ID") ]]; then + echo "${green}Envoy to Envoy mTLS connection test succeded${norm}" + FAILED= + break + else + echo "${red}Envoy to Envoy mTLS connection test failed${norm}" + sleep 5 + FAILED=true + fi + done + if [ -n "${FAILED}" ]; then + echo "${red}There were test failures${norm}" + exit 1 + fi +} + +trap cleanup EXIT + +echo "${bold}Preparing environment...${norm}" + +cleanup +set_env + +ECHO_CONTAINER_ID=$(docker container ls -qf "name=echo") +WEB_DOMAIN_CONTAINER_ID=$(docker container ls -qf "name=web-domain") +echo $WEB_DOMAIN_CONTAINER_ID +WEB_FEDERATED_CONTAINER_ID=$(docker container ls -qf "name=web-federated") +echo $WEB_FEDERATED_CONTAINER_ID + +docker cp "${DIR}"/wait-for-envoy.sh "$ECHO_CONTAINER_ID":/tmp +docker cp "${DIR}"/wait-for-envoy.sh "$WEB_DOMAIN_CONTAINER_ID":/tmp +docker cp "${DIR}"/wait-for-envoy.sh "$WEB_FEDERATED_CONTAINER_ID":/tmp + +echo "${bold}Waiting for envoy...${norm}" + +docker exec -w /tmp "$ECHO_CONTAINER_ID" sh wait-for-envoy.sh Echo +docker exec -w /tmp "$WEB_DOMAIN_CONTAINER_ID" sh wait-for-envoy.sh "Web domain" +docker exec -w /tmp "$WEB_FEDERATED_CONTAINER_ID" sh wait-for-envoy.sh "Web federated" + +echo "${bold}Running domain test...${norm}" +run_test "8080" "spiffe://domain.test/web-server" +sleep 10 + +echo "${bold}Running federated test...${norm}" +run_test "8088" "spiffe://federated.test/web-server" + +echo "${green}Test passed!${norm}" +exit 0 diff --git a/examples/envoy-federated/wait-for-envoy.sh b/examples/envoy-federated/wait-for-envoy.sh new file mode 100644 index 0000000..119fbe5 --- /dev/null +++ b/examples/envoy-federated/wait-for-envoy.sh @@ -0,0 +1,27 @@ +#!/bin/sh + +bold='\033[1;37m' +norm='\033[0m' +red='\033[0;31m' + +i=0 +while [ "$i" -lt 30 ]; do + i=$(( i+1 )) + LOGLINE="all dependencies initialized. starting workers" + + if [ -z "$(grep "${LOGLINE}" envoy.log)" ]; then + echo "${1}'s envoy is not ready yet, sleeping for a while..." + sleep 5 + continue + fi + + echo -e "${bold}${1}'s envoy is ready!${norm}" + ENVOY_READY=true + break +done + +if [ -z "${ENVOY_READY}" ]; then + echo "${red}Timed out waiting for ${1}'s envoy${norm}" + exit 1 +fi +exit 0