-
Notifications
You must be signed in to change notification settings - Fork 18.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Secret Management #27794
Secret Management #27794
Conversation
refs := []*swarmapi.SecretReference{} | ||
for _, s := range sr { | ||
var mode swarmapi.SecretReference_Mode | ||
switch s.Mode { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Redundant switch. IMO should be
mode := swarmapi.SecretReference_FILE
if s.Mode == types.SecretReferenceSystem {
mode = swarmapi.SecretReference_SYSTEM
}
refs := []*types.SecretReference{} | ||
for _, s := range sr { | ||
var mode types.SecretReferenceMode | ||
switch s.Mode { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe make convertSecretReferenceMode
function
} | ||
|
||
// Meta | ||
secret.Version.Index = s.Meta.Version.Index |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you can embed Version
and Spec
in structure initialization
|
||
// SecretFromGRPC converts a grpc Secret to a Secret. | ||
func SecretFromGRPC(s *swarmapi.Secret) swarmtypes.Secret { | ||
logrus.Debugf("%+v", s) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe add some words here.
|
||
// SecretSpecToGRPC converts Secret to a grpc Secret. | ||
func SecretSpecToGRPC(s swarmtypes.SecretSpec) (swarmapi.SecretSpec, error) { | ||
spec := swarmapi.SecretSpec{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just return this here
@@ -4,6 +4,7 @@ import ( | |||
executorpkg "github.com/docker/docker/daemon/cluster/executor" | |||
"github.com/docker/swarmkit/api" | |||
"golang.org/x/net/context" | |||
"src/github.com/docker/swarmkit/agent/exec" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
wrong import
return fmt.Errorf("error creating secret mount path: %s", err) | ||
} | ||
|
||
logrus.Debugf("injecting secret: name=%s path=%s", s.Name, fPath) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you can use WithFields
for creating such structured messages :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ya thx :)
|
||
// create tmpfs | ||
if err := os.MkdirAll(localMountPath, 0700); err != nil { | ||
return fmt.Errorf("error creating secret local mount path: %s", err) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we have pkg/errors
vendored now. Would you mind to use errors.Wrap{f}
in such cases?
@@ -175,6 +176,41 @@ func (daemon *Daemon) setupIpcDirs(c *container.Container) error { | |||
return nil | |||
} | |||
|
|||
func (daemon *Daemon) setupSecretDir(c *container.Container) error { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
shouldn't this func cleanup on error? I mean like unmount and remove secrets dir.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok i added a defer func that should cleanup. ptal thx!
|
||
func (daemon *Daemon) SetContainerSecrets(name string, secrets []*containertypes.ContainerSecret) error { | ||
if !secretsSupported() { | ||
logrus.Warn("secrets are not supported on this platform") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds like this warning will be logged on each container run. Is that right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Having issues making this work:
$ echo foo=bar | docker secret create test
$ docker service create --name test --secret test:/foo busybox top
$ docker exec <cid> ls /foo
/foo
does not exist.
I have concerns on supporting the format <name>:<target>
which puts us in the same sticky situation we are in with -v
on run
.
rawVersion := r.URL.Query().Get("version") | ||
version, err := strconv.ParseUint(rawVersion, 10, 64) | ||
if err != nil { | ||
return fmt.Errorf("Invalid secret version '%s': %s", rawVersion, err.Error()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we use api/errors with something like a BadRequest?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also lower-case errors
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thx -- i was following the convention. i'll update
|
||
id := vars["id"] | ||
if err := sr.backend.UpdateSecret(id, version, secret); err != nil { | ||
return fmt.Errorf("Error updating secret: %s", err) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would this only ever be an internal error? If not I'd not wrap the error here and make sure the backend provides a sufficient message.
) | ||
|
||
// parseSecretString parses the requested secret and returns the secret name | ||
// and target. Expects format SECRET_NAME:TARGET |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👎
This format is nice in the happy case, but in the long run is a nightmare.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok. do you have an alternative?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think @stevvooe once suggested the unicode arrow ;)
But seriously, I think the code here has the right approach. The secret name and target need to be specified adjacent to each other instead of using separate options. We could use key-value pairs, but I think that would be really ugly and unintuitive.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@cpuguy83 @aaronlehmann what about something like
--secret ssh-dev,target=id_rsa,uid=1000,gid=1000,mode=0600
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
any thoughts on the above @cpuguy83 @aaronlehmann ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would say in general it looks good.
I think ssh-dev
should be source=ssh-dev
to match --mount
(big discussion on whether to allow the positional syntax from that PR).
Only question I'd ask is, how does this work with if/when an alternative secrets backend is supported (e.g. Vault). Would the current options work on all backends/platforms? Can Windows support this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not a big fan of source=
. It complicates the normal case where all you want to specify is a source, which now involves key/value pairs. --secret-add source=secretname
doesn't look like a particularly friendly CLI to me.
What about keeping the key/value pairs as the general case, but allowing a shorthand of --secret-add secretname
(i.e. if no =
is found, take the argument as the source)? I'm not completely sure I like this, but I think I like it more than forcing key/value syntax for this argument.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Find by me.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But mixing positional and k/v in the same spec (e.g. --secret-add secretname,target=mysecret
) not so much.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree with @aaronlehmann's suggestion. We should have a shorthand of --secret-add secretname
} | ||
|
||
// remount secrets ro | ||
if err := mount.Mount("tmpfs", localMountPath, "tmpfs", "remount,ro"); err != nil { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not ramfs?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we decided to not use ramfs since the swarmkit storage swaps to disk so we don't gain anything by using ramfs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
makes sense
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ehazlett does this mean secrets are effectively stored plain-text on disk?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For the container they are stored in tmpfs (in memory). They are encrypted at rest in the Swarm storage.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok awesome, thanks @ehazlett 😁
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yw :)
@@ -259,6 +266,26 @@ func (container *Container) IpcMounts() []Mount { | |||
return mounts | |||
} | |||
|
|||
// SecretMounts returns the list of Secret mounts | |||
func (container *Container) SecretMounts() []Mount { | |||
var mounts []Mount |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we have a slice but only ever append one thing to it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it was to add more in the future but good point. i'll remove the slice.
const DefaultSHMSize int64 = 67108864 | ||
const ( | ||
DefaultSHMSize int64 = 67108864 | ||
containerSecretMountPath = "/run/secrets" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
doesn't match the doc string
ExecCommands *exec.Store `json:"-"` | ||
HostConfig *containertypes.HostConfig `json:"-"` // do not serialize the host config in the json, otherwise we'll make the container unportable | ||
ExecCommands *exec.Store `json:"-"` | ||
Secrets []*containertypes.ContainerSecret `json:"-"` // do not serialize |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't like that we're shoving secrets in here purely for indexing, but I guess we are doing it for other things as well right now.
ctx, cancel := c.getRequestContext() | ||
defer cancel() | ||
|
||
r, err := c.node.client.GetSecret(ctx, &swarmapi.GetSecretRequest{SecretID: id}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doesn't work when a name is specified.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nor prefixes
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
correct. swarmkit only supports getting by id atm
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
swarmkit can filter by name or name prefix using the ListSecrets
RPC. This is the same as other object types like service and node - Get
is always by ID.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So we could implement a GetSecretByName
that effectively uses ListSecrets
with a name
filter under it.
Also sorry, I added some code review in there though it's still in design review. The other item would be only supporting stdin... but that's probably not a big deal... who knows for Windows though. |
Tests are also failing because of the linter. |
lint issues should be fixed. @cpuguy83 the design was to have the |
Aha! It goes in |
@cpuguy83 I was possibly thinking something like the more granular mount syntax in the future but it might be too much for now. idk |
@ehazlett looks like vendor needs to be updated. |
type Secret struct { | ||
ID string | ||
Meta | ||
Spec *SecretSpec `json:",omitempty"` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Spec
should not be a pointer.
Meta | ||
Spec *SecretSpec `json:",omitempty"` | ||
Digest string `json:",omitempty"` | ||
SecretSize int64 `json:",omitempty"` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We probably don't want omitempty
for SecretSize
. Probably not for any of these fields, actually.
|
||
type SecretSpec struct { | ||
Annotations | ||
Data []byte `json:",omitempty"` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again, probably don't want to omit the Data
field if the secret is zero-length.
sec = append(sec, &swarm.SecretReference{ | ||
SecretID: s, | ||
Mode: swarm.SecretReferenceFile, | ||
Target: "", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suppose we still need to add the ability for the user to specify a custom target?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There seems to be an inconsistency between service create
and service update
. Shouldn't they both take secrets in the format that specifies a name and target (i.e. use parseSecrets
)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ya we actually don't even need this because i set it outside in order to do the lookup. good catch thx!
tokens := strings.Split(secretString, ":") | ||
|
||
secretName := strings.TrimSpace(tokens[0]) | ||
targetName := "" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
targetName = secretName
to avoid the else case below?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1
|
||
foundSecrets := make(map[string]*swarmtypes.Secret) | ||
for _, secret := range secrets { | ||
foundSecrets[secret.Spec.Annotations.Name] = &secret |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks wrong. It will set every map value to point to the same iterator. Just make the map values secret IDs instead of pointers to secrets. That will make things simpler.
|
||
// Error returns a string representation of a secretNotFoundError | ||
func (e secretNotFoundError) Error() string { | ||
return fmt.Sprintf("Error: No such secret: %s", e.name) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no such secret: %s
} | ||
} | ||
|
||
return mount |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it correct to return an empty Mount
structure in this case? I see that it's unconditionally appended to a slice in createSpec
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had this as a nil check before; i'll switch back
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Few questions :
What should happen if I remove a secret that is mounted ? Right now it's still available in the container, that might be by design, just asking 👼.
On service create
, there is a problem if I specify multiple secrets (using --secret
) ; I get the same content for all the secrets.
$ docker service create sec1 <<EOF
A
EOF
$ docker secret create sec2 <<EOF
B
EOF
$ docker service create --name test --secret sec1 --secret sec2 busybox top
# Task container is 2b66…
$ docker exec -ti 2b66 sh
$ ls /run/secrets
sec1 sec2
$ cat /run/secrets/sec1
B
# should have be A
$ cat /run/secrets/sec2
B
On service update
, it doesn't seems it updates the secret or re-create the task. Not sure what is the preferred way to go (I would think re-create the container), but as of now, it doesn't have any effect. — just saw the checkbox, nevermind 👼
$ docker service ps test
NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR
test.1.imutqdjh4k2cm24e90mpkt9vh busybox 4c780a8f3912 Running Running 37 seconds ago
$ docker secret create sec3 <<EOF
C
EOF
$ docker service update --secret sec3 test
test
$ docker service ps test
NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR
test.1.imutqdjh4k2cm24e90mpkt9vh busybox 4c780a8f3912 Running Running 7 minutes ago
$ docker exec -ti 2b6 sh
$ ls /run/secrets
sec1 sec2
For docker secret
subcommands (like rm
or inspect
), should we support prefixes (like on all container subcommands) and names ? right now it just supports the full ID.
Other than that, it looks good 👼 🐸
@@ -40,5 +40,10 @@ func (sr *swarmRouter) initRoutes() { | |||
router.NewPostRoute("/nodes/{id:.*}/update", sr.updateNode), | |||
router.NewGetRoute("/tasks", sr.getTasks), | |||
router.NewGetRoute("/tasks/{id:.*}", sr.getTask), | |||
router.NewGetRoute("/secrets", sr.getSecrets), | |||
router.NewPostRoute("/secrets/create", sr.createSecret), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This could be router.NewPostRoute("/secrets", sr.createSecret)
👼
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ya I was trying to follow the convention but I like /secrets
better. thx!
That's probably because of this: https://github.com/docker/docker/pull/27794/files#r85236096 |
@@ -18,6 +18,10 @@ type executor struct { | |||
backend executorpkg.Backend | |||
} | |||
|
|||
type secretProvider interface { | |||
Get(secretID string) *api.Secret | |||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks like it's unused.
log.Errorf("error cleaning up secret mount: %s", err) | ||
} | ||
} | ||
}(setupErr) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Args to a deferred function are evaluated at the time of the defer
statement, so this won't work as expected.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ah ok thx
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i'm using a named return now so i think this should work. PTAL
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It still looks wrong. You shouldn't pass setupErr
as an argument to the deferred function. You should just let it capture the variable from the outer scope.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you are correct. thx
|
||
// create tmpfs | ||
if err := os.MkdirAll(localMountPath, 0700); err != nil { | ||
setupErr = errors.Wrap(err, "error creating secret local mount path") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it correct to continue if the directory can't be created?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nope -- i need to rework the cleanup
setupErr = errors.Wrap(err, "error creating secret local mount path") | ||
} | ||
if err := mount.Mount("tmpfs", localMountPath, "tmpfs", "nodev"); err != nil { | ||
setupErr = errors.Wrap(err, "unable to setup secret mount") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it correct to continue if the mount fails?
We need a way to define the secret on the container. Since we can't call the secret store direct from the engine side I chose to implement it here and have the cluster define it as discussed during design.
|
@ehazlett In general, these types should be isomorphic to swarm types. We can share types with swarm types, just fine. We do this for mounts. |
Is the design of the secret management allowing for future features like one-time-read options? Or is the design not allowing that kind of extensions of dynamically adding/removing secrets in a secure way? |
The design of the secrets functionality in this PR does not preclude such future features. Many of the existing features were designed with these use cases in mind for the future. |
opened #28516 for tracking the remaining review comments |
- `cassandra`: 3.0.10 - `docker`: 1.13.0-rc1 (currently failing the `override-cmd` test; see moby/moby#28329), `docker daemon` -> `dockerd` (docker-library/docker#31) - `drupal`: 8.2.3 - `java`: alpine 7.121.2.6.8-r0, 8.111.14-r0 - `openjdk`: alpine 7.121.2.6.8-r0, 8.111.14-r0 - `python`: 3.6.0b3 (docker-library/python#157), pip 9.0.1 (docker-library/python#154) - `ruby`: 2.3.2, 2.2.6 - `tomcat`: 6.0.48, 7.0.73, 8.0.39, 8.5.8, 9.0.0.M13 - `wordpress`: add `WORDPRESS_*_FILE` support for passing parameters via files especially for [Docker secrets](moby/moby#27794) (docker-library/wordpress#186)
@jefflill You might also want to make sure your second tmpfs is mounted securely, with Short term, I also wish the /run/secrets/ could have an option to simply o-rwx the directory (since it's read-only and we can't chmod it). That said, read once sounds a lot better... :) |
Actually, I've verified that opts/mount: add tmpfs-specific options This is super clean now. --Jeff On Fri, Nov 18, 2016 at 7:30 AM, Alexandre Guédon [email protected]
|
@ehazlett wow it's pretty cool ! Was working on a vault integration of our own when I've seen that. As the feature comes from swarmkit, could we imagine to have it in non swarm docker use (docker for mac, docker engine)? And what about docker-compose support? There's already a discussion opened about secrets docker/compose#1534 To wum up, I would be very pleased to be able to use secrets on my local machine/ci/... the same way than on the big cluster ;) |
@shouze this all works on docker for mac, but currently only for "services" (so if you have swarm mode enabled ( |
@thaJeztah ho nice! I've just tested it, very very nice ;) I guess that when enhancements in |
Hi, Sorry for the question but does the secrets are stored in an encrypted way ? |
@nasskach yes; see the (work in progress) documentation; https://github.com/docker/docker.github.io/pull/568/files |
This adds support for the new Secret API in Swarmkit (moby/swarmkit#1567). This adds commands (
create
,inspect
,ls
,rm
) for managing secrets in a Swarm cluster. This is currently for Swarm mode only as the backing store is Swarm and as such is only for Linux. This is the foundation for future secret support in Docker with potential improvements such as Windows support, different backing stores, etc.This takes the ideas and collective recommendations from #13490 to set the groundwork. It exposes a file system based interface for secret access. The engine creates a tmpfs and mounts it in container (
/run/secrets
). A file is created for each secret that is assigned to the service. For example, if a secret named "ssh-dev" with a target of "id_rsa" is assigned to the service, a file with the secret contents will be created at/run/secrets/id_rsa
.TODO:
Service update support to add / rm secrets from a serviceUser facing docsMore testsDepends on Fixed bug in secrets assignment, added logging swarmkit#1697Examples (these will be turned into user docs):
Create a Secret:
Create a Service with a Secret
We can then check the created container for the secret:
Create with full options