@@ -25,7 +25,7 @@ const (
2525 defaultListenAddr = "127.0.0.1:8787"
2626 defaultVerifierURL = "http://127.0.0.1:8080/verify"
2727 defaultConsulURL = "http://127.0.0.1:8500"
28- defaultTokenTTL = time . Hour
28+ defaultTokenTTL = 0
2929)
3030
3131func main () {
@@ -34,7 +34,7 @@ func main() {
3434 verifierURL = flag .String ("verifier-url" , envOr ("DSTACK_VERIFIER_URL" , defaultVerifierURL ), "dstack-verifier /verify URL" )
3535 consulURL = flag .String ("consul-url" , envOr ("CONSUL_HTTP_ADDR" , defaultConsulURL ), "Consul HTTP API URL" )
3636 policyPath = flag .String ("policy" , os .Getenv ("ADMISSION_POLICY_FILE" ), "path to admission policy JSON" )
37- tokenTTL = flag .Duration ("token-ttl" , durationEnvOr ("ADMISSION_TOKEN_TTL" , defaultTokenTTL ), "issued Consul ACL token TTL" )
37+ tokenTTL = flag .Duration ("token-ttl" , durationEnvOr ("ADMISSION_TOKEN_TTL" , defaultTokenTTL ), "issued Consul ACL token TTL; 0 disables expiration " )
3838 )
3939 flag .Parse ()
4040
@@ -87,6 +87,7 @@ type Policy struct {
8787type Workload struct {
8888 WorkloadID string `json:"workload_id"`
8989 Identity string `json:"identity"`
90+ PeerID string `json:"peer_id,omitempty"`
9091 ConsulService string `json:"consul_service"`
9192 ConsulPermissions ConsulPermissions `json:"consul_permissions,omitempty"`
9293 AllowedComposeHashes []string `json:"allowed_compose_hashes"`
@@ -95,9 +96,10 @@ type Workload struct {
9596}
9697
9798type ConsulPermissions struct {
98- KeyPrefixes []string `json:"key_prefixes,omitempty"`
99- SessionWrite bool `json:"session_write,omitempty"`
100- AgentReadSelf bool `json:"agent_read_self,omitempty"`
99+ KeyPrefixes []string `json:"key_prefixes,omitempty"`
100+ SessionWrite bool `json:"session_write,omitempty"`
101+ AgentReadSelf bool `json:"agent_read_self,omitempty"`
102+ NodeIdentitySelf bool `json:"node_identity_self,omitempty"`
101103}
102104
103105func ParsePolicy (b []byte ) (* Policy , error ) {
@@ -133,8 +135,15 @@ func ParsePolicy(b []byte) (*Policy, error) {
133135 if strings .TrimSpace (w .Identity ) == "" {
134136 return nil , fmt .Errorf ("workloads[%d].identity is required" , i )
135137 }
136- if strings .TrimSpace (w .ConsulService ) == "" {
137- return nil , fmt .Errorf ("workloads[%d].consul_service is required" , i )
138+ w .PeerID = strings .TrimSpace (w .PeerID )
139+ if w .PeerID != "" {
140+ if err := validateConsulAgentName (w .PeerID ); err != nil {
141+ return nil , fmt .Errorf ("workloads[%d].peer_id: %w" , i , err )
142+ }
143+ }
144+ w .ConsulService = strings .TrimSpace (w .ConsulService )
145+ if w .ConsulService == "" && ! w .ConsulPermissions .hasGrant () {
146+ return nil , fmt .Errorf ("workloads[%d] must define consul_service or consul_permissions" , i )
138147 }
139148 for j , prefix := range w .ConsulPermissions .KeyPrefixes {
140149 prefix = strings .Trim (prefix , "/" )
@@ -160,16 +169,20 @@ func ParsePolicy(b []byte) (*Policy, error) {
160169 return & p , nil
161170}
162171
163- func (p * Policy ) Match (identity , composeHash string ) (* Workload , error ) {
172+ func (p * Policy ) Match (identity , composeHash , peerID string ) (* Workload , error ) {
164173 normalized , err := normalizeComposeHash (composeHash )
165174 if err != nil {
166175 return nil , fmt .Errorf ("invalid compose_hash from quote: %w" , err )
167176 }
177+ peerID = strings .TrimSpace (peerID )
168178 for _ , w := range p .byIdentity [identity ] {
169- if _ , ok := w .allowedComposeHashSet [normalized ]; ok {
179+ if _ , ok := w .allowedComposeHashSet [normalized ]; ok && ( w . PeerID == "" || w . PeerID == peerID ) {
170180 return & w , nil
171181 }
172182 }
183+ if peerID != "" {
184+ return nil , fmt .Errorf ("identity %q is not allowed for compose_hash %s and peer_id %q" , identity , normalized , peerID )
185+ }
173186 return nil , fmt .Errorf ("identity %q is not allowed for compose_hash %s" , identity , normalized )
174187}
175188
@@ -227,11 +240,11 @@ type AttestRequest struct {
227240}
228241
229242type AttestResponse struct {
230- ConsulACLToken string `json:"consul_acl_token"`
231- ExpiresAt time.Time `json:"expires_at"`
232- WorkloadID string `json:"workload_id"`
233- ComposeHash string `json:"compose_hash"`
234- PolicyEpoch int `json:"policy_epoch"`
243+ ConsulACLToken string `json:"consul_acl_token"`
244+ ExpiresAt * time.Time `json:"expires_at,omitempty "`
245+ WorkloadID string `json:"workload_id"`
246+ ComposeHash string `json:"compose_hash"`
247+ PolicyEpoch int `json:"policy_epoch"`
235248}
236249
237250func (s * Server ) handleAttest (w http.ResponseWriter , r * http.Request ) {
@@ -264,10 +277,9 @@ func (s *Server) handleAttest(w http.ResponseWriter, r *http.Request) {
264277 defer cancel ()
265278
266279 verified , err := s .verifier .Verify (ctx , VerifyRequest {
267- Attestation : req .Attestation ,
268- Quote : req .Quote ,
269- EventLog : rawJSONAsString (req .EventLog ),
270- VMConfig : req .VMConfig ,
280+ Quote : firstNonEmpty (req .Quote , req .Attestation ),
281+ EventLog : rawJSONAsString (req .EventLog ),
282+ VMConfig : req .VMConfig ,
271283 })
272284 if err != nil {
273285 writeError (w , http .StatusBadGateway , "VERIFIER_FAILED" , err .Error ())
@@ -282,17 +294,22 @@ func (s *Server) handleAttest(w http.ResponseWriter, r *http.Request) {
282294 return
283295 }
284296
285- workload , err := s .policy .Match (req .Identity , verified .ComposeHash )
297+ workload , err := s .policy .Match (req .Identity , verified .ComposeHash , bindingStatement . PeerID )
286298 if err != nil {
287299 writeError (w , http .StatusForbidden , "ADMISSION_REJECTED" , err .Error ())
288300 return
289301 }
290302
291- expiresAt := s .now ().Add (s .tokenTTL )
303+ var expiresAt * time.Time
304+ if s .tokenTTL > 0 {
305+ t := s .now ().Add (s .tokenTTL )
306+ expiresAt = & t
307+ }
292308 token , err := s .issuer .IssueToken (ctx , TokenRequest {
293309 Description : fmt .Sprintf ("attested %s %s" , workload .WorkloadID , verified .ComposeHash ),
294310 ConsulService : workload .ConsulService ,
295311 ConsulPermissions : workload .ConsulPermissions ,
312+ Datacenter : s .policy .Cluster ,
296313 PeerID : bindingStatement .PeerID ,
297314 TTL : s .tokenTTL ,
298315 })
@@ -413,10 +430,10 @@ func (s *NonceStore) gc(now time.Time) {
413430}
414431
415432type VerifyRequest struct {
416- Attestation string `json:"attestation,omitempty "`
417- Quote string `json:"quote"`
418- EventLog string `json:"event_log"`
419- VMConfig string `json:"vm_config"`
433+ Attestation * string `json:"attestation"`
434+ Quote string `json:"quote"`
435+ EventLog string `json:"event_log"`
436+ VMConfig string `json:"vm_config"`
420437}
421438
422439type VerifiedQuote struct {
@@ -518,6 +535,7 @@ type TokenRequest struct {
518535 Description string
519536 ConsulService string
520537 ConsulPermissions ConsulPermissions
538+ Datacenter string
521539 PeerID string
522540 TTL time.Duration
523541}
@@ -533,20 +551,40 @@ type ConsulIssuer struct {
533551}
534552
535553func (i * ConsulIssuer ) IssueToken (ctx context.Context , req TokenRequest ) (string , error ) {
536- if req .ConsulService == "" {
537- return "" , errors .New ("consul service is required " )
554+ if req .TTL < 0 {
555+ return "" , errors .New ("token TTL must not be negative " )
538556 }
539557 payload := map [string ]any {
540- "Description" : req .Description ,
541- "ExpirationTTL" : fmt .Sprintf ("%ds" , int (req .TTL .Seconds ())),
542- "ServiceIdentities" : []map [string ]string {
558+ "Description" : req .Description ,
559+ }
560+ if req .TTL > 0 {
561+ payload ["ExpirationTTL" ] = fmt .Sprintf ("%ds" , int (req .TTL .Seconds ()))
562+ }
563+ if req .ConsulService != "" {
564+ payload ["ServiceIdentities" ] = []map [string ]string {
543565 {"ServiceName" : req .ConsulService },
544- },
566+ }
567+ }
568+ if req .ConsulPermissions .NodeIdentitySelf {
569+ peerID := strings .TrimSpace (req .PeerID )
570+ if err := validateConsulAgentName (peerID ); err != nil {
571+ return "" , fmt .Errorf ("node_identity_self requires attested peer_id: %w" , err )
572+ }
573+ datacenter := strings .TrimSpace (req .Datacenter )
574+ if err := validateConsulAgentName (datacenter ); err != nil {
575+ return "" , fmt .Errorf ("node_identity_self requires datacenter: %w" , err )
576+ }
577+ payload ["NodeIdentities" ] = []map [string ]string {
578+ {"NodeName" : peerID , "Datacenter" : datacenter },
579+ }
545580 }
546581 rules , err := consulACLRules (req .ConsulPermissions , req .PeerID )
547582 if err != nil {
548583 return "" , err
549584 }
585+ if req .ConsulService == "" && ! req .ConsulPermissions .NodeIdentitySelf && rules == "" {
586+ return "" , errors .New ("token request must define consul service identity, node identity, or ACL rules" )
587+ }
550588 if rules != "" {
551589 policyName , err := i .EnsurePolicy (ctx , "attested-workload-" + shortSHA256 (rules , 16 ), "Additional permissions for attested workloads" , rules )
552590 if err != nil {
@@ -705,6 +743,10 @@ func consulACLRules(p ConsulPermissions, peerID string) (string, error) {
705743 return b .String (), nil
706744}
707745
746+ func (p ConsulPermissions ) hasGrant () bool {
747+ return len (p .KeyPrefixes ) > 0 || p .SessionWrite || p .AgentReadSelf || p .NodeIdentitySelf
748+ }
749+
708750func validateConsulAgentName (name string ) error {
709751 if name == "" {
710752 return errors .New ("peer_id is empty" )
0 commit comments