October 13, 2026 · v1.5.27
v1.5.27 deepens AUTH=true: requests signed with an IAM user or STS session key now have their signature, session token and request time verified, credential failures answer with each protocol's code and status, IoT evaluates its policies on MQTT connect, publish, subscribe and delivery, Cognito verifies access tokens, and S3 list grants can be scoped by s3:prefix. It also adds IoT tagging, retained messages and job execution APIs and the Cognito InboundFederation trigger, makes Cognito token lifetimes follow the app client, and brings a round of Step Functions fixes: aws-sdk parameters and results take the SDK's shape, ListExecutions lists the most recent first, and JSONata Fail and Wait fields are evaluated.
AUTH=true a REST API invoke that failed authentication got the generic JSON error and ignored the API's gateway responses; a wrong secret, a skewed clock or a credential date other than the request date now answers INVALID_SIGNATURE (InvalidSignatureException), an expired session EXPIRED_TOKEN (ExpiredTokenException), and an unknown key or a malformed Authorization header DEFAULT_4XX with the matching x-amzn-ErrorType, 403 with {"message": ...}. Contributed by @iot-rocket.AUTH=true a request signed with such a key and the wrong secret, or changed after signing, was served; it now answers SignatureDoesNotMatch (or the service's InvalidSignatureException), an Authorization header without its Signature or SignedHeaders answers IncompleteSignature (Lambda MissingAuthenticationTokenException, S3 AuthorizationHeaderMalformed) and a credential date other than the request date SignatureDoesNotMatch (S3 AuthorizationHeaderMalformed), S3 unsigned, streaming and signed-chunk payloads are checked the same way, and the root key still accepts any secret. Contributed by @iot-rocket.AUTH=true a request signed with an IAM user or STS session key was served whatever its time; one signed more than 15 minutes (STS, IAM, SNS, SQS, DynamoDB, S3, ECR, RDS, ElastiCache) or 5 minutes (the default for other services) from the server clock answers SignatureDoesNotMatch / InvalidSignatureException with AWS's Signature expired or Signature not yet current message, RequestTimeTooSkewed from S3 and AuthFailure from EC2. Contributed by @iot-rocket.AUTH=true an STS session key sent without its token was accepted and now answers as an unknown key, and a wrong token, or a token with an IAM user key, answers InvalidClientTokenId (UnrecognizedClientException from JSON services, InvalidToken 400 from S3), as on AWS. Contributed by @iot-rocket.InboundFederation trigger on SAML and OIDC sign-in. LambdaConfig.InboundFederation was stored but never invoked; SAML and OIDC federated sign-in now call the Lambda with InboundFederation_ExternalProvider, the provider-prefixed userName and the IdP's samlResponse or idToken. A non-empty userAttributesToMap replaces the IdP attributes before AttributeMapping, {} keeps them, and a failing or missing function fails the sign-in with UserLambdaValidationException. Contributed by @kjdev.DeleteJobExecution. DELETE /things/{thingName}/jobs/{jobId}/executionNumber/{executionNumber} answered 400 Unsupported IoT path; it now deletes the execution (an IN_PROGRESS one only with force), drops it from the job's lists and counts, completes the job when only finished executions are left, and publishes the deleted job execution event and the thing's jobs/notify topics; a CONTINUOUS job's thing left with no execution gets none back until it leaves the group, which leaves a REMOVED execution 1. Contributed by @iot-rocket.ListJobExecutionsForJob. GET /jobs/{jobId}/things answered 400 Unsupported IoT path; it now lists every execution of the job, earlier ones of a rejoined thing included, most recently updated first, with the status filter and maxResults / nextToken paging. Contributed by @iot-rocket.TagResource, UntagResource and ListTagsForResource. the three operations answered Unsupported IoT path, so the tags the Create calls accept could not be read back. They now work on thing types, thing groups, policies, topic rules, jobs, job templates, CA certificates, provisioning templates and domain configurations with AWS's limits and errors, Create calls and CloudFormation Tags write the same tags, and deleting a resource drops them. Contributed by @iot-rocket.iot:Connect under AUTH=true. a device certificate on the mTLS listener connected under any client id, with or without a policy; with AUTH=true the CONNECT now needs an attached policy that allows iot:Connect on client/<client id> and none that denies it, with ${iot:ClientId}, ${iot:Connection.Thing.ThingName}, iot:Connection.Thing.IsAttached and ${iot:Certificate.Subject.CommonName} resolved. A refusal closes an MQTT 3.1.1 connection without a CONNACK, answers MQTT 5 with CONNACK 0x87 and publishes $aws/events/presence/connect_failed/{clientId}. Contributed by @iot-rocket.AUTH=true. after the CONNECT a certificate session could publish, subscribe and receive on any topic; it now needs iot:Publish on the topic (and iot:RetainPublish for a retained message or will), iot:Subscribe per topic filter and iot:Receive per delivered message. A refused PUBLISH or SUBSCRIBE closes an MQTT 3.1.1 connection (disconnect reason CLIENT_ERROR), MQTT 5 answers PUBACK 0x87, DISCONNECT 0x87 at QoS 0 (AUTH_ERROR) or 0x87 for the refused filter, which the subscribed event leaves out (no event when none is granted), a refused will refuses the CONNECT, and a message the subscriber may not receive is dropped. Contributed by @iot-rocket.GetRetainedMessage and ListRetainedMessages answered 501 not yet implemented; they now return the broker's retained messages (newest first, paged by maxResults and nextToken, with the user properties of an MQTT 5 publish), and a PERSIST_STATE=1 restart keeps each message's lastModifiedTime and user properties. Contributed by @iot-rocket.AUTH=true an unknown, inactive or expired access key answered UnrecognizedClientException 403 from every service; it now gets the code, status and Content-Type AWS sends for the service's protocol, for example InvalidClientTokenId 403 from IAM and SQS, UnrecognizedClientException 400 from DynamoDB, InvalidAccessKeyId 403 from S3 and AuthFailure 401 from EC2. Contributed by @iot-rocket.AWS::Logs::LogGroup delete. a stack delete or a LogGroupName replacement removed only the group record, so a metric filter or resource policy added through the Logs API stayed and came back with a new group of the same name; the group is now deleted through DeleteLogGroup. Contributed by @iot-rocket.Ref. Ref returned <apiId>/<stageName>, causing an AWS::ApiGatewayV2::ApiMapping using the reference as its Stage to fail with Invalid stage identifier specified; new stages now return the stage name. Stage updates, replacement on ApiId or StageName changes, and deletion use the resolved resource properties, including for stacks with legacy physical IDs. Contributed by @matuscongrady.AWS::Events::Rule stored an ARN-valued EventBusName as the bus name, so PutEvents succeeded without delivering to its targets. Create, update, delete and drift detection now resolve the ARN through the existing EventBridge parser and use the bus name internally. Contributed by @matuscongrady.CreateTrail left a new trail logging, and GetTrailStatus reported a StartLoggingTime an hour back and a LatestDeliveryTime of now. A new trail now reports IsLogging false until StartLogging, and GetTrailStatus returns only the StartLoggingTime and StopLoggingTime of calls made (a restart keeps the last stop time), without LatestDeliveryTime or empty error strings. Contributed by @iot-rocket.GetOpenIdToken claims. the token was a user-pool ID token with an empty audience; it is now issued by https://cognito-identity.amazonaws.com with the identity pool id as aud, the identity id as sub, an amr list naming the login providers and the user-pool sign-in, the pool ARN in https://cognito-identity.amazonaws.com/identity-pool-arn and a 10-minute lifetime. An identity id that no pool knows gets ResourceNotFoundException instead of a token. Contributed by @iot-rocket.CUSTOM_AUTH on clients without ALLOW_ flows. a client created without ExplicitAuthFlows, or with only the legacy names, refused CUSTOM_AUTH; it is now enabled, as on AWS, and a client whose ALLOW_ list leaves it out answers CUSTOM_AUTH is not enabled for the client. as AWS does. Contributed by @iot-rocket.ExplicitAuthFlows is enforced for the password flows under AUTH=true. with AUTH=true a client that lists its auth flows now refuses USER_PASSWORD_AUTH, ADMIN_USER_PASSWORD_AUTH and ADMIN_NO_SRP_AUTH unless the list enables them (the legacy USER_PASSWORD_AUTH and ADMIN_NO_SRP_AUTH names included), with the InvalidParameterException AWS returns. Contributed by @iot-rocket.AUTH=true. GetUser and the other calls that take an AccessToken accepted any JWT carrying a user's sub, including an ID token, a token with an altered payload and a token revoked by GlobalSignOut, AdminUserGlobalSignOut or RevokeToken; with AUTH=true they now answer NotAuthorizedException with Invalid Access Token, Could not verify signature for Access Token or Access Token has been revoked, as AWS does. Contributed by @iot-rocket.AUTH=true. ConfirmSignUp and ConfirmForgotPassword accepted any code, and ConfirmForgotPassword changed the password even when no reset was requested; with AUTH=true they take only the issued code (123456 for sign-up and 654321 for a password reset, as before), answer a wrong one with CodeMismatchException and a code that was never issued with ExpiredCodeException, and ConfirmSignUp refuses a user who is not UNCONFIRMED with NotAuthorizedException, as AWS does. Contributed by @iot-rocket./oauth2/authorize with MetadataFile. a SAML provider created with only MetadataFile was refused with InvalidParameterException; the IdP sign-in URL now comes from IDPSSOEndpoint, then the metadata's HTTP-Redirect SingleSignOnService, then MetadataURL. Contributed by @kjdev.ExpiresIn and the /oauth2/token expires_in were always one hour; they now follow the client's AccessTokenValidity, IdTokenValidity and TokenValidityUnits (one hour when unset, as on AWS), a client created without the two validities no longer reports 60 for them, and an expired token is refused by API Gateway's Cognito and JWT authorizers and, with AUTH=true, an expired access token by Cognito with Access Token has expired. Contributed by @iot-rocket.TagUser and TagRole with department added a second tag beside Department, and UntagUser and UntagRole missed a key spelled differently; user and role tag keys now compare without case and keep the spelling last written, and CreateUser, CreateRole, TagUser and TagRole refuse keys that repeat without case with InvalidInput Duplicate tag keys found. Please note that Tag keys are case insensitive., as does an AWS::IAM::User or AWS::IAM::Role whose stack tag differs from one of its own tags only in case. Other IAM resources keep case-sensitive keys. Contributed by @gakuto-cw21.cryptography and minted the broker certificate before any device connected. It now binds port 8883 without TLS and builds the context on the first connection, and x509_utils imports cryptography on first use: an idle container drops from about 36.7 to 32.8 MiB.RegisterCertificate under an inactive CA. a certificate issued by a CA that is not ACTIVE registered anyway; it is now refused with CertificateStateException, with or without caCertificatePem. An unregistered CA and a CA that did not issue the certificate are refused with AWS's codes and messages. Contributed by @iot-rocket.DeleteThing detached a thing's certificates and deleted it, and DeleteCertificate refused only an ACTIVE certificate. Now DeleteThing refuses a thing with a certificate attached, DeleteCertificate refuses a certificate with things attached and, without forceDelete, one with policies attached (forceDelete detaches them), DeletePolicy refuses a policy with non-default versions and DeleteThingType one that things still use, all with AWS's messages. Contributed by @iot-rocket.CreateTopicRule and ReplaceTopicRule accepted a republish action whose topic the rule's FROM filter matches, so the rule fed on its own output; they now refuse it with AWS's InvalidRequestException, and an AWS::IoT::TopicRule with such an action fails its stack. Contributed by @iot-rocket.principal() and traceid(). both resolved to Undefined; principal() now returns the client certificate id for mTLS, the caller's GetCallerIdentity UserId for HTTP Publish and SigV4 WebSocket sessions (also their lifecycle events' principalIdentifier) and the rule role's session for a republish, and traceid() returns a per-message id that the errorAction document carries as cloudwatchTraceId. Contributed by @iot-rocket.s3:prefix, s3:delimiter and s3:max-keys under AUTH=true. ListObjects, ListObjectsV2 and ListObjectVersions set none of the three condition keys, so a ListBucket or ListBucketVersions grant scoped by them, such as StringLike on s3:prefix for one user's home folder, never matched and the listing answered AccessDenied; they now carry the request's prefix, delimiter and max-keys, path-style and virtual-hosted. Reported by @bettiah.AUTH=true. an X-Amz-Expires over 604800 seconds or not a number, or a malformed X-Amz-Date, answers 400 AuthorizationQueryParametersError, and a URL dated more than 15 minutes ahead answers 403 AccessDenied Request is not yet valid, where they were served. Contributed by @iot-rocket.PutSigningProfile. PutSigningProfile accepted any certificate ARN, or no signingMaterial at all, on every platform; the Lambda and Notation platforms now refuse any signingMaterial (InvalidProfileParameters), and with AUTH=true the IoT and FreeRTOS platforms refuse a profile without signingMaterial and a certificate ARN that is malformed, in another region or account, unknown to ACM or not issued, with AWS's errors. Contributed by @iot-rocket.ListExecutions order and pagination. executions came back oldest first, so maxResults=1 returned the oldest, and nextToken was an offset; they now come back most recent first (running executions by startDate, the rest by stopDate), the token resumes after the last execution returned so one started between pages is not repeated, an unreadable token answers InvalidToken and maxResults=0 uses the default, as on AWS. Contributed by @lucasmfraser.ecs and ecr received an aws-sdk task's PascalCase Parameters as sent, so their handlers ignored them (describeClusters with Clusters described default, describeRepositories with RepositoryNames returned every repository), and sfn and cloudwatchlogs lowercased every key, map keys included, so tagResource stored Team-Name as team-Name; the parameters are now renamed by the operation's input model, with map keys such as tag keys kept as written. Reported by @lucasmfraser.Executions[].executionArn, SecretList[].ARN), timestamps stayed epoch numbers and absent members came back as null; the result is now shaped by the service's output model: structure members renamed at every depth (DynamoDB's BOOL, NULL and SS as Bool, Nul and Ss), map keys such as attribute names and tag keys kept as sent, timestamps as ISO 8601 with milliseconds, and absent members left out. A leading acronym is now lowered in every aws-sdk result, including two-letter names and ones with digits (ID as Id, EC2InstanceId as Ec2InstanceId, MD5OfMessageBody as Md5OfMessageBody). Contributed by @lucasmfraser.Error and Cause expressions as literal text, a JSONata Wait failed on a Seconds expression, ignored a Timestamp expression and dropped its Output and Assign, and JSONPath ErrorPath/CausePath were ignored; they are now evaluated, with AWS's States.QueryEvaluationError or States.Runtime for a result of the wrong type, a Seconds that is not a non-negative integer and an unparsable Timestamp. Contributed by @lucasmfraser.docker pull ministackorg/ministack:1.5.27 docker run -d -p 4566:4566 ministackorg/ministack:1.5.27
Or pin in compose.yaml:
services:
ministack:
image: ministackorg/ministack:1.5.27
ports:
- "4566:4566"
Issues and PRs welcome on GitHub. Discussion on r/ministack.