diff --git a/config/config_test.go b/config/config_test.go
index 68a6a1c13..28141d231 100644
--- a/config/config_test.go
+++ b/config/config_test.go
@@ -1025,6 +1025,48 @@ func TestParse(t *testing.T) {
 			},
 			false,
 		},
+		{
+			"template_uid",
+			`template {
+				uid = 1000
+			}`,
+			&Config{
+				Templates: &TemplateConfigs{
+					&TemplateConfig{
+						Uid: Int(1000),
+					},
+				},
+			},
+			false,
+		},
+		{
+			"template_gid",
+			`template {
+				gid = 1000
+			}`,
+			&Config{
+				Templates: &TemplateConfigs{
+					&TemplateConfig{
+						Gid: Int(1000),
+					},
+				},
+			},
+			false,
+		},
+		{
+			"template_uid_gid_default",
+			`template {
+			}`,
+			&Config{
+				Templates: &TemplateConfigs{
+					&TemplateConfig{
+						Uid: nil,
+						Gid: nil,
+					},
+				},
+			},
+			false,
+		},
 		{
 			"template_source",
 			`template {
diff --git a/config/template.go b/config/template.go
index a42e4d87f..3dd34776d 100644
--- a/config/template.go
+++ b/config/template.go
@@ -64,6 +64,20 @@ type TemplateConfig struct {
 	// secrets from Vault.
 	Perms *os.FileMode `mapstructure:"perms"`
 
+	// Uid is the numeric uid that will be set when creating the file on disk.
+	// Useful when simply setting Perms is not enough.
+	//
+	// Platform dependent: this doesn't work on Windows but it fails gracefully
+	// with a warning
+	Uid *int `mapstructure:"uid"`
+
+	// Gid is the numeric gid that will be set when creating the file on disk.
+	// Useful when simply setting Perms is not enough.
+	//
+	// Platform dependent: this doesn't work on Windows but it fails gracefully
+	// with a warning
+	Gid *int `mapstructure:"gid"`
+
 	// Source is the path on disk to the template contents to evaluate. Either
 	// this or Contents should be specified, but not both.
 	Source *string `mapstructure:"source"`
diff --git a/manager/runner.go b/manager/runner.go
index eb2db8abf..952a398f6 100644
--- a/manager/runner.go
+++ b/manager/runner.go
@@ -780,6 +780,8 @@ func (r *Runner) runTemplate(tmpl *template.Template, runCtx *templateRunCtx) (*
 			DryStream:      r.outStream,
 			Path:           config.StringVal(templateConfig.Destination),
 			Perms:          config.FileModeVal(templateConfig.Perms),
+			Uid:            templateConfig.Uid,
+			Gid:            templateConfig.Gid,
 		})
 		if err != nil {
 			return nil, errors.Wrap(err, "error rendering "+templateConfig.Display())
diff --git a/renderer/file_ownership.go b/renderer/file_ownership.go
new file mode 100644
index 000000000..f4879573c
--- /dev/null
+++ b/renderer/file_ownership.go
@@ -0,0 +1,49 @@
+//go:build !windows
+// +build !windows
+
+package renderer
+
+import (
+	"os"
+	"syscall"
+)
+
+func getFileOwnership(path string) (*int, *int) {
+	file_info, err := os.Stat(path)
+
+	if err != nil {
+		return nil, nil
+	}
+
+	file_sys := file_info.Sys()
+	st := file_sys.(*syscall.Stat_t)
+	return intPtr(int(st.Uid)), intPtr(int(st.Gid))
+}
+
+func setFileOwnership(path string, uid, gid *int) error {
+	wantedUid := sanitizeUidGid(uid)
+	wantedGid := sanitizeUidGid(gid)
+	if wantedUid == -1 && wantedGid == -1 {
+		return nil //noop
+	}
+	return os.Chown(path, wantedUid, wantedGid)
+}
+
+func isChownNeeded(path string, uid, gid *int) bool {
+	wantedUid := sanitizeUidGid(uid)
+	wantedGid := sanitizeUidGid(gid)
+	if wantedUid == -1 && wantedGid == -1 {
+		return false
+	}
+
+	currUid, currGid := getFileOwnership(path)
+	return wantedUid != *currUid || wantedGid != *currGid
+}
+
+// sanitizeUidGid sanitizes the uid/gid so that can be an input for os.Chown
+func sanitizeUidGid(id *int) int {
+	if id == nil {
+		return -1
+	}
+	return *id
+}
diff --git a/renderer/file_ownership_windows.go b/renderer/file_ownership_windows.go
new file mode 100644
index 000000000..dd9f14690
--- /dev/null
+++ b/renderer/file_ownership_windows.go
@@ -0,0 +1,20 @@
+//go:build windows
+// +build windows
+
+package renderer
+
+import (
+	"log"
+	"os"
+)
+
+func setFileOwnership(path string, uid, gid *int) error {
+	if uid != nil || gid != nil {
+		log.Printf("[WARN] (runner) cannot set uid/gid for rendered files on Windows")
+	}
+	return nil
+}
+
+func isChownNeeded(path string, wantedUid, wantedGid *int) bool {
+	return false
+}
diff --git a/renderer/renderer.go b/renderer/renderer.go
index 90e178321..6190381a1 100644
--- a/renderer/renderer.go
+++ b/renderer/renderer.go
@@ -36,6 +36,7 @@ type RenderInput struct {
 	DryStream      io.Writer
 	Path           string
 	Perms          os.FileMode
+	Uid, Gid       *int
 }
 
 // RenderResult is returned and stored. It contains the status of the render
@@ -65,7 +66,13 @@ func Render(i *RenderInput) (*RenderResult, error) {
 		return nil, errors.Wrap(err, "failed reading file")
 	}
 
-	if bytes.Equal(existing, i.Contents) && fileExists {
+	var chownNeeded bool
+
+	if fileExists {
+		chownNeeded = isChownNeeded(i.Path, i.Uid, i.Gid)
+	}
+
+	if bytes.Equal(existing, i.Contents) && fileExists && !chownNeeded {
 		return &RenderResult{
 			DidRender:   false,
 			WouldRender: true,
@@ -79,6 +86,10 @@ func Render(i *RenderInput) (*RenderResult, error) {
 		if err := AtomicWrite(i.Path, i.CreateDestDirs, i.Contents, i.Perms, i.Backup); err != nil {
 			return nil, errors.Wrap(err, "failed writing file")
 		}
+
+		if err = setFileOwnership(i.Path, i.Uid, i.Gid); err != nil {
+			return nil, errors.Wrap(err, "failed setting file ownership")
+		}
 	}
 
 	return &RenderResult{
@@ -186,3 +197,8 @@ func AtomicWrite(path string, createDestDirs bool, contents []byte, perms os.Fil
 
 	return nil
 }
+
+// intPtr returns a pointer to the given int.
+func intPtr(i int) *int {
+	return &i
+}
diff --git a/renderer/renderer_test.go b/renderer/renderer_test.go
index 5d2052564..8e7984ff8 100644
--- a/renderer/renderer_test.go
+++ b/renderer/renderer_test.go
@@ -315,3 +315,214 @@ func TestRender(t *testing.T) {
 		}
 	})
 }
+
+func TestRender_Chown(t *testing.T) {
+
+	// Can't change uid unless root, but can try
+	// changing the group id.
+	// setting Uid to -1 means no change
+	wantedUid := -1
+
+	//we enumerate the groups the current user (running the tests) belongs to
+	groups, err := os.Getgroups()
+	if err != nil {
+		t.Fatalf("getgroups: %s", err)
+	}
+	t.Log("groups: ", groups)
+
+	// we'll use the last group because we can assume it's not the default one
+	// for the current user (thinking about CI/CD).
+	// In order to make sure that this is tested properly we would have to
+	// preconfigure the environment and specify the gid here or (better) add
+	// the user to a group and leave this dynamic avoiding hardcoded values,
+	// worst case scenario, if the user belongs to a single group, these tests
+	// would not be testing the cange of ownership but only the fact that it doesn't
+	// fail unexpectedly
+	wantedGid := groups[0]
+
+	t.Run("sets-file-ownership-when-file-exists-same-content", func(t *testing.T) {
+
+		outDir, err := ioutil.TempDir("", "")
+		if err != nil {
+			t.Fatal(err)
+		}
+		defer os.RemoveAll(outDir)
+		outFile, err := ioutil.TempFile(outDir, "")
+		if err != nil {
+			t.Fatal(err)
+		}
+		contents := []byte("first")
+		if _, err := outFile.Write(contents); err != nil {
+			t.Fatal(err)
+		}
+		path := outFile.Name()
+		if err = outFile.Close(); err != nil {
+			t.Fatal(err)
+		}
+
+		rr, err := Render(&RenderInput{
+			Path:     path,
+			Contents: contents,
+			Uid:      intPtr(wantedUid),
+			Gid:      intPtr(wantedGid),
+		})
+		if err != nil {
+			t.Fatal(err)
+		}
+		switch {
+		case rr.WouldRender && rr.DidRender: //we expect rerendering to disk here
+		default:
+			t.Fatalf("Bad render results; would: %v, did: %v",
+				rr.WouldRender, rr.DidRender)
+		}
+
+		gotUid, gotGid := getFileOwnership(path)
+		if *gotGid != wantedGid {
+			t.Fatalf("Bad render results; gotUid: %v, wantedGid: %v, gotGid: %v",
+				*gotUid, wantedGid, *gotGid)
+		}
+
+	})
+
+	t.Run("sets-file-ownership-when-file-exists-diff-content", func(t *testing.T) {
+
+		outDir, err := ioutil.TempDir("", "")
+		if err != nil {
+			t.Fatal(err)
+		}
+		defer os.RemoveAll(outDir)
+		outFile, err := ioutil.TempFile(outDir, "")
+		if err != nil {
+			t.Fatal(err)
+		}
+		contents := []byte("first")
+		if _, err := outFile.Write(contents); err != nil {
+			t.Fatal(err)
+		}
+		path := outFile.Name()
+		if err = outFile.Close(); err != nil {
+			t.Fatal(err)
+		}
+
+		diff_contents := []byte("not-first")
+		rr, err := Render(&RenderInput{
+			Path:     path,
+			Contents: diff_contents,
+			Uid:      intPtr(wantedUid),
+			Gid:      intPtr(wantedGid),
+		})
+		if err != nil {
+			t.Fatal(err)
+		}
+		switch {
+		case rr.WouldRender && rr.DidRender:
+		default:
+			t.Fatalf("Bad render results; would: %v, did: %v",
+				rr.WouldRender, rr.DidRender)
+		}
+
+		gotUid, gotGid := getFileOwnership(path)
+		if *gotGid != wantedGid {
+			t.Fatalf("Bad render results; gotUid: %v, wantedGid: %v, gotGid: %v",
+				*gotUid, wantedGid, *gotGid)
+		}
+
+	})
+	t.Run("sets-file-ownership-when-file-no-exists", func(t *testing.T) {
+
+		outDir, err := ioutil.TempDir("", "")
+		if err != nil {
+			t.Fatal(err)
+		}
+		defer os.RemoveAll(outDir)
+		path := path.Join(outDir, "no-exists")
+		contents := []byte("first")
+
+		rr, err := Render(&RenderInput{
+			Path:     path,
+			Contents: contents,
+			Uid:      intPtr(wantedUid),
+			Gid:      intPtr(wantedGid),
+		})
+		if err != nil {
+			t.Fatal(err)
+		}
+		switch {
+		case rr.WouldRender && rr.DidRender:
+		default:
+			t.Fatalf("Bad render results; would: %v, did: %v",
+				rr.WouldRender, rr.DidRender)
+		}
+
+		gotUid, gotGid := getFileOwnership(path)
+		if *gotGid != wantedGid {
+			t.Fatalf("Bad render results; gotUid: %v, wantedGid: %v, gotGid: %v",
+				*gotUid, wantedGid, *gotGid)
+		}
+
+	})
+	t.Run("sets-file-ownership-when-empty-file-no-exists", func(t *testing.T) {
+
+		outDir, err := ioutil.TempDir("", "")
+		if err != nil {
+			t.Fatal(err)
+		}
+		defer os.RemoveAll(outDir)
+		path := path.Join(outDir, "no-exists")
+		contents := []byte{}
+
+		rr, err := Render(&RenderInput{
+			Path:     path,
+			Contents: contents,
+			Uid:      intPtr(wantedUid),
+			Gid:      intPtr(wantedGid),
+		})
+		if err != nil {
+			t.Fatal(err)
+		}
+		switch {
+		case rr.WouldRender && rr.DidRender:
+		default:
+			t.Fatalf("Bad render results; would: %v, did: %v",
+				rr.WouldRender, rr.DidRender)
+		}
+
+		gotUid, gotGid := getFileOwnership(path)
+		if *gotGid != wantedGid {
+			t.Fatalf("Bad render results; gotUid: %v, wantedGid: %v, gotGid: %v",
+				*gotUid, wantedGid, *gotGid)
+		}
+	})
+
+	t.Run("should-be-noop-when-missing-gid", func(t *testing.T) {
+
+		outDir, err := ioutil.TempDir("", "")
+		if err != nil {
+			t.Fatal(err)
+		}
+		defer os.RemoveAll(outDir)
+		path := path.Join(outDir, "no-exists")
+		contents := []byte{}
+
+		rr, err := Render(&RenderInput{
+			Path:     path,
+			Contents: contents,
+			Uid:      intPtr(1000),
+		})
+		if err != nil {
+			t.Fatal(err)
+		}
+		switch {
+		case rr.WouldRender && rr.DidRender:
+		default:
+			t.Fatalf("Bad render results; would: %v, did: %v",
+				rr.WouldRender, rr.DidRender)
+		}
+
+		gotUid, gotGid := getFileOwnership(path)
+		if *gotGid == wantedGid {
+			t.Fatalf("Bad render results; we shouldn't have altered uid/gid. gotUid: %v, gotGid: %v",
+				*gotUid, *gotGid)
+		}
+	})
+}