github.com/pf-qiu/concourse/v6@v6.7.3-0.20201207032516-1f455d73275f/topgun/core/vault_test.go (about)

     1  package topgun_test
     2  
     3  import (
     4  	"fmt"
     5  	"io/ioutil"
     6  	"os"
     7  	"regexp"
     8  	"strings"
     9  	"sync"
    10  	"time"
    11  
    12  	"github.com/onsi/gomega/gbytes"
    13  	"github.com/onsi/gomega/gexec"
    14  
    15  	. "github.com/pf-qiu/concourse/v6/topgun"
    16  	. "github.com/pf-qiu/concourse/v6/topgun/common"
    17  	. "github.com/onsi/ginkgo"
    18  	. "github.com/onsi/gomega"
    19  )
    20  
    21  var _ = Describe("Vault", func() {
    22  	var tokenDuration = 30 * time.Second
    23  
    24  	BeforeEach(func() {
    25  		if !strings.Contains(string(Bosh("releases").Out.Contents()), "vault") {
    26  			Skip("vault release not uploaded")
    27  		}
    28  	})
    29  
    30  	Describe("A deployment with vault", func() {
    31  		var (
    32  			v             *vault
    33  			varsStore     *os.File
    34  			vaultInstance *BoshInstance
    35  		)
    36  
    37  		BeforeEach(func() {
    38  			var err error
    39  
    40  			varsStore, err = ioutil.TempFile("", "vars-store.yml")
    41  			Expect(err).ToNot(HaveOccurred())
    42  			Expect(varsStore.Close()).To(Succeed())
    43  
    44  			Deploy(
    45  				"deployments/concourse.yml",
    46  				"-o", "operations/add-vault.yml",
    47  				"-v", "web_instances=0",
    48  				"-v", "vault_url=dontcare",
    49  				"-v", "vault_client_token=dontcare",
    50  				"-v", "vault_auth_backend=dontcare",
    51  				"-v", "vault_auth_params=dontcare",
    52  			)
    53  
    54  			vaultInstance = JobInstance("vault")
    55  			v = newVault(vaultInstance.IP)
    56  
    57  			v.Run("policy-write", "concourse", "vault/concourse-policy.hcl")
    58  		})
    59  
    60  		AfterEach(func() {
    61  			Expect(os.Remove(varsStore.Name())).To(Succeed())
    62  		})
    63  
    64  		testTokenRenewal := func() {
    65  			Context("when enough time has passed such that token would have expired", func() {
    66  				BeforeEach(func() {
    67  					v.Run("write", "concourse/main/team_secret", "value=some_team_secret")
    68  					v.Run("write", "concourse/main/resource_version", "value=some_exposed_version_secret")
    69  
    70  					By("waiting for long enough that the initial token would have expired")
    71  					time.Sleep(tokenDuration)
    72  				})
    73  
    74  				It("renews the token", func() {
    75  					watch := Fly.StartWithEnv(
    76  						[]string{
    77  							"EXPECTED_TEAM_SECRET=some_team_secret",
    78  							"EXPECTED_RESOURCE_VERSION_SECRET=some_exposed_version_secret",
    79  						},
    80  						"execute", "-c", "tasks/credential-management.yml",
    81  					)
    82  					Wait(watch)
    83  					Expect(watch).To(gbytes.Say("all credentials matched expected values"))
    84  				})
    85  			})
    86  		}
    87  
    88  		Context("with token auth", func() {
    89  			BeforeEach(func() {
    90  				By("creating a periodic token")
    91  				create := v.Run("token-create", "-period", tokenDuration.String(), "-policy", "concourse")
    92  				content := string(create.Out.Contents())
    93  				token := regexp.MustCompile(`token\s+(.*)`).FindStringSubmatch(content)[1]
    94  
    95  				By("renewing the token throughout the deploy")
    96  				renewing := new(sync.WaitGroup)
    97  				stopRenewing := make(chan struct{})
    98  
    99  				defer func() {
   100  					By("not renewing the token anymore, leaving it to Concourse")
   101  					close(stopRenewing)
   102  					renewing.Wait()
   103  				}()
   104  
   105  				renewTicker := time.NewTicker(5 * time.Second)
   106  				renewing.Add(1)
   107  				go func() {
   108  					defer renewing.Done()
   109  					defer GinkgoRecover()
   110  
   111  					for {
   112  						select {
   113  						case <-renewTicker.C:
   114  							v.Run("token-renew", token)
   115  						case <-stopRenewing:
   116  							return
   117  						}
   118  					}
   119  				}()
   120  
   121  				By("deploying concourse with the token")
   122  				Deploy(
   123  					"deployments/concourse.yml",
   124  					"-o", "operations/add-vault.yml",
   125  					"--vars-store", varsStore.Name(),
   126  					"-v", "vault_url="+v.URI(),
   127  					"-v", "vault_ip="+v.IP(),
   128  					"-v", "web_instances=1",
   129  					"-v", "vault_client_token="+token,
   130  					"-v", `vault_auth_backend=""`,
   131  					"-v", "vault_auth_params={}",
   132  				)
   133  			})
   134  
   135  			testCredentialManagement(func() {
   136  				v.Run("write", "concourse/main/team_secret", "value=some_team_secret")
   137  				v.Run("write", "concourse/main/pipeline-creds-test/assertion_script", "value="+assertionScript)
   138  				v.Run("write", "concourse/main/pipeline-creds-test/canary", "value=some_canary")
   139  				v.Run("write", "concourse/main/pipeline-creds-test/resource_type_secret", "value=some_resource_type_secret")
   140  				v.Run("write", "concourse/main/pipeline-creds-test/resource_secret", "value=some_resource_secret")
   141  				v.Run("write", "concourse/main/pipeline-creds-test/job_secret", "username=some_username", "password=some_password")
   142  				v.Run("write", "concourse/main/pipeline-creds-test/resource_version", "value=some_exposed_version_secret")
   143  			}, func() {
   144  				v.Run("write", "concourse/main/team_secret", "value=some_team_secret")
   145  				v.Run("write", "concourse/main/resource_version", "value=some_exposed_version_secret")
   146  			})
   147  
   148  			testTokenRenewal()
   149  		})
   150  
   151  		Context("with TLS auth", func() {
   152  			BeforeEach(func() {
   153  				Deploy(
   154  					"deployments/concourse.yml",
   155  					"-o", "operations/add-vault.yml",
   156  					"--vars-store", varsStore.Name(),
   157  					"-o", "operations/enable-vault-tls.yml",
   158  					"-v", "vault_url="+v.URI(),
   159  					"-v", "vault_ip="+v.IP(),
   160  					"-v", "vault_client_token=dontcare",
   161  					"-v", `vault_auth_backend=""`,
   162  					"-v", "vault_auth_params={}",
   163  					"-v", "web_instances=0",
   164  				)
   165  
   166  				vaultCACertFile, err := ioutil.TempFile("", "vault-ca.cert")
   167  				Expect(err).ToNot(HaveOccurred())
   168  
   169  				vaultCACert := vaultCACertFile.Name()
   170  
   171  				session := Bosh("interpolate", "--path", "/vault_ca/certificate", varsStore.Name())
   172  				_, err = fmt.Fprintf(vaultCACertFile, "%s", session.Out.Contents())
   173  				Expect(err).ToNot(HaveOccurred())
   174  				Expect(vaultCACertFile.Close()).To(Succeed())
   175  
   176  				v.SetCA(vaultCACert)
   177  				v.Unseal()
   178  
   179  				v.Run("auth-enable", "cert")
   180  				v.Run(
   181  					"write",
   182  					"auth/cert/certs/concourse",
   183  					"display_name=concourse",
   184  					"certificate=@"+vaultCACert,
   185  					"policies=concourse",
   186  					fmt.Sprintf("ttl=%d", tokenDuration/time.Second),
   187  				)
   188  
   189  				Deploy(
   190  					"deployments/concourse.yml",
   191  					"-o", "operations/add-vault.yml",
   192  					"--vars-store", varsStore.Name(),
   193  					"-o", "operations/enable-vault-tls.yml",
   194  					"-v", "vault_url="+v.URI(),
   195  					"-v", "vault_ip="+v.IP(),
   196  					"-v", "web_instances=1",
   197  					"-v", `vault_client_token=""`,
   198  					"-v", "vault_auth_backend=cert",
   199  					"-v", "vault_auth_params={}",
   200  				)
   201  			})
   202  
   203  			testTokenRenewal()
   204  		})
   205  
   206  		Context("with approle auth", func() {
   207  			BeforeEach(func() {
   208  				v.Run("auth-enable", "approle")
   209  
   210  				v.Run(
   211  					"write",
   212  					"auth/approle/role/concourse",
   213  					"policies=concourse",
   214  					fmt.Sprintf("period=%d", tokenDuration/time.Second),
   215  				)
   216  
   217  				getRole := v.Run("read", "auth/approle/role/concourse/role-id")
   218  				content := string(getRole.Out.Contents())
   219  				roleID := regexp.MustCompile(`role_id\s+(.*)`).FindStringSubmatch(content)[1]
   220  
   221  				createSecret := v.Run("write", "-f", "auth/approle/role/concourse/secret-id")
   222  				content = string(createSecret.Out.Contents())
   223  				secretID := regexp.MustCompile(`secret_id\s+(.*)`).FindStringSubmatch(content)[1]
   224  
   225  				Deploy(
   226  					"deployments/concourse.yml",
   227  					"-o", "operations/add-vault.yml",
   228  					"--vars-store", varsStore.Name(),
   229  					"-v", "vault_url="+v.URI(),
   230  					"-v", "vault_ip="+v.IP(),
   231  					"-v", "web_instances=1",
   232  					"-v", `vault_client_token=""`,
   233  					"-v", "vault_auth_backend=approle",
   234  					"-v", `vault_auth_params={"role_id":"`+roleID+`","secret_id":"`+secretID+`"}`,
   235  				)
   236  			})
   237  
   238  			testTokenRenewal()
   239  		})
   240  	})
   241  })
   242  
   243  type vault struct {
   244  	ip               string
   245  	key1, key2, key3 string
   246  	token            string
   247  	caCert           string
   248  }
   249  
   250  func newVault(ip string) *vault {
   251  	v := &vault{
   252  		ip: ip,
   253  	}
   254  	v.init()
   255  	return v
   256  }
   257  
   258  func (v *vault) SetCA(filename string) { v.caCert = filename }
   259  func (v *vault) IP() string            { return v.ip }
   260  func (v *vault) ClientToken() string   { return v.token }
   261  func (v *vault) URI() string {
   262  	if v.caCert == "" {
   263  		return "http://" + v.ip + ":8200"
   264  	}
   265  
   266  	return "https://" + v.ip + ":8200"
   267  }
   268  
   269  func (v *vault) Run(command string, args ...string) *gexec.Session {
   270  	env := append(
   271  		os.Environ(),
   272  		"VAULT_ADDR="+v.URI(),
   273  		"VAULT_TOKEN="+v.token,
   274  	)
   275  	if v.caCert != "" {
   276  		env = append(
   277  			env,
   278  			"VAULT_CACERT="+v.caCert,
   279  			"VAULT_SKIP_VERIFY=true",
   280  		)
   281  	}
   282  	session := Start(env, "vault", append([]string{command}, args...)...)
   283  	Wait(session)
   284  	return session
   285  }
   286  
   287  func (v *vault) init() {
   288  	init := v.Run("init")
   289  	content := string(init.Out.Contents())
   290  	v.key1 = regexp.MustCompile(`Unseal Key 1: (.*)`).FindStringSubmatch(content)[1]
   291  	v.key2 = regexp.MustCompile(`Unseal Key 2: (.*)`).FindStringSubmatch(content)[1]
   292  	v.key3 = regexp.MustCompile(`Unseal Key 3: (.*)`).FindStringSubmatch(content)[1]
   293  	v.token = regexp.MustCompile(`Initial Root Token: (.*)`).FindStringSubmatch(content)[1]
   294  	v.Unseal()
   295  	v.Run("mount", "-path", "concourse/main", "generic")
   296  }
   297  
   298  func (v *vault) Unseal() {
   299  	v.Run("unseal", v.key1)
   300  	v.Run("unseal", v.key2)
   301  	v.Run("unseal", v.key3)
   302  }